推箱子游戏(Sokoban)的实现。游戏由多个单元格组成,每个单元格可以是透明的、墙或可移动的区域。游戏使用Cell类定义单元格的状态,如类型(透明、墙、可移动区域)、圆角大小及坐标偏移。而MyPosition类则用于表示位置信息,并提供设置位置的方法。
游戏主体结构Sokoban定义了游戏的基本元素,包括网格单元格的状态、胜利位置、箱子的位置以及玩家的位置等,并提供了初始化游戏状态的方法。游戏中还包含有动画效果,当玩家尝试移动时,会检查目标位置是否允许移动,并根据情况决定是否需要移动箱子。此外,游戏支持触摸输入,并在完成一次移动后检查是否所有箱子都在目标位置上,如果是,则游戏胜利,并显示一个对话框展示游戏用时。
【算法分析】
1. 移动玩家和箱子算法分析:
算法思路:根据玩家的滑动方向,计算新的位置坐标,然后检查新位置的合法性,包括是否超出边界、是否是墙等情况。如果新位置是箱子,则需要进一步判断箱子后面的位置是否为空,以确定是否可以推动箱子。
实现逻辑:通过定义方向对象和计算新位置坐标的方式,简化了移动操作的逻辑。在移动过程中,需要考虑动画效果的控制,以提升用户体验。
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;
// 检查新位置是否合法
// 箱子移动逻辑...
// 动画效果控制...
}
2. 胜利条件判断算法分析:
算法思路:遍历所有箱子的位置,检查每个箱子是否在一个胜利位置上,如果所有箱子都在胜利位置上,则判定游戏胜利。
实现逻辑:通过嵌套循环和数组方法,实现了对胜利条件的判断。这种算法适合用于检查游戏胜利条件是否满足的场景。
isVictoryConditionMet(): boolean {
return this.cratePositions.every(crate => {
return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);
});
}
3. 动画控制算法分析:
算法思路:利用动画函数实现移动过程中的动画效果,包括移动过程的持续时间和结束后的处理逻辑。
实现逻辑:通过嵌套调用动画函数,实现了移动过程中的动画效果控制。这种方式可以使移动过程更加流畅和生动。
animateToImmediately({
duration: 150,
onFinish: () => {
animateToImmediately({
duration: 0,
onFinish: () => {
// 动画结束后的处理...
}
}, () => {
// 动画过程中的处理...
});
}
}, () => {
// 动画效果控制...
});
4. 触摸操作和手势识别算法分析:
算法思路:监听触摸事件和手势事件,识别玩家的滑动方向,然后调用相应的移动函数处理玩家和箱子的移动。
实现逻辑:通过手势识别和事件监听,实现了玩家在屏幕上滑动操作的识别和响应。这种方式可以使玩家通过触摸操作来控制游戏的进行。
gesture(
SwipeGesture({ direction: SwipeDirection.All })
.onAction((_event: GestureEvent) => {
// 手势识别和处理逻辑...
})
)
【完整代码】
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;
this.grid[0][5].topRight = 25;
this.grid[1][0].topLeft = 25;
this.grid[4][0].bottomLeft = 25;
this.grid[5][1].bottomLeft = 25;
this.grid[5][5].bottomRight = 25;
this.grid[1][1].bottomRight = 10;
this.grid[4][1].topRight = 10;
this.grid[2][4].topLeft = 10;
this.grid[2][4].bottomLeft = 10;
this.initializeGame(); // 初始化游戏
}
initializeGame() { // 初始化游戏状态
this.startTime = Date.now(); // 设置游戏开始时间为当前时间
// 设置胜利位置和箱子位置...
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 })
});
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;
const targetCell = this.grid[newX][newY];
// 检查新位置是否超出边界
if (!targetCell) {
return;
}
// 如果新位置是墙,则不能移动
if (targetCell.type === 1) {
return;
}
let crateIndex = -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;
}
}
if (this.isAnimationRunning) {
return
}
this.isAnimationRunning = true
animateToImmediately({
duration: 150,
onFinish: () => {
animateToImmediately({
duration: 0,
onFinish: () => {
this.isAnimationRunning = 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;
this.grid[this.playerPosition.x][this.playerPosition.y].y = dx * this.cellWidth;
if (crateIndex !== -1) {
this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].x = dy * this.cellWidth;
this.grid[this.cratePositions[crateIndex].x][this.cratePositions[crateIndex].y].y = dx * this.cellWidth;
}
console.info(`dx:${dx},dy:${dy}`)
})
}
build() {
Column({ space: 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 })
.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")//#995d12 #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")//#995d12 #cb8321
.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` })
})
}
})
}
}
Button('重新开始').clickEffect({ level: ClickEffectLevel.MIDDLE })
.onClick(() => {
this.initializeGame();
});
}
.width('100%')
.height('100%')
.backgroundColor("#fdb300")
.padding({ top: 20 })
.onTouch((e) => {
if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置
this.screenStartX = e.touches[0].x;
this.screenStartY = e.touches[0].y;
} else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置
this.lastScreenX = e.changedTouches[0].x;
this.lastScreenY = e.changedTouches[0].y;
}
})
.gesture(
SwipeGesture({ direction: SwipeDirection.All })// 支持方向中 all可以是上下左右
.onAction((_event: GestureEvent) => {
const swipeX = this.lastScreenX - this.screenStartX;
const swipeY = this.lastScreenY - this.screenStartY;
// 清除开始位置记录,准备下一次滑动判断
this.screenStartX = 0;
this.screenStartY = 0;
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');
}
}
})
)
}
}