HarmonyOS - 基于ArkUI(JS)实现推箱子游戏 原创 精华
作者 : 郝志鹏
前言
从零开始学习HarmonyOS开发相关知识,对于从没接触过的FA感到无从下手。单纯看文档,练习文档中的组件提升不大,突发奇想决定结合ArkUI(JS)和canvas实现一个简单的推箱子小游戏,来深入学习FA的实际应用。
实现效果
游戏说明
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开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。
看文档确实不如做一个小游戏能更好的学到知识。
自己动手写东西印象会更深刻吧,学到东西印象就更深一点
都懂,就想知道是不是直接上了最难的一关