鸿蒙开发案例:推箱子

 

推箱子游戏(Sokoban)的实现。游戏由多个单元格组成,每个单元格可以是透明的、墙或可移动的区域。游戏使用Cell类定义单元格的状态,如类型(透明、墙、可移动区域)、圆角大小及坐标偏移。而MyPosition类则用于表示位置信息,并提供设置位置的方法。

游戏主体结构Sokoban定义了游戏的基本元素,包括网格单元格的状态、胜利位置、箱子的位置以及玩家的位置等,并提供了初始化游戏状态的方法。游戏中还包含有动画效果,当玩家尝试移动时,会检查目标位置是否允许移动,并根据情况决定是否需要移动箱子。此外,游戏支持触摸输入,并在完成一次移动后检查是否所有箱子都在目标位置上,如果是,则游戏胜利,并显示一个对话框展示游戏用时。

【算法分析】

1. 移动玩家和箱子算法分析:

算法思路:根据玩家的滑动方向,计算新的位置坐标,然后检查新位置的合法性,包括是否超出边界、是否是墙等情况。如果新位置是箱子,则需要进一步判断箱子后面的位置是否为空,以确定是否可以推动箱子。

实现逻辑:通过定义方向对象和计算新位置坐标的方式,简化了移动操作的逻辑。在移动过程中,需要考虑动画效果的控制,以提升用户体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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. 胜利条件判断算法分析:

算法思路:遍历所有箱子的位置,检查每个箱子是否在一个胜利位置上,如果所有箱子都在胜利位置上,则判定游戏胜利。

实现逻辑:通过嵌套循环和数组方法,实现了对胜利条件的判断。这种算法适合用于检查游戏胜利条件是否满足的场景。

1
2
3
4
5
isVictoryConditionMet(): boolean {
    return this.cratePositions.every(crate => {
        return this.victoryPositions.some(victory => crate.x === victory.x && crate.y === victory.y);
    });
}

  

3. 动画控制算法分析:

算法思路:利用动画函数实现移动过程中的动画效果,包括移动过程的持续时间和结束后的处理逻辑。

实现逻辑:通过嵌套调用动画函数,实现了移动过程中的动画效果控制。这种方式可以使移动过程更加流畅和生动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
animateToImmediately({
    duration: 150,
    onFinish: () => {
        animateToImmediately({
            duration: 0,
            onFinish: () => {
                // 动画结束后的处理...
            }
        }, () => {
            // 动画过程中的处理...
        });
    }
}, () => {
    // 动画效果控制...
});

  

4. 触摸操作和手势识别算法分析:

算法思路:监听触摸事件和手势事件,识别玩家的滑动方向,然后调用相应的移动函数处理玩家和箱子的移动。

实现逻辑:通过手势识别和事件监听,实现了玩家在屏幕上滑动操作的识别和响应。这种方式可以使玩家通过触摸操作来控制游戏的进行。

1
2
3
4
5
6
gesture(
    SwipeGesture({ direction: SwipeDirection.All })
        .onAction((_event: GestureEvent) => {
            // 手势识别和处理逻辑...
        })
)

  【完整代码】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
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');
            }
          }
        })
    )
  }
}

  

posted @   zhongcx  阅读(109)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示