写在前面
本文由我和@WiKiBeta共同完成,也是我们第一次接触HarmonyOS,对OS了解的越多,越觉得开发OS是一件不简单的事,开发APP只是其中的一部分,背后的工程实在是有点超出想象。这次我们通过对张荣超老师课程(课程链接)的学习,我们对如何使用IDE开发HOS中的APP有了一定的理解,以下是我们学习完成后写下的笔记,如果有纰漏,希望各位谅解并指出。
概述
本次课程目标是开发能在鸿蒙设备上运行的经典小游戏2048,本次学习实现的功能主要有:1.实现页面布局 2.在画布上显示所有的格子以及格子里的数字 3. 页面初始化时随机选择两个格子,并放入2或4。本次课程中,未完整实现的响应滑动事件功能将在以后的学习笔记中进行补充。(课程使用的开发软件为DevEco Studio, 语言为JavaScript).
准备工作
开发工具
华为HarmonyOS的应用开发工具DevEco Studio下载地址
node.js下载地址
编写位置
文件用途
具体流程
1. 实现页面布局
效果如下
- 在画布上显示所有的格子以及格子里的数字
效果如下
- 页面初始化时随机选择两个格子,并放入2或4
页面初始化:
随机选择两个格子,并放入2或4:
效果如下:
源代码(详解)
hml
<div class="container">
<text class="scores">
最高分:{
{
bestScores}}//将bestScores与最高分动态绑定,即bestScores可变
</text>
<text class="scores">
当前分:{
{
currentScores}}//cunrrentScores与最高分动态绑定,即bestScores可变
</text>
<stack class="stack">
<canvas class="canvas" ref="canvas" onswipe="swipeGrids">
//定义canvas组件,类为canvas,ref指向canvas对象实例,onswipe指向“滑动”这一事件
</canvas>
<div class="subcontainer" show="{
{isShow}}">
<text class="gameover">
游戏结束
</text>
</div>
</stack>
<input type="button" value="重新开始" class="btn" onclick="restartGame"/>//输入一个组件input,定义种类为button以显示一个按钮,value即按钮上将显示的文本,类定义为btn
</div>//以上就定义了我们有哪些组件
css
.container {
//整个界面的基本布局
flex-direction: column;//将界面中的组件竖向排列
width: 454px;
height: 454px;
justify-content: center;//justify-content 用于设置或检索弹性盒子元素在主轴(横轴)方向上的对齐方式,使其中心化
align-items: center;//align-content 属性对齐交叉轴上的各项(垂直),这里使其中心化
}
.scores {
width: 300px;
height:20px;
font-size: 18px;
text-align: center;
letter-spacing: 0px;//使类为scores的元素排列的更加紧凑
margin-top: 10px;//在类为scores的元素周围设置10px的外边距
}
.stack{
width: 305px;
height: 305px;
margin-top: 10px;
}
.canvas{
width:305px;
height:305px;
background-color: #BBADA0;//背景色,16进制
}
.subcontainer {
width: 305px;
height: 305px;
justify-content: center;
align-items: center;
background-color: transparent;
}
.gameover {
font-size: 38px;
color: black;
}
.btn{
width:150px;
height:30px;
background-color: #AD9D8F;
font-size: 24px;
margin-top: 10px;
}
js
var grids;//设置变量grids
var context;//使context作为全局变量,因为使用频率较高
const THEME = {
//设置常量theme,其中含有子集normal和faded,用于填充字体颜色和网格背景色,normal用于平常,faded仅在游戏结束后生效
normal: {
"0": "#CDC1B4",
"2": "#EEE4DA",
"4": "#EDE0C8",
"8": "#F2B179",
"16": "#F59563",
"32": "#F67C5F",
"64": "#F65E3B",
"128": "#EDCF72",
"256": "#EDCC61",
"512": "#99CC00",
"1024": "#83AF9B",
"2048": "#0099CC",
"2or4": "#645B52",
"others": "#FFFFFF"
},
faded: {
"0": "#D4C8BD",
"2": "#EDE3DA",
"4": "#EDE1D1",
"8": "#F0CBAA",
"16": "#F1BC9F",
"32": "#F1AF9D",
"64": "#F1A08B",
"128": "#EDD9A6",
"256": "#F6E5B0",
"512": "#CDFF3F",
"1024": "#CADCD4",
"2048": "#75DBFF",
"2or4": "#645B52",
"others": "#FFFFFF"
}
};
var colors = THEME.normal;//normal中的颜色使用频率较高,因为单独定义一个变量使其指定normal
const MARGIN =5;//定义常量MARGIN,为girds中grid与grid的间距
const SIDELEN=70;//grid的边长
export default {
//export default命令,为模块指定默认输出
data: {
currentScores: 0,//默认值为0
bestScores: 9818
isShow: false//用于设定在游戏结束之前subcantainer中的东西都不会显示,由于isShow为动态绑定,所以在游戏结束时可以通过改变false->True使subcantainer中的东西显示出来
},
onInit(){
//初始化游戏界面:
this.initGrids();//调用initGrids,使所有网格的填充色为字符0元素的背景色,而字符0本身没有颜色,从而使界面看起来像清空
this.addTwoOrFourToGrids();//在grids中任意指定一个grid使其为2or4
this.addTwoOrFourToGrids();
},
initGrids(){
//使所有网格的填充色为字符0元素的背景色,而字符0本身没有颜色,从而使界面看起来像清空
grids=[[0,0,0,0],
[0,0,0,0],
[0,0,0,0],
[0,0,0,0]];
},
onReady(){
//首次显示页面,页面初次渲染完成,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互(用于渲染)
context=this.$refs.canvas.getContext("2d");//获得canvas对应的2d绘制引擎,并将其赋值给全局变量context
},
onShow(){
//页面载入后触发onShow方法,显示页面。每次打开页面都会调用一次(用于显示)
this.drawGrids();//给界面上色,绘制grids
},
drawGrids() {
//绘制grids
for (let row = 0; row < 4; row++) {
for (let column = 0; column < 4; column++) {
//遍历所有grid
let gridStr = grids[row][column].toString();//将grid上的数字转化为字符串
context.fillStyle = colors[gridStr];//网格的填充色,根据字符串来定,以context绘图实现
let leftTopX = column * (MARGIN + SIDELEN) + MARGIN;//grid左上角的横坐标
let leftTopY = row * (MARGIN + SIDELEN) + MARGIN;//grid左上角的纵坐标
context.fillRect(leftTopX, leftTopY, SIDELEN, SIDELEN);//定义绘制的范围,四个参数,绘制出矩形
context.font = "24px HYQiHei-65S";//绘制的字体形式
if (gridStr != "0") {
//“0”不用绘制,融于背景色
if (gridStr == "2" || gridStr == "4") {
context.fillStyle = colors["2or4"];
} else {
context.fillStyle = colors["others"];
}
let offsetX = (4 - gridStr.length) * (SIDELEN / 8);//str左上角与gridX方向上的间距,四个字符占的长度=SIDELEN
let offsetY = (SIDELEN - 24) / 2;str左上角与gridY方向上的间距
context.fillText(gridStr, leftTopX + offsetX, leftTopY + offsetY);//接收gird上的字符串以及str左上角的横坐标以及纵坐标并以设置好的context.font将其进行绘制
}
}
}
},
addTwoOrFourToGrids(){
//在初始化或restart时选中两个grid作为最开始时出现的grid,其值为2or4
let array=[];
for(let row =0;row<4;row++){
for(let column=0;column<4;column++){
if(grids[row][column]==0){
array.push([row,column]);//遍历并存储网格上数字为0的网格位置(为什么要找0?因为init和swipeGrids中都会用到,所以不能省略找0的步骤)
}
}
}
let randomIndes=Math.floor(Math.random()*array.length);//取[0,array.length-1]之间的任意一个整数
let row=array[randomIndes][0];//索引中数组为0的元素为行索引,实际上是当后面的索引为[0],前面的索引为[randomIndes]时,array[randomIndes][0]=0,1,2,3的可能是相等的,因此相当于使row从中去一个作为行索引
let column=array[randomIndes][1];//索引中数组为1的元素为列索引
if(Math.random()<0.8){
//使出现2的概率大于4,选定一个grid,使其的值为2or4
grids[row][column]=2;
}else{
grids[row][column]=4;
}
},
swipeGrids(event) {
let newGrids = this.changeGrids(event.direction);//调用和changeGrids()函数,使newGrids为经过滑动后产生的新Grids
if (newGrids.toString() != grids.toString()) {
grids = newGrids;将滑动前的girds变为滑动后的newGrids,用newGrids不方便已经设置好的其他操作
this.addTwoOrFourToGrids();//滑动后随机产生一个值为2or4的grid
this.drawGrids();//由于滑动后grids上的值已经变了,但整体的绘制仍然为未滑动前的样式,所以需要调用drawGrids函数将新得到的grids进行绘制
if (this.isGridsFull() == true && this.isGridsNotMergeable() == true) {
//每次滑动都要检验游戏是否达到可以结束的标准(格子都满了且相邻格子不能进行融合同时满足),如果满足则进行谢列步骤
colors = THEME.faded;//将faded子集中的颜色赋值给color
this.drawGrids();//重新绘制,将grids的整体色调都调成褪色(faded)状态
this.isShow = true;//改变isShow的值,改变hml中‘show’的状态,使subcantainer中的东西可以得到展示(gameover字体显示在界面上,结束前一直是“transparent”(透明))
}
}
},
changeGrids(direction) {
//接受由机器感应后传至的方向参数,从而对每个方向的滑动做出相应具体的操作,返回值为滑动后新生成的newGrids
let newGrids = [[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]];//先生成空的Grids,在之后的操作会让所有应该得到现实的格子得到显示
if (direction == 'left' || direction == 'right') {
//不用或的话将其分开也可以,这样的目的是简化代码
let step = 1;
if (direction == 'right') {
step = -1;
}//方向不同,step不同,+-用于方面下面把格子往不同的方向移动,起到方向引导的作用
for (let row = 0; row < 4; row++) {
//左右平行移动,所以一行一行的进行操作
let array = [];
let column = 0;
if (direction == 'right') {
column = 3;
}//column=0表示最左边的列,=3表示最右边的列,游戏实现的必要,因为左滑要把非0格子向column=0的那一列推
for (let i = 0; i < 4; i++) {
if (grids[row][column] != 0) {
//把非0格子上的数字都推进array中
array.push(grids[row][column]);
}
column += step;//每循环一次column+1,从而达到遍历那一行每一列的目的
}
for (let i = 0; i < array.length - 1; i++) {
//这个循环的结果以direction=left为例,[2,2,4,2]->[4,0,4,2];[[2,2,2,2]->[4,0,4,0],文字解释比较难,所以我以产生结果的过程作为例子,希望大家能明白
if (array[i] == array[i + 1]) {
array[i] += array[i + 1];
this.updateCurrentScores(array[i]);//更新当前分,每有两个格子合并,合并后格子上的数字即为要加上的分数
array[i + 1] = 0;
i++;
}
}
column = 0;//重新将column定义为0或3,方便下面的循环实现“移动”非0格的操作
if (direction == 'right') {
column = 3;
}
for (const elem of array) {
//遍历array中的元素
if (elem != 0) {
newGrids[row][column] = elem;
//至此才对newGrids做出改变,在这之前它一直都是全0,由[2,2,4,2]->[4,0,4,2];[[2,2,2,2]->[4,0,4,0]我们列出这一步的结果在newGrids上为[0,0,0,0]->[4,4,2,0];[0,0,0,0]->[4,4,0,0](注意:原来newGrids这一行都是0,并不是直接用这个[4,0,4,2]或这个[4,0,4,0]做出改变,此处需要多加理解,这个changeGrids()函数是相对难理解的)
column += step;//要注意取到非0元素列数column才加step=1,取得0时如果加上会出现空的格子
}
}
}//我们以“left”这个方向做出了详细解答,其他的方向原理是一模一样的,其他方向看不明白的话再重新回顾left方向就可以了
} else if (direction == 'up' || direction == 'down') {
let step = 1;
if (direction == 'down') {
step = -1;
}
for (let column = 0; column < 4; column++) {
let array = [];
let row = 0;
if (direction == 'down') {
row = 3;
}
for (let i = 0; i < 4; i++) {
if (grids[row][column] != 0) {
array.push(grids[row][column]);
}
row += step;
}
for (let i = 0; i < array.length - 1; i++) {
if (array[i] == array[i + 1]) {
array[i] += array[i + 1];
this.updateCurrentScores(array[i]);
array[i + 1] = 0;
i++;
}
}
row = 0;
if (direction == 'down') {
row = 3;
}
for (const elem of array) {
if (elem != 0) {
newGrids[row][column] = elem;
row += step;
}
}
}
}
return newGrids;
},
updateCurrentScores(gridNum) {
//由于动态绑定的缘故,currentScores可以实时更新
this.currentScores += gridNum;
},
isGridsFull() {
//判断grids中还有没有“0”,没有,返回True,否则,返回false
if ( grids.toString().split(",").indexOf("0") == -1) {
return true;
} else {
return false;
}
},
isGridsNotMergeable() {
//判断grids中还存不存在有grid融合的可能
for (let row = 0; row < 4; row++) {
for (let column = 0; column < 4; column++) {
if (column < 3) {
if (grids[row][column] == grids[row][column +1]) {
return false;//判断每一行相邻的两个grid是否相等,有相等的说明存在融合的可能,则返回false
}
}
if (row < 3) {
if (grids[row][column] == grids[row + 1][column]) {
return false;//判断每一列相邻的两个grid是否相等,有相等的说明存在融合的可能,则返回false
}
}
}
}
return true;//各个grid所蕴含的元素与周围的元素都不相同,说明没有融合的可能,返回True
},
restartGame(){
//相应点击按钮这一事件,重新开始游戏
this.initGrids();
this.addTwoOrFourToGrids();
this.addTwoOrFourToGrids();
this.drawGrids();
}
}
写在后面
由于张老师的课程时间限制的原因,还未实现所有的功能,在张老师推出完整教程后我们可能会写出完整的版本。以上就是我们的学习笔记,希望大家看完能有所收获,谢谢。在这里感谢张荣超老师精彩细致的讲解,让我们这些小白也能有所体会,同时也感谢ojs师兄,lcz老师,和wbh老师带领我们进入这个领域,让我们有幸接触到HarmonyOS,最后,希望我们能和大家一起进步,将来写出更好的代码。
转载:https://blog.csdn.net/Brian_Leeee/article/details/109753448
查看评论