小言_互联网的博客

从零开始使用华为DevEco Studio编写2048小游戏

431人阅读  评论(0)

写在前面

本文由我和@WiKiBeta共同完成,也是我们第一次接触HarmonyOS,对OS了解的越多,越觉得开发OS是一件不简单的事,开发APP只是其中的一部分,背后的工程实在是有点超出想象。这次我们通过对张荣超老师课程(课程链接)的学习,我们对如何使用IDE开发HOS中的APP有了一定的理解,以下是我们学习完成后写下的笔记,如果有纰漏,希望各位谅解并指出。

概述

本次课程目标是开发能在鸿蒙设备上运行的经典小游戏2048,本次学习实现的功能主要有:1.实现页面布局 2.在画布上显示所有的格子以及格子里的数字 3. 页面初始化时随机选择两个格子,并放入2或4。本次课程中,未完整实现的响应滑动事件功能将在以后的学习笔记中进行补充。(课程使用的开发软件为DevEco Studio, 语言为JavaScript).

准备工作

开发工具

华为HarmonyOS的应用开发工具DevEco Studio下载地址
node.js下载地址

编写位置

文件用途

具体流程

1.	实现页面布局



效果如下

  1. 在画布上显示所有的格子以及格子里的数字

效果如下

  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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场