鸿蒙开发案例:记忆翻牌

 

【游戏简介】

记忆翻牌游戏是一种经典的益智游戏,玩家需要翻开隐藏的卡片,找出所有成对的图案。每翻开一对卡片,如果图案相同,则这对卡片会永久显示出来,否则会在一段时间后自动翻回背面。游戏的目标是在尽可能短的时间内找到所有匹配的对子。

【支持API 12】

经过测试,确认本应用支持API 12及以上版本。

【实现细节】

首先,我们定义了一个GameCell类来表示游戏中的每一个单元格。每个单元格有四个状态属性:isVisible表示是否可见,rotationAngle表示旋转角度,isFrontVisible表示是否正面朝上,isMatched表示是否已匹配成功。此外,我们还需要两个布尔类型的属性来记录动画是否正在运行,以便控制动画的播放。

单元格类提供了revealFace和hideFace方法,分别用来展示和隐藏单元格的正面。这两个方法都使用了动画效果,以增强用户体验。

接下来,我们创建了MemoryGame组件来管理整个游戏的逻辑。在游戏中,我们需要定义几个状态变量,如gameCells数组来存储所有的单元格实例,cellSize和cellSpacing来控制单元格的大小与间距,transitionDuration来设定过渡动画的持续时间,以及firstSelectedIndex和secondSelectedIndex来记录玩家点击的两个单元格的索引。

游戏开始时,我们通过aboutToAppear方法初始化游戏,创建必要的单元格实例并调用shuffleCards方法打乱它们的顺序。shuffleCards方法不仅打乱了单元格的顺序,还重置了游戏的状态,如清除之前的选择记录,更新游戏开始时间等。

当玩家点击某个单元格时,我们通过onClick事件处理器来处理用户的点击事件。如果点击的是第一个单元格,则记录下来;如果是第二个单元格,则通过checkForMatch方法来检查这两个单元格是否匹配。如果匹配,则标记它们为已匹配;如果不匹配,则在短暂延迟后将它们翻回背面。

游戏界面由一系列排列整齐的单元格组成,每个单元格都具有点击事件,允许玩家进行操作。当所有单元格都被正确匹配后,游戏结束,此时会显示一个对话框告知玩家用时,并提供重新开始游戏的按钮。

【完整代码】

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
import { promptAction } from '@kit.ArkUI' // 导入用于显示对话框的模块
 
// 使用装饰器来观察数据变化
@ObservedV2
class GameCell { // 定义单元格类
  @Trace value: string; // 单元格的值,即卡片上的图案
  @Trace isVisible: boolean = false; // 控制卡片是否可见
  isFrontVisible: boolean = false; // 控制卡片是否正面朝上
  isMatched: boolean = false; // 标记卡片是否已被匹配
  isAnimationRunning: boolean = false; // 动画是否正在运行
  @Trace rotationAngle: number = 0; // 卡片的旋转角度
 
  constructor(value: string) { // 构造函数,初始化单元格
    this.value = value; // 设置单元格的值
  }
 
  // 展示卡片正面的方法
  revealFace(animationTime: number, callback?: () => void) {
    if (this.isAnimationRunning) { // 如果已经有动画在运行,则返回
      return;
    }
    this.isAnimationRunning = true; // 设置动画状态为运行中
    animateToImmediately({ // 开始动画
      duration: animationTime, // 动画持续时间
      iterations: 1, // 动画迭代次数
      curve: Curve.Linear, // 动画曲线类型
      onFinish: () => { // 动画完成后的回调
        animateToImmediately({ // 再次开始动画
          duration: animationTime, // 动画持续时间
          iterations: 1, // 动画迭代次数
          curve: Curve.Linear, // 动画曲线类型
          onFinish: () => { // 动画完成后的回调
            this.isFrontVisible = true; // 设置卡片为正面朝上
            this.isAnimationRunning = false; // 设置动画状态为停止
            if (callback) { // 如果有回调函数,则执行
              callback();
            }
          }
        }, () => { // 动画开始时的回调
          this.isVisible = true; // 设置卡片为可见
          this.rotationAngle = 0; // 设置旋转角度为0
        });
      }
    }, () => { // 动画开始时的回调
      this.isVisible = false; // 设置卡片为不可见
      this.rotationAngle = 90; // 设置旋转角度为90度
    });
  }
 
  // 重置卡片状态的方法
  reset() {
    this.isVisible = false; // 设置卡片为不可见
    this.rotationAngle = 180; // 设置旋转角度为180度
    this.isFrontVisible = false; // 设置卡片为背面朝上
    this.isAnimationRunning = false; // 设置动画状态为停止
    this.isMatched = false; // 设置卡片为未匹配
  }
 
  // 隐藏卡片正面的方法
  hideFace(animationTime: number, callback?: () => void) {
    if (this.isAnimationRunning) { // 如果已经有动画在运行,则返回
      return;
    }
    this.isAnimationRunning = true; // 设置动画状态为运行中
    animateToImmediately({ // 开始动画
      duration: animationTime, // 动画持续时间
      iterations: 1, // 动画迭代次数
      curve: Curve.Linear, // 动画曲线类型
      onFinish: () => { // 动画完成后的回调
        animateToImmediately({ // 再次开始动画
          duration: animationTime, // 动画持续时间
          iterations: 1, // 动画迭代次数
          curve: Curve.Linear, // 动画曲线类型
          onFinish: () => { // 动画完成后的回调
            this.isFrontVisible = false; // 设置卡片为背面朝上
            this.isAnimationRunning = false; // 设置动画状态为停止
            if (callback) { // 如果有回调函数,则执行
              callback();
            }
          }
        }, () => { // 动画开始时的回调
          this.isVisible = false; // 设置卡片为不可见
          this.rotationAngle = 180; // 设置旋转角度为180度
        });
      }
    }, () => { // 动画开始时的回调
      this.isVisible = true; // 设置卡片为可见
      this.rotationAngle = 90; // 设置旋转角度为90度
    });
  }
}
 
// 定义组件入口
@Entry
@Component
struct MemoryGame { // 定义游戏组件
  @State gameCells: GameCell[] = []; // 存储游戏中的所有单元格
  @State cellSize: number = 150; // 单元格的大小
  @State cellSpacing: number = 5; // 单元格之间的间距
  @State transitionDuration: number = 150; // 过渡动画的持续时间
  @State firstSelectedIndex: number | null = null; // 记录第一次选择的卡片索引
  @State secondSelectedIndex: number | null = null; // 记录第二次选择的卡片索引
  @State isGameOver: boolean = false; // 游戏是否结束
  @State startTime: number = 0; // 游戏开始时间
 
  aboutToAppear(): void { // 组件即将显示时触发
    let cardValues: string[] = ["A", "B", "C", "D", "E", "F", "G", "H"]; // 定义卡片的值
    for (let value of cardValues) { // 遍历卡片值
      this.gameCells.push(new GameCell(value)); // 添加到游戏单元格数组中
      this.gameCells.push(new GameCell(value)); // 每个值添加两次以形成对
    }
    this.shuffleCards(); // 洗牌
  }
 
  // 洗牌方法
  shuffleCards() {
    this.firstSelectedIndex = null; // 清除第一次选择索引
    this.secondSelectedIndex = null; // 清除第二次选择索引
    this.isGameOver = false; // 游戏未结束
    this.startTime = Date.now(); // 设置游戏开始时间为当前时间
 
    for (let i = 0; i < 16; i++) { // 重置所有单元格状态
      this.gameCells[i].reset();
    }
 
    for (let i = this.gameCells.length - 1; i > 0; i--) { // 洗牌算法
      const randomIndex = Math.floor(Math.random() * (i + 1)); // 随机索引
      let tempValue = this.gameCells[i].value; // 临时保存值
      this.gameCells[i].value = this.gameCells[randomIndex].value; // 交换值
      this.gameCells[randomIndex].value = tempValue; // 交换值
    }
  }
 
  // 检查卡片是否匹配的方法
  checkForMatch() {
    if (this.firstSelectedIndex !== null && this.secondSelectedIndex !== null) { // 确保两个索引都不为空
      const firstCell = this.gameCells[this.firstSelectedIndex]; // 获取第一个选中的单元格
      const secondCell = this.gameCells[this.secondSelectedIndex]; // 获取第二个选中的单元格
 
      if (firstCell.value === secondCell.value) { // 如果两个单元格的值相同
        firstCell.isMatched = true; // 标记为已匹配
        secondCell.isMatched = true; // 标记为已匹配
        this.firstSelectedIndex = null; // 清除第一次选择索引
        this.secondSelectedIndex = null; // 清除第二次选择索引
 
        if (this.gameCells.every(cell => cell.isMatched)) { // 如果所有单元格都已匹配
          this.isGameOver = true; // 游戏结束
          console.info("游戏结束"); // 打印信息
          promptAction.showDialog({ // 显示对话框
            title: '游戏胜利!', // 对话框标题
            message: '恭喜你,用时:' + ((Date.now() - this.startTime) / 1000).toFixed(3) + '秒', // 对话框消息
            buttons: [{ text: '重新开始', color: '#ffa500' }] // 对话框按钮
          }).then(() => { // 对话框关闭后执行
            this.shuffleCards(); // 重新开始游戏
          });
        }
      } else { // 如果两个单元格的值不同
        setTimeout(() => { // 延迟一段时间后
          if (this.firstSelectedIndex !== null) { // 如果第一个索引不为空
            this.gameCells[this.firstSelectedIndex].hideFace(this.transitionDuration, () => { // 隐藏卡片
              this.firstSelectedIndex = null; // 清除第一次选择索引
            });
          }
          if (this.secondSelectedIndex !== null) { // 如果第二个索引不为空
            this.gameCells[this.secondSelectedIndex].hideFace(this.transitionDuration, () => { // 隐藏卡片
              this.secondSelectedIndex = null; // 清除第二次选择索引
            });
          }
        }, 400); // 延迟时间
      }
    }
  }
 
  // 构建游戏界面的方法
  build() {
    Column({ space: 20 }) { // 创建一个垂直布局
      Flex({ wrap: FlexWrap.Wrap }) { // 创建一个可换行的弹性布局
        ForEach(this.gameCells, (gameCell: GameCell, index: number) => { // 遍历游戏单元格
          Text(`${gameCell.isVisible ? gameCell.value : ''}`) // 显示单元格的值或空字符串
            .width(`${this.cellSize}lpx`) // 设置宽度
            .height(`${this.cellSize}lpx`) // 设置高度
            .margin(`${this.cellSpacing}lpx`) // 设置边距
            .fontSize(`${this.cellSize / 2}lpx`) // 设置字体大小
            .textAlign(TextAlign.Center) // 文本居中
            .backgroundColor(gameCell.isVisible ? Color.Orange : Color.Gray) // 设置背景颜色
            .fontColor(Color.White) // 设置字体颜色
            .borderRadius(5) // 设置圆角
            .rotate({ // 设置旋转
              x: 0,
              y: 1,
              z: 0,
              angle: gameCell.rotationAngle, // 旋转角度
              centerX: `${this.cellSize / 2}lpx`, // 中心点X坐标
              centerY: `${this.cellSize / 2}lpx`, // 中心点Y坐标
            })
            .onClick(() => { // 单击事件处理
              if (this.isGameOver) { // 如果游戏已结束
                console.info("游戏已结束"); // 打印信息
                return;
              }
              if (gameCell.isMatched) { // 如果单元格已匹配
                console.info("当前已标记"); // 打印信息
                return;
              }
              if (this.firstSelectedIndex == null) { // 如果没有第一次选择
                this.firstSelectedIndex = index; // 设置第一次选择索引
                if (!gameCell.isFrontVisible) { // 如果不是正面朝上
                  gameCell.revealFace(this.transitionDuration); // 展示正面
                }
              } else if (this.firstSelectedIndex == index) { // 如果与第一次选择相同
                console.info("和上一次点击的是一样的,不予处理"); // 打印信息
              } else if (this.secondSelectedIndex == null) { // 如果没有第二次选择
                this.secondSelectedIndex = index; // 设置第二次选择索引
                if (!gameCell.isFrontVisible) { // 如果不是正面朝上
                  gameCell.revealFace(this.transitionDuration, () => { // 展示正面
                    this.checkForMatch(); // 检查是否匹配
                  });
                }
              }
            });
        });
      }.width(`${(this.cellSize + this.cellSpacing * 2) * 4}lpx`); // 设置宽度
      Button('重新开始') // 创建“重新开始”按钮
        .onClick(() => { // 按钮点击事件
          this.shuffleCards(); // 重新开始游戏
        });
    }.height('100%').width('100%'); // 设置高度和宽度
  }
}

  

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