2048小游戏实训项目


1 项目概述

本项目旨在构建一款可运行在鸿蒙智能手表和手机上的2048小游戏。通过本项目使学生初步掌握在富鸿蒙设备上基于Canvas组件实现绘图的知识和技巧,同时加深理解基于JS UI框架的鸿蒙应用开发模式,掌握通过HML+CSS构建用户界面和利用JavaScript语言实现业务逻辑的基本方法。

2 需求分析

本实训案例需要实现2048小游戏的基本功能,具体包括:

手表版:

手机版:

3 开发步骤

3.1 创建工程

按如下步骤创建初始工程:

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

运行效果如下图所示:

3.2 设计布局

在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
    }
}

运行效果如下图所示:

3.3 绘制方格

在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();
}

运行效果如下图所示:

3.4 添加方格

删除上一步对方格矩阵的初始化:

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();
}

运行效果如下图所示:

3.5 滑动合并

定义左滑处理函数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();
    }
}

运行效果如下图所示:

3.6 游戏终止

在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();
}

运行效果如下图所示:

3.7 统计得分

定义记分函数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;
}

运行效果如下图所示:

3.8 适配手机

修改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);
            }
        }
}

运行效果如下图所示:

4 项目总结

在这个项目中,我们实现了一款面向华为Wearable设备的小游戏——2048,并将其成功地移植到了手机上。虽然华为目前还没有推出基于本地x86处理器的富鸿蒙仿真终端,但借助于远程模拟器,富鸿蒙应用开发者同样可以在DevEco Studio集成开发环境中,高效地编写和调试程序代码。那么如何将我们编写好的程序代码发布到真机上运行,并联机调试呢?我们将在后续课程中和大家分享。

更多精彩,敬请期待……


达内集团C++教学部 2021年7月1日