本项目旨在构建一款可运行在鸿蒙智能手表和手机上的2048小游戏。通过本项目使学生初步掌握在富鸿蒙设备上基于Canvas组件实现绘图的知识和技巧,同时加深理解基于JS UI框架的鸿蒙应用开发模式,掌握通过HML+CSS构建用户界面和利用JavaScript语言实现业务逻辑的基本方法。
本实训案例需要实现2048小游戏的基本功能,具体包括:
手表版:
手机版:
按如下步骤创建初始工程:
DevEco Studio
Create HarmonyOS Project
Empty Ability(JS)
Project Name: Game2048
Project Type: Application
Package Name: com.hostest.game2048
Save Location: C:\HOSTest\Game2048
Compatible API Version: SDK: API Version 5
Device Type: Wearable
运行效果如下图所示:
在entry\src\main\js\default\i18n\en-US.json文件中添加英文版字符串:
{ "strings": { "best": "Best", "score": "Score", "restart": "Restart" } }
在entry\src\main\js\default\i18n\zh-CN.json文件中添加中文版字符串:
{ "strings": { "best": "最高分", "score": "当前分", "restart": "重新开始" } }
在entry\src\main\js\default\pages\index\index.hml文件中设计界面布局:
<div class="container"> <text class="txt">{{$t("strings.best")}}: {{best}}</text> <text class="txt">{{$t("strings.score")}}: {{score}}</text> <canvas class="cvs"></canvas> <input class="btn" type="button" value="{{$t('strings.restart')}}" /> </div>
在entry\src\main\js\default\pages\index\index.css文件中设计界面样式:
.container { flex-direction: column; justify-content: center; align-items: center; }
@media screen and (device-type: wearable) { .txt { width: 80px; height: 16px; font-size: 12px; text-align: center; } .cvs { width: 150px; height: 150px; margin-top: 4px; background-color: #bbada0; } .btn { width: 80px; height: 22px; font-size: 14px; margin-top: 6px; background-color: #ff7f27; } }
在entry\src\main\js\default\pages\index\index.js文件中定义动态数据:
export default { data: { best: 0, score: 0 } }
运行效果如下图所示:
在entry\src\main\js\default\pages\index\index.hml文件中为canvas组件添加引用名:
<div class="container"> ... <canvas class="cvs" ref="cvs"></canvas> ... </div>
定义颜色表:
const colors = { "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", "OTHS": "#ffffff"};
定义方格矩阵:
var cells = [ [ 0, 2, 4, 8], [ 16, 32, 64, 128], [256, 512, 1024, 2048], [ 8, 4, 2, 0]];
定义绘制方格函数drawCells(),并在onShow()回调中调用该函数:
drawCells() { let canvas = this.$refs.cvs.getContext("2d"); for (let i = 0; i < 4; ++i) for (let j = 0; j < 4; ++j) { let t = cells[i][j].toString(); canvas.fillStyle = colors[t]; let b = 2, s = 35; let x = b + j * (s + b), y = b + i * (s + b); canvas.fillRect(x, y, s, s); if (t != "0") { if (t == "2" || t == "4") canvas.fillStyle = colors["2OR4"]; else canvas.fillStyle = colors["OTHS"]; let h = 14, w = h * 9 / 16; canvas.font = h + "px HYQiHei-655"; x += (s - t.length * w) / 2; y += (s + h) / 2; canvas.fillText(t, x, y); } } }
onShow() { this.drawCells(); }
运行效果如下图所示:
删除上一步对方格矩阵的初始化:
var cells;
定义方格矩阵初始化函数initCells():
initCells() { cells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; }
定义在随机空格处随机添加2或4的函数addCell(),并使添加2的概率略大:
addCell() { let nils = []; for (let i = 0; i < 4; ++i) for (let j = 0; j < 4; ++j) if (cells[i][j] == 0) nils.push([i,j]); let nil = nils[Math.floor(Math.random() * nils.length)]; if (Math.random() < 0.8) cells[nil[0]][nil[1]] = 2; else cells[nil[0]][nil[1]] = 4; }
在onInit()回调中调用initCells()函数,初始化方格矩阵,并通过addCell()函数产生两个初始方格:
onInit() { this.initCells(); this.addCell(); this.addCell(); }
为“重新开始”按钮添加点击事件处理函数onClick(),重置游戏场景:
<input class="btn" type="button" value="{{$t('strings.restart')}}"
onclick="onClick"/>
onClick() { this.initCells(); this.addCell(); this.addCell(); this.drawCells(); }
运行效果如下图所示:
定义左滑处理函数swipeLeft():
swipeLeft() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 0; j < 4; ++j) if (cells[i][j] != 0) merge.push(cells[i][j]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; merge[j+1] = 0; ++j; } let j = 0; for (const elem of merge) if (elem != 0) { newCells[i][j] = elem; ++j; } } return newCells; }
定义右滑处理函数swipeRight():
swipeRight() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 3; j >= 0; --j) if (cells[i][j] != 0) merge.push(cells[i][j]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; merge[j+1] = 0; ++j; } let j = 3; for (const elem of merge) if (elem != 0) { newCells[i][j] = elem; --j; } } return newCells; }
定义上滑处理函数swipeUp():
swipeUp() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 0; j < 4; ++j) if (cells[j][i] != 0) merge.push(cells[j][i]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; merge[j+1] = 0; ++j; } let j = 0; for (const elem of merge) if (elem != 0) { newCells[j][i] = elem; ++j; } } return newCells; }
定义下滑处理函数swipeDown():
swipeDown() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 3; j >= 0; --j) if (cells[j][i] != 0) merge.push(cells[j][i]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; merge[j+1] = 0; ++j; } let j = 3; for (const elem of merge) if (elem != 0) { newCells[j][i] = elem; --j; } } return newCells; }
定义滑动处理函数swipeCells():
swipeCells(direction) { switch (direction) { case "left": return this.swipeLeft(); case "right": return this.swipeRight(); case "up": return this.swipeUp(); case "down": return this.swipeDown(); } }
为canvas组件添加滑动事件处理函数onSwipe(),处理滑动:
<canvas class="cvs" ref="cvs" onswipe="onSwipe" ></canvas>
onSwipe(e) { let newCells = this.swipeCells(e.direction); if (cells.toString() != newCells.toString()) { cells = newCells; this.addCell(); this.drawCells(); } }
运行效果如下图所示:
在en-US.json和zh-CN.json文件中添加名为“gameover”的字符串:
{ "strings": { "best": "Best", "score": "Score", "gameover": "Gameover", "restart": "Restart" } }
{ "strings": { "best": "最高分", "score": "当前分", "gameover": "游戏结束", "restart": "重新开始" } }
修改index.hml文件,添加“Gameover”图层:
<div class="divPage"> <text class="txtScore">{{$t("strings.best")}}: {{best}}</text> <text class="txtScore">{{$t("strings.score")}}: {{score}}</text> <stack class="stk"> <canvas class="cvs" ref="cvs" onswipe="onSwipe" ></canvas> <div class="divGameover" show="{{gameover}}"> <text class="txtGameover">{{$t("strings.gameover")}}</text> </div> </stack> <input class="btn" type="button" value="{{$t('strings.restart')}}" onclick="onClick"/> </div>
修改index.css文件中的样式描述:
.divPage { flex-direction: column; justify-content: center; align-items: center; }
@media screen and (device-type: wearable) { .txtScore { width: 80px; height: 16px; font-size: 12px; text-align: center; } .stk { width: 150px; height: 150px; margin-top: 4px; } .cvs { width: 150px; height: 150px; background-color: #bbada0; } .divGameover { width: 150px; height: 150px; justify-content: center; align-items: center; background-color: transparent; } .txtGameover { font-size: 28px; color: #a00000; } .btn { width: 80px; height: 22px; font-size: 14px; margin-top: 6px; background-color: #ff7f27; } }
修改index.js文件中有关颜色表的定义,以区分正常和消褪两个颜色主题,初始为正常色:
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", "OTHS": "#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", "OTHS": "#ffffff"}};
var colors = theme.normal;
增加用于控制“Gameover”图层显隐的布尔型动态数据:
data: { best: 0, score: 0, gameover: false }
定义判断所有方格是否都被填满的函数full():
full() { return cells.toString().split(",").indexOf("0") == -1; }
定义判断是否还有合并可能的函数mergeable():
mergeable() { for (let i = 0; i < 4; ++i) for (let j = 0; j < 4; ++j) { if (i < 3 && cells[i][j] == cells[i+1][j]) return true; if (j < 3 && cells[i][j] == cells[i][j+1]) return true; } return false; }
定义宣告游戏结束的函数over():
over(){ colors = theme.faded; this.gameover = true; }
定义宣告重新开始的函数reset():
reset() { colors = theme.normal; this.gameover = false; }
修改canvas组件的滑动事件处理函数onSwipe(),每次滑动后判断是否终止游戏:
onSwipe(e) { let newCells = this.swipeCells(e.direction); if (cells.toString() != newCells.toString()) { cells = newCells; this.addCell(); if (this.full() == true && this.mergeable() == false) this.over(); this.drawCells(); } }
修改“重新开始”按钮的点击事件处理函数onClick(),增加对reset()函数的调用:
onClick() { this.reset(); this.initCells(); this.addCell(); this.addCell(); this.drawCells(); }
运行效果如下图所示:
定义记分函数award():
award(score) { this.score += score; if (this.best < this.score) this.best = this.score; }
在四向滑动函数中增加对award()函数的调用:
swipeLeft() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 0; j < 4; ++j) if (cells[i][j] != 0) merge.push(cells[i][j]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; this.award(merge[j]); merge[j+1] = 0; ++j; } let j = 0; for (const elem of merge) if (elem != 0) { newCells[i][j] = elem; ++j; } } return newCells; }
swipeRight() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 3; j >= 0; --j) if (cells[i][j] != 0) merge.push(cells[i][j]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; this.award(merge[j]); merge[j+1] = 0; ++j; } let j = 3; for (const elem of merge) if (elem != 0) { newCells[i][j] = elem; --j; } } return newCells; }
swipeUp() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 0; j < 4; ++j) if (cells[j][i] != 0) merge.push(cells[j][i]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; this.award(merge[j]); merge[j+1] = 0; ++j; } let j = 0; for (const elem of merge) if (elem != 0) { newCells[j][i] = elem; ++j; } } return newCells; }
swipeDown() { let newCells = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; ++i) { let merge = []; for (let j = 3; j >= 0; --j) if (cells[j][i] != 0) merge.push(cells[j][i]); for (let j = 0; j < merge.length - 1; ++j) if (merge[j] == merge[j+1]) { merge[j] += merge[j+1]; this.award(merge[j]); merge[j+1] = 0; ++j; } let j = 3; for (const elem of merge) if (elem != 0) { newCells[j][i] = elem; --j; } } return newCells; }
重新开始游戏,分数清零:
reset() { colors = theme.normal; this.gameover = false; this.score = 0; }
运行效果如下图所示:
修改entry\src\main\config.json文件中设备类型:
... "module": { ... "deviceType": [ "phone" ] ... } ...
修改entry\src\main\js\default\pages\index\index.css文件中的样式描述:
@media screen and (device-type: phone) { .txtScore { width: 160px; height: 32px; font-size: 24px; text-align: center; } .stk { width: 300px; height: 300px; margin-top: 8px; } .cvs { width: 300px; height: 300px; background-color: #bbada0; } .divGameover { width: 300px; height: 300px; justify-content: center; align-items: center; background-color: transparent; } .txtGameover { font-size: 56px; color: #a00000; } .btn { width: 160px; height: 44px; font-size: 28px; margin-top: 12px; background-color: #ff7f27; } }
修改entry\src\main\js\default\pages\index\index.js文件中的绘制方格函数drawCells():
drawCells() { let canvas = this.$refs.cvs.getContext("2d"); for (let i = 0; i < 4; ++i) for (let j = 0; j < 4; ++j) { let t = cells[i][j].toString(); canvas.fillStyle = colors[t]; let b = 4, s = 70; let x = b + j * (s + b), y = b + i * (s + b); canvas.fillRect(x, y, s, s); if (t != "0") { if (t == "2" || t == "4") canvas.fillStyle = colors["2OR4"]; else canvas.fillStyle = colors["OTHS"]; let h = 28, w = h * 9 / 16; canvas.font = h + "px HYQiHei-655"; x += (s - t.length * w) / 2; y += (s + h) / 2; canvas.fillText(t, x, y); } } }
运行效果如下图所示:
在这个项目中,我们实现了一款面向华为Wearable设备的小游戏——2048,并将其成功地移植到了手机上。虽然华为目前还没有推出基于本地x86处理器的富鸿蒙仿真终端,但借助于远程模拟器,富鸿蒙应用开发者同样可以在DevEco Studio集成开发环境中,高效地编写和调试程序代码。那么如何将我们编写好的程序代码发布到真机上运行,并联机调试呢?我们将在后续课程中和大家分享。