回复
鸿蒙NEXT开发案例:推箱子
zhongcx
发布于 2024-12-1 09:29
浏览
0收藏
【引言】
推箱子是一种经典的益智游戏,通过操作玩家角色将箱子推到指定位置,达到胜利的游戏目标。本文将介绍如何使用鸿蒙NEXT开发推箱子游戏,并展示相关代码实现。
【环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI
【算法分析】
- 移动玩家和箱子算法分析:
算法思路:根据玩家的滑动方向,计算新的位置坐标,然后检查新位置的合法性,包括是否超出边界、是否是墙等情况。如果新位置是箱子,则需要进一步判断箱子后面的位置是否为空,以确定是否可以推动箱子。
实现逻辑:通过定义方向对象和计算新位置坐标的方式,简化了移动操作的逻辑。在移动过程中,需要考虑动画效果的控制,以提升用户体验。
movePlayer(direction: string) {
const directions: object = Object({
'right': Object({ dx: 0, dy: 1}),
'left': Object({ dx:0 , dy:-1 }),
'down': Object({ dx: 1, dy: 0 }),
'up': Object({ dx: -1, dy: 0 })
});
const dx: number = directions[direction]['dx']; //{ dx, dy }
const dy: number = directions[direction]['dy']; //{ dx, dy }
const newX: number = this.playerPosition.x + dx;
const newY: number = this.playerPosition.y + dy;
// 检查新位置是否合法
// 箱子移动逻辑...
// 动画效果控制...
}
- 胜利条件判断算法分析:
算法思路:遍历所有箱子的位置,检查每个箱子是否在一个胜利位置上,如果所有箱子都在胜利位置上,则判定游戏胜利。
实现逻辑:通过嵌套循环和数组方法,实现了对胜利条件的判断。这种算法适合用于检查游戏胜利条件是否满足的场景。
isVictoryConditionMet(): boolean {
return this.cratePositions.every(crate => {
return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);
});
}
- 动画控制算法分析:
算法思路:利用动画函数实现移动过程中的动画效果,包括移动过程的持续时间和结束后的处理逻辑。
实现逻辑:通过嵌套调用动画函数,实现了移动过程中的动画效果控制。这种方式可以使移动过程更加流畅和生动。
animateToImmediately({
duration: 150,
onFinish: () => {
animateToImmediately({
duration: 0,
onFinish: () => {
// 动画结束后的处理...
}
}, () => {
// 动画过程中的处理...
});
}
}, () => {
// 动画效果控制...
});
4. 触摸操作和手势识别算法分析:
算法思路:监听触摸事件和手势事件,识别玩家的滑动方向,然后调用相应的移动函数处理玩家和箱子的移动。
实现逻辑:通过手势识别和事件监听,实现了玩家在屏幕上滑动操作的识别和响应。这种方式可以使玩家通过触摸操作来控制游戏的进行。
【完整代码】
import { promptAction } from '@kit.ArkUI' // 导入ArkUI工具包中的提示操作模块
@ObservedV2
// 观察者模式装饰器
class Cell { // 定义游戏中的单元格类
@Trace // 跟踪装饰器,标记属性以被跟踪
type: number = 0; // 单元格类型,0:透明,1:墙,2:可移动区域
@Trace topLeft: number = 0; // 左上角圆角大小
@Trace topRight: number = 0; // 右上角圆角大小
@Trace bottomLeft: number = 0; // 左下角圆角大小
@Trace bottomRight: number = 0; // 右下角圆角大小
@Trace x: number = 0; // 单元格的X坐标偏移量
@Trace y: number = 0; // 单元格的Y坐标偏移量
constructor(cellType: number) { // 构造函数
this.type = cellType; // 初始化单元格类型
}
}
@ObservedV2
// 观察者模式装饰器
class MyPosition { // 定义位置类
@Trace // 跟踪装饰器,标记属性以被跟踪
x: number = 0; // X坐标
@Trace y: number = 0; // Y坐标
setPosition(x: number, y: number) { // 设置位置的方法
this.x = x; // 更新X坐标
this.y = y; // 更新Y坐标
}
}
@Entry
// 入口装饰器
@Component
// 组件装饰器
struct Sokoban { // 定义游戏主结构
cellWidth: number = 100; // 单元格宽度
@State grid: Cell[][] = [// 游戏网格状态
[new Cell(0), new Cell(1), new Cell(1), new Cell(1), new Cell(1), new Cell(1)], // 第一行单元格
[new Cell(1), new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1)], // 第二行单元格
[new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1), new Cell(1)], // 第三行单元格
[new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(2), new Cell(1)], // 第四行单元格
[new Cell(1), new Cell(1), new Cell(2), new Cell(2), new Cell(2), new Cell(1)], // 第五行单元格
[new Cell(0), new Cell(1), new Cell(1), new Cell(1), new Cell(1), new Cell(1)],// 第六行单元格
];
@State victoryPositions: MyPosition[] = [new MyPosition(), new MyPosition()]; // 胜利位置数组
@State cratePositions: MyPosition[] = [new MyPosition(), new MyPosition()]; // 箱子位置数组
playerPosition: MyPosition = new MyPosition(); // 玩家位置
@State screenStartX: number = 0; // 触摸开始时的屏幕X坐标
@State screenStartY: number = 0; // 触摸开始时的屏幕Y坐标
@State lastScreenX: number = 0; // 触摸结束时的屏幕X坐标
@State lastScreenY: number = 0; // 触摸结束时的屏幕Y坐标
@State startTime: number = 0; // 游戏开始时间
isAnimationRunning: boolean = false // 动画是否正在运行
aboutToAppear(): void { // 游戏加载前的准备工作
// 初始化某些单元格的圆角大小...
this.grid[0][1].topLeft = 25; // 设置(0,1)单元格的左上角圆角大小
this.grid[0][5].topRight = 25; // 设置(0,5)单元格的右上角圆角大小
this.grid[1][0].topLeft = 25; // 设置(1,0)单元格的左上角圆角大小
this.grid[4][0].bottomLeft = 25; // 设置(4,0)单元格的左下角圆角大小
this.grid[5][1].bottomLeft = 25; // 设置(5,1)单元格的左下角圆角大小
this.grid[5][5].bottomRight = 25; // 设置(5,5)单元格的右下角圆角大小
this.grid[1][1].bottomRight = 10; // 设置(1,1)单元格的右下角圆角大小
this.grid[4][1].topRight = 10; // 设置(4,1)单元格的右上角圆角大小
this.grid[2][4].topLeft = 10; // 设置(2,4)单元格的左上角圆角大小
this.grid[2][4].bottomLeft = 10; // 设置(2,4)单元格的左下角圆角大小
this.initializeGame(); // 初始化游戏
}
initializeGame() { // 初始化游戏状态
this.startTime = Date.now(); // 设置游戏开始时间为当前时间
// 设置胜利位置和箱子位置...
this.victoryPositions[0].setPosition(1, 3); // 设置第一个胜利位置
this.victoryPositions[1].setPosition(1, 4); // 设置第二个胜利位置
this.cratePositions[0].setPosition(2, 2); // 设置第一个箱子位置
this.cratePositions[1].setPosition(2, 3); // 设置第二个箱子位置
this.playerPosition.setPosition(1, 2); // 设置玩家初始位置
}
isVictoryPositionVisible(x: number, y: number): boolean { // 判断位置是否为胜利位置
return this.victoryPositions.some(position => position.x === x && position.y === y); // 返回是否有胜利位置与给定位置匹配
}
isCratePositionVisible(x: number, y: number): boolean { // 判断位置是否为箱子位置
return this.cratePositions.some(position => position.x === x && position.y === y); // 返回是否有箱子位置与给定位置匹配
}
isPlayerPositionVisible(x: number, y: number): boolean { // 判断位置是否为玩家位置
return this.playerPosition.x === x && this.playerPosition.y === y; // 返回玩家位置是否与给定位置相同
}
movePlayer(direction: string) { // 移动玩家的方法
// 定义移动方向对象
const directions: object = Object({
'right': Object({ dx: 0, dy: 1 }), // 向右移动的偏移量
'left': Object({ dx: 0, dy: -1 }), // 向左移动的偏移量
'down': Object({ dx: 1, dy: 0 }), // 向下移动的偏移量
'up': Object({ dx: -1, dy: 0 }) // 向上移动的偏移量
});
// 获取当前移动方向的X和Y偏移量
const dx: number = directions[direction]['dx']; //{ dx, dy }
const dy: number = directions[direction]['dy']; //{ dx, dy }
// 计算玩家新位置的X和Y坐标
const newX: number = this.playerPosition.x + dx;
const newY: number = this.playerPosition.y + dy;
// 获取玩家新位置对应的目标单元格
const targetCell = this.grid[newX][newY];
// 检查新位置是否超出边界
if (!targetCell) {
return; // 如果超出边界,直接返回
}
// 如果新位置是墙,则不能移动
if (targetCell.type === 1) {
return; // 如果是墙,直接返回
}
let crateIndex = -1; // 初始化箱子索引为-1
// 检查新位置是否有箱子
if (this.isCratePositionVisible(newX, newY)) {
const crateBehindCell = this.grid[newX + dx][newY + dy]; // 获取箱子后面的单元格
// 如果箱子后面没有单元格或不是可移动区域,返回
if (!crateBehindCell || crateBehindCell.type !== 2) {
return; // 如果后面不是可移动区域,返回
}
// 查找当前箱子的索引
crateIndex = this.cratePositions.findIndex(crate => crate.x === newX && crate.y === newY);
// 如果找不到
if (crateIndex === -1 || this.isCratePositionVisible(newX + dx, newY + dy)) {
return; // 如果箱子索引为-1或者新位置后面有箱子,返回
}
}
if (this.isAnimationRunning) {
return; // 如果动画正在运行,返回
}
this.isAnimationRunning = true; // 设置动画运行标志为true
animateToImmediately({
// 立即执行动画
duration: 150, // 动画持续时间
onFinish: () => { // 动画结束时的回调
animateToImmediately({
// 立即执行动画
duration: 0, // 动画持续时间为0,即立即完成
onFinish: () => { // 动画结束时的回调
this.isAnimationRunning = false; // 设置动画运行标志为false
}
}, () => { // 动画执行时的回调
if (crateIndex !== -1) { // 如果有箱子移动
// 清空箱子当前位置
this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x = 0;
this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y = 0;
// 更新箱子位置
this.cratePositions[crateIndex].x += dx;
this.cratePositions[crateIndex].y += dy;
}
// 清空玩家当前位置
this.grid[this.playerPosition.x][this.playerPosition.y].x = 0;
this.grid[this.playerPosition.x][this.playerPosition.y].y = 0;
// 更新玩家位置
this.playerPosition.setPosition(newX, newY);
// 检查是否获胜
const isAllCrateOnTarget = this.cratePositions.every(crate => {
return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);
});
if (isAllCrateOnTarget) { // 如果所有箱子都在胜利位置
console.log("恭喜你,你赢了!"); // 输出恭喜信息
// 可以在这里添加胜利处理逻辑
promptAction.showDialog({
// 显示对话框
title: '游戏胜利!', // 对话框标题
message: '恭喜你,用时:' + ((Date.now() - this.startTime) / 1000).toFixed(3) + '秒', // 对话框消息
buttons: [{ text: '重新开始', color: '#ffa500' }] // 对话框按钮
}).then(() => { // 对话框关闭后执行
this.initializeGame(); // 重新开始游戏
});
}
});
}
}, () => { // 动画执行时的回调
this.grid[this.playerPosition.x][this.playerPosition.y].x = dy * this.cellWidth; // 设置玩家X坐标偏移量
this.grid[this.playerPosition.x][this.playerPosition.y].y = dx * this.cellWidth; // 设置玩家Y坐标偏移量
if (crateIndex !== -1) { // 如果有箱子移动
this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x =
dy * this.cellWidth; // 设置箱子X坐标偏移量
this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y =
dx * this.cellWidth; // 设置箱子Y坐标偏移量
}
console.info(`dx:${dx},dy:${dy}`); // 输出dx和dy的信息
});
}
build() { // 构建游戏界面
Column({ space: 20 }) { // 垂直布局,间距20
//游戏区
Stack() { // 堆叠布局
//非零区加瓷砖
Column() { // 垂直布局
ForEach(this.grid, (row: [], rowIndex: number) => { // 遍历网格行
Row() { // 水平布局
ForEach(row, (item: Cell, colIndex: number) => { // 遍历网格列
Stack() { // 堆叠布局
Text()// 文本组件
.width(`${this.cellWidth}lpx`)// 设置宽度
.height(`${this.cellWidth}lpx`)// 设置高度
.backgroundColor(item.type == 0 ? Color.Transparent :
((rowIndex + colIndex) % 2 == 0 ? "#cfb381" : "#e1ca9f"))// 根据单元格类型设置背景颜色
.borderRadius({
// 设置圆角
topLeft: item.topLeft > 10 ? item.topLeft : 0,
topRight: item.topRight > 10 ? item.topRight : 0,
bottomLeft: item.bottomLeft > 10 ? item.bottomLeft : 0,
bottomRight: item.bottomRight > 10 ? item.bottomRight : 0
});
//如果和是胜利坐标,显示叉号
Stack() { // 堆叠布局
Text()// 文本组件
.width(`${this.cellWidth / 2}lpx`)// 设置宽度
.height(`${this.cellWidth / 8}lpx`)// 设置高度
.backgroundColor(Color.White); // 设置背景颜色
Text()// 文本组件
.width(`${this.cellWidth / 8}lpx`)// 设置宽度
.height(`${this.cellWidth / 2}lpx`)// 设置高度
.backgroundColor(Color.White); // 设置背景颜色
}.rotate({ angle: 45 }) // 旋转45度
.visibility(this.isVictoryPositionVisible(rowIndex, colIndex) ? Visibility.Visible :
Visibility.None); // 根据是否为胜利位置设置可见性
}
})
}
})
}
Column() { // 垂直布局
ForEach(this.grid, (row: [], rowIndex: number) => { // 遍历网格行
Row() { // 水平布局
ForEach(row, (item: Cell, colIndex: number) => { // 遍历网格列
//是否显示箱子
Stack() { // 堆叠布局
Text()// 文本组件
.width(`${this.cellWidth}lpx`)// 设置宽度
.height(`${this.cellWidth}lpx`)// 设置高度
.backgroundColor(item.type == 1 ? "#412c0f" : Color.Transparent)// 根据单元格类型设置背景颜色
.borderRadius({
// 设置圆角
topLeft: item.topLeft,
topRight: item.topRight,
bottomLeft: item.bottomLeft,
bottomRight: item.bottomRight
});
Text('箱')// 文本组件,显示箱子
.fontColor(Color.White)// 设置字体颜色
.textAlign(TextAlign.Center)// 设置文本对齐方式
.fontSize(`${this.cellWidth / 2}lpx`)// 设置字体大小
.width(`${this.cellWidth - 5}lpx`)// 设置宽度
.height(`${this.cellWidth - 5}lpx`)// 设置高度
.backgroundColor("#cb8321")// 设置背景颜色
.borderRadius(10)// 设置圆角
.visibility(this.isCratePositionVisible(rowIndex, colIndex) ? Visibility.Visible :
Visibility.None); // 根据是否为箱子位置设置可见性
Text('我')// 文本组件,显示玩家
.fontColor(Color.White)// 设置字体颜色
.textAlign(TextAlign.Center)// 设置文本对齐方式
.fontSize(`${this.cellWidth / 2}lpx`)// 设置字体大小
.width(`${this.cellWidth - 5}lpx`)// 设置宽度
.height(`${this.cellWidth - 5}lpx`)// 设置高度
.backgroundColor("#007dfe")// 设置背景颜色
.borderRadius(10)// 设置圆角
.visibility(this.isPlayerPositionVisible(rowIndex, colIndex) ? Visibility.Visible :
Visibility.None); // 根据是否为玩家位置设置可见性
}
.width(`${this.cellWidth}lpx`) // 设置宽度
.height(`${this.cellWidth}lpx`) // 设置高度
.translate({ x: `${item.x}lpx`, y: `${item.y}lpx` }) // 根据单元格的X和Y坐标设置位置
})
}
})
}
}
// 创建重新开始按钮
Button('重新开始')// 按钮文本为“重新开始”
.clickEffect({ level: ClickEffectLevel.MIDDLE })// 设置点击效果
.onClick(() => { // 按钮点击事件
this.initializeGame(); // 重新初始化游戏
});
}
.width('100%') // 设置组件宽度为100%
.height('100%') // 设置组件高度为100%
.backgroundColor("#fdb300") // 设置背景颜色
.padding({ top: 20 }) // 设置顶部内边距为20
.onTouch((e) => { // 触摸事件处理
if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置
this.screenStartX = e.touches[0].x; // 记录触摸开始时的X坐标
this.screenStartY = e.touches[0].y; // 记录触摸开始时的Y坐标
} else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置
this.lastScreenX = e.changedTouches[0].x; // 记录触摸结束时的X坐标
this.lastScreenY = e.changedTouches[0].y; // 记录触摸结束时的Y坐标
}
})
.gesture( // 添加手势识别
SwipeGesture({ direction: SwipeDirection.All })// 支持所有方向的滑动手势
.onAction((_event: GestureEvent) => { // 手势动作处理
const swipeX = this.lastScreenX - this.screenStartX; // 计算X轴滑动距离
const swipeY = this.lastScreenY - this.screenStartY; // 计算Y轴滑动距离
// 清除开始位置记录,准备下一次滑动判断
this.screenStartX = 0; // 重置初始X坐标
this.screenStartY = 0; // 重置初始Y坐标
if (Math.abs(swipeX) > Math.abs(swipeY)) { // 判断滑动方向
if (swipeX > 0) { // 向右滑动
this.movePlayer('right'); // 移动玩家向右
} else { // 向左滑动
this.movePlayer('left'); // 移动玩家向左
}
} else { // 垂直滑动
if (swipeY > 0) { // 向下滑动
this.movePlayer('down'); // 移动玩家向下
} else { // 向上滑动
this.movePlayer('up'); // 移动玩家向上
}
}
})
)
}
}
分类
标签
赞
收藏
回复
相关推荐