> 文档中心 > [深大深鸿会]利用DevEco Studio从零开发OpenHarmony小游戏——2048(下)

[深大深鸿会]利用DevEco Studio从零开发OpenHarmony小游戏——2048(下)

从零开发鸿蒙小游戏——2048(下)

  • 前言
  • 概述
  • 项目的实现
    • 滑动事件
    • 移动与合并格子
    • 结束条件的判断
    • 游戏结束画面
  • 源码展示
    • index.hml
    • index.css
    • index.js
  • 尾声

前言

在上一篇文章中我们跟着张荣超老师的视频课程学习了在鸿蒙设备上开发2048小游戏的一部分,目前已经实现的功能有:页面布局、格子显示、随机增添格子、重新开始等。本文我们将继续学习2048游戏的剩下功能。上篇文章链接:从零开发鸿蒙小游戏2048(上)
共同学习的小伙伴:
xxl_connorxian
RichardCwy
Les24601_
JE13543733623
yeswin411

概述

在上一篇文章中我们已经实现了2048小游戏开始游戏时的初始化,也就是我们已经获得了2048游戏每次开始时的状态,我们接下来的目标就是实现小游戏的运行,让游戏真正的动起来。为此我们需要实现一个滑动事件,在每次滑动后屏幕中的格子会改变,同时我们也需要有一个函数可以判断格子是否还有可滑动的方块,来确保每次滑动是可行的,我们也需要一个更新滑动后显示的函数,以及更新当前分数的函数。这样,基本的2048游戏就完成了。接下来我会详细讲解上述函数的实现。

项目的实现

滑动事件

在完成了游戏开始的相关操作后,我们剩下要完成的工作就是整个滑动事件了。在游戏界面已有格子的情况下滑动屏幕,格子会朝滑动方向移动,并合并相同的格子,然后在空位上随机生成一个格子,并判断游戏是否结束。整个事件处理的逻辑非常直观,其中增添新格子的功能我们是已经实现的,我们尚未实现的还有格子的移动与合并函数以及游戏的结束判断函数。

首先我们需要在index.hml文件中为canvas组件添加一个onswipe属性,令其属性值为我们即将在index.js要写的函数"swipeGrids"

<canvas class="canvas" ref="canvas" onswipe="swipeGrids"></canvas>

接着在index.js文件中增添函数swipeGrids,代码如下:

    swipeGrids(event) { let newGrids = this.changeGrids(event.direction);//根据方向移动场上格子,并将格子新的状态赋给一个新数组 if (newGrids.toString() != grids.toString()) {//若两数组不同(格子发生了改变     grids = newGrids;     this.addTwoOrFourToGrids();//增添新的格子     this.drawGrids();//绘制  if (this.isGridsFull() == true && this.isGridsNotMergeable() == true) {//游戏结束  this.gameover();     } }    },

接着我们来看changGrids函数的实现

移动与合并格子

在滑动前,需要定义一个新的4*4元素全为零的数组newGrids用于记录滑动后格子位置以及数值的情况。
以向右滑动为例。当我们向右滑动后,游戏界面上所有格子都需要向右移动,而所有格子共有四行,每一行有四个格子,我们只需要按顺序将每一行的格子向左移动并合并,整个游戏界面就达成了整体向右滑动的目的。我们先利用一个空数组array来按顺序读入那一行中非零元素的数值,这样在数组array中所有格子都是相邻的,中间不会有任何的空的格子。然后我们再判断相邻格子是否有相等的,如果有,则将位置靠前的那个格子数值翻倍,同时将第二个格子变为零。最后将整个array数组的非零元素按顺序记录到newGrids数组中,这样就达成了移动并合并的操作。
在这里插入图片描述
注意:当事件为向左滑动时array从左开始依次读入grids的非零元,并从左边把newGrids的元素值更改为array上的元素值,向右滑动则刚好相反。向上滑动与向下滑动类似。

具体代码如下:

 changeGrids(direction) { let newGrids = [[0, 0, 0, 0],   [0, 0, 0, 0],   [0, 0, 0, 0],   [0, 0, 0, 0]]; 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;//如果为left则从0开始right从3开始,  if (direction == 'right') {      column = 3;  }  for (let i = 0; i < 4; i++) {      if (grids[row][column] != 0) {//把所有非零元依次放入数组中   array.push(grids[row][column]);      }      column += step;//当direction为left时则从0向3递增,为right时则从3向0递减  }  for (let i = 0; i < array.length - 1; i++) {//访问当前元素及他的下一个元素,所有循环次数为length-1      if (array[i] == array[i + 1]) {//判断是否可合并,   array[i] += array[i + 1];//合并,   this.updateCurrentScores(array[i]);//更新分数   array[i + 1] = 0;//合并后参与合并的第二个元素消失   i++;      }  }  column = 0;  if (direction == 'right') {      column = 3;  }  for (const elem of array) {      if (elem != 0) {//跳过array里的空元素   newGrids[row][column] = elem;//把合并后的状态赋给新数组grids,   column += step;      }  }     } } 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) { this.currentScores += gridNum;    },

结束条件的判断

当游戏界面上格子已经满了,并且无论怎么滑动都没有可以合并的格子后,游戏就结束了。判断格子是否是满的可以通过查找grids数组中是否还有0来进行。当满格子后判断是否还有可合并格子则可以通过遍历整个grids数组分别判断其元素是否与它相邻的元素相等来判断。
代码如下:

    isGridsFull() { if (grids.toString().split(",").indexOf("0") == -1) {//split() 方法用于把一个字符串分割成字符串数组。当找不到"0"时则说明全满     return true; } else {     return false; }    },    isGridsNotMergeable() { 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;//一旦有可合并的函数就返回假,不用继续判断      }  }  if (row < 3) {//同理      if (grids[row][column] == grids[row + 1][column]) {   return false;      }  }     } } return true;//    },

游戏结束画面

当场上格子满了且无法合并后,我们希望游戏界面能显示游戏结束的提示,并且场上格子的颜色褪色。
在这里插入图片描述
在index.hml文件中,我们可以将画布组件canvas和一个用于显示游戏结束的文本组件放到一个栈组件中,同时文本在画布的上方,在游玩时设置为不可见,当游戏结束时将其设为可见。同时可以添加一个用于记录褪色后格子数值对应颜色的字典,在游戏结束时将变量colors改为褪色后的字典,再重新绘制一遍场上格子,就达成了使格子褪色的效果。
最后当我们点击重新开始时,我们需要重新将游戏结束文本设为不可见,同时把colors改回原来的颜色字典,然后将当前分重新置零。具体的代码在这里就不进行展示了,想要了解可以查看下方的源码。
完成了上述工作后,我们的2048游戏就大体完成了。

源码展示

index.hml

<div class="container">    <text class="scores"> 最高分:{{bestScores}}    </text>    <text class="scores"> 当前分:{{currentScores}}    </text>    <stack class="stack"> <canvas class="canvas" ref="canvas" onswipe="swipeGrids"></canvas> <div class="subcontainer" show="{{isShow}}">     <text class="gameover">  游戏结束     </text> </div>    </stack>    <input type="button" value="重新开始" class="btn" onclick="restartGame"/></div>

index.css

.container {    flex-direction: column;    justify-content: center;    align-items: center;    width: 454px;    height: 454px;}.scores {    font-size: 18px;    text-align: center;    width: 300px;    height: 20px;    letter-spacing: 0px;    margin-top: 10px;}.stack{    width: 305px;    height: 305px;    margin-top: 10px;}.canvas {    width: 305px;    height: 305px;    background-color: #BBADA0;}.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;}

index.js

var grids;var context;const THEME = {    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;const SIDELEN = 70;const MARGIN = 5;export default {    data: { bestScores: 9818, currentScores: 0, isShow: false    },    onInit() { this.initGrids(); this.addTwoOrFourToGrids(); this.addTwoOrFourToGrids();    },    initGrids() { grids = [[0, 0, 0, 0],   [0, 0, 0, 0],   [0, 0, 0, 0],   [0, 0, 0, 0]];    },    addTwoOrFourToGrids() { 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])  }     } } // array: [[0, 0], [0, 1], [0, 2], [0, 3], [1, 0], [1, 1], [1, 2], [1, 3], [2, 0], [2, 1], [2, 2], [2, 3], [3, 0], [3, 1], [3, 2], [3, 3]] // Math.random(): [0, 1)之间的小数 // Math.random() * 16: [0, 16)之间的小数 // Math.floor(x): 小于等于x的最大整数 // Math.floor(Math.random() * 16):[0, 15]之间的整数 // Math.floor(Math.random() * array.length):[0, array.length-1]之间的整数 let randomIndex = Math.floor(Math.random() * array.length); let row = array[randomIndex][0];    //array相当于2*array.length的二维数组 let column = array[randomIndex][1]; //获得第randomIndex个空位的横纵坐标 if (Math.random() < 0.8) {     grids[row][column] = 2; } else {     grids[row][column] = 4; }    },    onReady() { context = this.$refs.canvas.getContext('2d');//获得2d绘制引擎将其赋值给变量context    },    onShow() { this.drawGrids();    },    drawGrids() { for (let row = 0; row < 4; row++) {     for (let column = 0; column < 4; column++) {  let gridStr = grids[row][column].toString();  context.fillStyle = colors[gridStr]; //绘图填充颜色  let leftTopX = column * (MARGIN + SIDELEN) + MARGIN;  let leftTopY = row * (MARGIN + SIDELEN) + MARGIN;  context.fillRect(leftTopX, leftTopY, SIDELEN, SIDELEN);  context.font = "24px HYQiHei-65S";//设置字体  if (gridStr != "0") {      if (gridStr == "2" || gridStr == "4") {   context.fillStyle = colors["2or4"];//字体颜色      } else {   context.fillStyle = colors["others"];      }      let offsetX = (4 - gridStr.length) * (SIDELEN / 8);      let offsetY = (SIDELEN - 24) / 2;      context.fillText(gridStr, leftTopX + offsetX, leftTopY + offsetY);//绘制字体  }     } }    },    swipeGrids(event) { let newGrids = this.changeGrids(event.direction);//根据方向移动场上格子,并将格子新的状态赋给一个新数组 if (newGrids.toString() != grids.toString()) {//若两数组不同(格子发生了改变     grids = newGrids;     this.addTwoOrFourToGrids();//增添新的格子     this.drawGrids();//绘制     if (this.isGridsFull() == true && this.isGridsNotMergeable() == true) {//游戏结束  this.gameover();     } }    },    gameover(){ colors = THEME.faded; this.drawGrids(); this.isShow = true;    },    isGridsFull() { if (grids.toString().split(",").indexOf("0") == -1) {//split() 方法用于把一个字符串分割成字符串数组。当找不到"0"时则说明全满     return true; } else {     return false; }    },    isGridsNotMergeable() {//用于判断结束条件 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;//一旦有可合并的函数就返回假,不用继续判断      }  }  if (row < 3) {//同理      if (grids[row][column] == grids[row + 1][column]) {   return false;      }  }     } } return true;//    },    changeGrids(direction) { let newGrids = [[0, 0, 0, 0],   [0, 0, 0, 0],   [0, 0, 0, 0],   [0, 0, 0, 0]]; 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;//如果为left则从0开始right从3开始,  if (direction == 'right') {      column = 3;  }  for (let i = 0; i < 4; i++) {      if (grids[row][column] != 0) {//把所有非零元依次放入数组中   array.push(grids[row][column]);      }      column += step;//当direction为left时则从0向3递增,为right时则从3向0递减  }  for (let i = 0; i < array.length - 1; i++) {//访问当前元素及他的下一个元素,所有循环次数为length-1      if (array[i] == array[i + 1]) {//判断是否可合并,   array[i] += array[i + 1];//合并,   this.updateCurrentScores(array[i]);//更新分数   array[i + 1] = 0;//合并后参与合并的第二个元素消失   i++;      }  }  column = 0;  if (direction == 'right') {      column = 3;  }  for (const elem of array) {      if (elem != 0) {//跳过array里的空元素   newGrids[row][column] = elem;//把合并后的状态赋给新数组grids,   column += step;      }  }     } } 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) { this.currentScores += gridNum;    },    restartGame() { this.currentScores = 0; this.isShow = false; colors = THEME.normal; this.initGrids(); this.addTwoOrFourToGrids(); this.addTwoOrFourToGrids(); this.drawGrids();    }}

尾声

以上便是我们关于鸿蒙2048小游戏的全部学习笔记了。我们所学习的游戏源码是由张荣超老师所编写,我们在张老师源码的基础上进行学习并复现,最终通过写这篇解读性博客的形式,来加深自己的记忆。也希望这篇文章能使更多的和我们一样的学习者看到了能有所收获,如果有大佬看见了也希望大佬能够斧正。最后再次感谢张荣超老师的教程、以及欧JS师兄的引领、王bh老师的教导。愿共勉之。