HarmonyOS - 基于ArkUI(JS)实现推箱子游戏 原创 精华

中软国际鸿蒙生态
发布于 2022-8-5 15:15
浏览
3收藏

作者 : 郝志鹏

前言

从零开始学习HarmonyOS开发相关知识,对于从没接触过的FA感到无从下手。单纯看文档,练习文档中的组件提升不大,突发奇想决定结合ArkUI(JS)和canvas实现一个简单的推箱子小游戏,来深入学习FA的实际应用。

实现效果

HarmonyOS - 基于ArkUI(JS)实现推箱子游戏-鸿蒙开发者社区

游戏说明

​ 1. 通过操作上、下、左、右四个按钮移动皮卡丘;

​ 2. 当皮卡丘对应的运动方向为墙壁时不可移动;

​ 3. 当皮卡丘对应的运动方向为一个箱子时,皮卡丘和箱子同时向对应方向移动一个单位;

​ 4. 当皮卡丘对应运动方向相邻的箱子后为另一个箱子或墙壁时,皮卡丘和箱子皆无法移动;

​ 5. 点击上一步可回退到上一步操作;

​ 6. 点击重置按钮可回退到初始位置;

​ 7. 当所有箱子都移动到圆形阴影所在位置,即可通关游戏;

项目说明

主要用到知识点:canvas, showToast

进行图像绘制

drawImage(image: Image, sx: number, sy: number, sWidth: number, sHeight: number, dx: number, dy: number, dWidth: number, dHeight: number):void

参数 类型 描述
image Image 图片资源,请参考Image对象
sx number 裁切源图像时距离源图像左上角的x坐标值。
sy number 裁切源图像时距离源图像左上角的y坐标值。
sWidth number 裁切源图像时需要裁切的宽度。
sHeight number 裁切源图像时需要裁切的高度。
dx number 绘制区域左上角在x轴的位置。
dy number 绘制区域左上角在y 轴的位置。
dWidth number 绘制区域的宽度。
dHeight number 绘制区域的高度。

删除指定区域内的绘制内容

clearRect(x: number, y: number, width:number, height: number): void

参数 类型 描述
x number 指定矩形上的左上角x坐标。
y number 指定矩形上的左上角y坐标。
width number 指定矩形的宽度。
height number 指定矩形的高度。

实现步骤

​ 1. 以画布左上角作为原点坐标(0, 0);

​ 2. 根据墙壁对应数组的坐标绘制墙壁,同理依次绘制圆形阴影、箱子、皮卡丘的位置;

​ 3. 上、下、左、右四个按钮绑定对应点击事件,用来移动皮卡丘;

​ 4. 当移动皮卡丘时进行碰撞检测的判断(是否碰到墙壁、是否碰到箱子、是否可以推动箱子、箱子的坐标位置是否与圆形阴影重合);

​ 5. 每次移动箱子判断,是否所有箱子都与圆形阴影重合,如果全部重合则代表游戏通关,弹出toast提示;

代码实现

1. hml部分

<div class="container">
    <canvas id='canvas'></canvas>
    <div class="btn-list">
        <div class='btn-list-item'>
            <div class='btn-item up'>
                <button class='btn' @click="handleUp">上</button>
            </div>
            <div class='middle'>
                <div class='btn-item left'>
                    <button class='btn' @click="handleLeft">左</button>
                </div>
                <div class='btn-item right'>
                    <button class='btn' @click="handleRight">右</button>
                </div>
            </div>
            <div class='btn-item down'>
                <button class='btn' @click="handleDown">下</button>
            </div>
        </div>
        <div class="btn-list-item">
            <div class='btn-item'>
                <button class='btn' @click="handleBack">上一步</button>
            </div>
            <div class='btn-item'>
                <button class='btn' @click="handleReset">重置</button>
            </div>
        </div>
    </div>
</div>

2. css部分

.container {
    flex-direction: column;
    padding: 5px;
}
#canvas {
    width: 100%;
    height: 500px;
}
.btn-list {
    justify-content: space-between;
}
.btn-list-item {
    width: 48%;
    flex-direction: column;
    justify-content: center;
}
.middle {
    justify-content: space-around;
}
.btn-item {
    flex: 1;
    justify-content: center;
    align-items: center;
}
.btn-item .btn {
    margin-top: 5px;
    padding: 10px 15px;
}

3. js部分

import prompt from '@system.prompt'; //引入prompt,在通关后弹出toast提示
export default {
    data: {
        walls: [ // 墙壁坐标,可根据地图进行自定义设置
            [0, 0], [1, 0], [2, 0], [3, 0], [4, 0],
            [0, 1], [4, 1],
            [0, 2], [2, 2], [4, 2],
            [0, 3], [4, 3],
            [0, 4], [2, 4], [4, 4], [5, 4], [6, 4],
            [0, 5], [6, 5],
            [0, 6], [6, 6],
            [0, 7], [1, 7], [2, 7], [3, 7], [4, 7], [5, 7], [6, 7]
        ],
        boxPos: [[3, 2], [3, 5], [4, 6]], // 箱子坐标,可根据地图进行自定义设置
        overPos: [[1, 1], [1, 5], [3, 6]], // 终点位置坐标,可根据地图进行自定义设置
        personPos: [3, 6], // 玩偶坐标,可根据地图进行自定义设置
        drawCanvas: '',
        boxHistory: ['[[3, 2], [3, 5], [4, 6]]'], // 箱子移动轨迹存储的历史记录
        personHistory: ['[3, 6]'], // 玩偶移动轨迹存储的历史记录
        winGame: false, // 游戏是否通关
    },
    onShow(){
        this.initCanvas()
    },
    initCanvas() {
        let canvas = this.$element('canvas')
        if(!canvas) return
        this.drawCanvas = canvas.getContext('2d', { antialias: true })
        this.initWalls()
        this.initOverPos()
        this.initBoxPos()
        this.initPersonPos()
    },
    initWalls() {
        let img = new Image();
        img.src = 'common/images/game/wall.jpg';
        // 循环遍历墙壁坐标点,使用drawImage绘制图片
        this.walls.forEach(item => {
            this.drawCanvas.drawImage(img, item[0] * 50, item[1] * 50, 50, 50);
        })
    },
    initBoxPos() {
        let self = this
        let img = new Image();
        img.src = 'common/images/game/box.png';
        let overImg = new Image();
        overImg.src = 'common/images/game/over_box.png';
        // 循环遍历箱子坐标点,使用drawImage绘制图片,判断箱子与圆形阴影是否重合选择绘制的图片
        this.boxPos.forEach(item => {
            this.drawCanvas.drawImage(self.boxOver(item) ? overImg : img , item[0] * 50, item[1] * 50, 50, 50);
        })
    },
    initOverPos() {
        let img = new Image();
        img.src = 'common/images/game/over.png';
        // 循环遍历圆形阴影坐标点,使用drawImage绘制图片
        this.overPos.forEach(item => {
            this.drawCanvas.drawImage(img, item[0] * 50, item[1] * 50, 50, 50);
        })
    },
    initPersonPos() {
        let img = new Image();
        img.src = 'common/images/game/player.png';
        // 绘制玩偶坐标位置
        this.drawCanvas.drawImage(img, this.personPos[0] * 50, this.personPos[1] * 50, 50, 50);
    },
    handleUp() {
        this.hanndleMove(1, -1) // (Y坐标,步长)
    },
    handleLeft() {
        this.hanndleMove(0, -1) // (X坐标,步长)
    },
    handleRight() {
        this.hanndleMove(0, 1) // (X坐标,步长)
    },
    handleDown() {
        this.hanndleMove(1, 1) // (Y坐标,步长)
    },
    hanndleMove(posIndex, step) {
        if (this.winGame) return // 通关后不能再移动
        let oldPersonPos = JSON.parse(JSON.stringify(this.personPos))
        this.personPos[posIndex] = oldPersonPos[posIndex] + step
        // 检测到是否撞墙
        if(this.collisionDetectionWall(this.personPos)) {
            this.personPos = oldPersonPos
            return
        }
        let boxIndex = this.collisionDetectionBox(this.personPos)
        // 检测到是否在推箱子
        if(boxIndex > -1) {
            let oldboxPos = JSON.parse(JSON.stringify(this.boxPos[boxIndex]));
            this.boxPos[boxIndex][posIndex] = oldboxPos[posIndex] + step;
            // 检测到推箱子是否撞墙或撞另一个箱子
            if (this.collisionDetectionWall(this.boxPos[boxIndex]) || this.collisionDetectionBoxToBox(boxIndex)) {
                this.personPos = oldPersonPos;
                this.boxPos[boxIndex] = oldboxPos;
                return;
            }
            this.isWin()
        }
        // 移动轨迹添加到历史记录,用于点击上一步操作
        this.addHistroy()
        // 清除画布内容
        this.drawCanvas.clearRect(0, 0, 500, 500);
        this.initCanvas()
    },
    // 碰墙检测
    collisionDetectionWall(pos) {
        for (let i = 0, len = this.walls.length; i < len; i++) {
            if (this.walls[i][0] == pos[0] && this.walls[i][1] == pos[1]) {
                return true;
            }
        }
        return false;
    },
    // 推箱子检测
    collisionDetectionBox(pos) {
        for (let i = 0, len = this.boxPos.length; i < len; i++) {
            if (this.boxPos[i][0] == pos[0] && this.boxPos[i][1] == pos[1]) {
                return i;
            }
        }
        return -1;
    },
    // 推箱子撞箱子检测
    collisionDetectionBoxToBox(index) {
        for (let i = 0, len = this.boxPos.length; i < len; i++) {
            if (i == index) continue
            if (this.boxPos[i][0] == this.boxPos[index][0] && this.boxPos[i][1] == this.boxPos[index][1]) {
                return true;
            }
        }
        return false;
    },
    // 箱子是否与终点重合
    boxOver(pos) {
        for (let i = 0, len = this.overPos.length; i < len; i++) {
            if (this.overPos[i][0] == pos[0] && this.overPos[i][1] == pos[1]) {
                return true;
            }
        }
        return false;
    },
    // 操作记录
    addHistroy() {
        this.boxHistory.push(JSON.stringify(this.boxPos))
        this.personHistory.push(JSON.stringify(this.personPos))
    },
    // 回退到上一步
    handleBack() {
        if (this.winGame) return // 通关后不能再移动
        if(this.boxHistory.length > 0 && this.personHistory.length > 0) {
            this.boxHistory.pop()
            this.personHistory.pop()
            this.boxPos = JSON.parse(this.boxHistory[this.boxHistory.length - 1])
            this.personPos = JSON.parse(this.personHistory[this.personHistory.length - 1])
            // 清除画布内容,重新绘制
            this.drawCanvas.clearRect(0, 0, 500, 500);
            this.initCanvas()
        }
    },
    // 重置
    handleReset() {
        // 去除通关记录
        this.winGame = false
        // 重置历史记录
        this.boxHistory = [].concat(this.boxHistory[0])
        this.personHistory = [].concat(this.personHistory[0])
        // 获取到初始位置并重新绘制
        this.boxPos = JSON.parse(this.boxHistory[0])
        this.personPos = JSON.parse(this.personHistory[0])
        this.drawCanvas.clearRect(0, 0, 500, 500);
        this.initCanvas()
    },
    isWin() {
        let count = 0;
        this.overPos.forEach((item) => {
            this.boxPos.forEach(box => {
                if (box[0] == item[0] && box[1] == item[1]) {
                    count++;
                }
            })
        });
        if (count == this.boxPos.length) {
            this.winGame = true
            this.showToast()
        }
    },
    showToast() {
        prompt.showToast({
            message: '恭喜你,通关了!',
            duration: 5000,
            bottom: 500,
        });
    },
};

总结

对于FA目前还在继续学习的路上,这个小游戏写的比较简单,还有很多不足之处,大家有什么想法和意见可以提出来,共同进步,谢谢。

注:图片素材见附件

更多原创内容请关注:中软国际 HarmonyOS 技术团队

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
图片资源.zip 15.59K 27次下载
6
收藏 3
回复
举报
3条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

看文档确实不如做一个小游戏能更好的学到知识。

1
回复
2022-8-5 16:24:53
中软国际鸿蒙生态
中软国际鸿蒙生态 回复了 红叶亦知秋
看文档确实不如做一个小游戏能更好的学到知识。

自己动手写东西印象会更深刻吧,学到东西印象就更深一点

回复
2022-8-5 16:49:39
物联风景
物联风景

都懂,就想知道是不是直接上了最难的一关

回复
2022-8-6 09:13:57
回复
    相关推荐