行业资讯

# HarmonyOS ArkTS 记忆翻牌游戏深度解析 —— Fisher-Yates 洗牌与翻牌匹配机制

发布时间:2026/7/29 1:37:23
# HarmonyOS ArkTS 记忆翻牌游戏深度解析 —— Fisher-Yates 洗牌与翻牌匹配机制 一、应用概述记忆翻牌Memory Card Matching是一款经典的益智游戏玩家在网格中翻开卡片记忆图案位置配对两张相同的卡片即可得分。本项目基于 HarmonyOS ArkTS 框架实现了一个完整的记忆翻牌应用涵盖卡片洗牌Fisher-Yates 算法、翻牌动画、匹配判定、锁定机制、计时与步数统计等核心功能。本文将从架构设计、算法实现、UI/UX 和 HarmonyOS 特性等角度进行全面剖析。二、架构设计2.1 系统架构┌──────────────────────────────────────────┐ │ Index.ets │ │ (主页面持有游戏状态 State) │ ├──────────────────────────────────────────┤ │ MemoryGameView.ets │ │ (游戏视图网格布局、状态面板) │ ├──────────────────────────────────────────┤ │ CardComponent.ets │ │ (卡片组件正面/背面、翻转动画) │ ├──────────────────────────────────────────┤ │ GameModel.ets │ │ (游戏模型卡片数据、状态管理) │ ├──────────────────────────────────────────┤ │ GameController.ets │ │ (游戏控制器洗牌、翻转、匹配逻辑) │ └──────────────────────────────────────────┘2.2 数据流设计用户点击卡片 → CardComponent.onClick() → GameController.flipCard(index) → 检查锁定状态 → 翻牌 → 两张已翻开→ 执行匹配检查 → 匹配成功保持翻开加分 → 匹配失败短暂展示后翻回 → 检查是否全部匹配 → 游戏结束三、核心技术实现3.1 卡片数据结构// GameModel.ets export enum CardState { Hidden 0, // 背面朝上未翻开 Revealed 1, // 正面朝上已翻开 Matched 2 // 已配对成功 } export class Card { id: number; // 唯一标识 emoji: string; // 图案内容使用 Emoji state: CardState; // 当前状态 constructor(id: number, emoji: string) { this.id id; this.emoji emoji; this.state CardState.Hidden; } } export class GameModel { cards: Card[] []; firstFlippedIndex: number -1; // 第一张翻开的卡片索引 secondFlippedIndex: number -1; // 第二张翻开的卡片索引 isLocked: boolean false; // 锁定标志防止快速连点 matchedPairs: number 0; // 已匹配对数 totalPairs: number 0; // 总对数 attempts: number 0; // 尝试次数 isGameOver: boolean false; // 游戏是否结束 timer: number 0; // 计时秒 }3.2 Fisher-Yates 洗牌算法Fisher-Yates也称为 Knuth 洗牌算法是生成数组随机排列的最优算法。它的核心思想是从数组尾部开始每次随机选取一个未处理的元素与当前位置交换逐步向前推进。// GameController.ets export function fisherYatesShuffleT(array: T[]): T[] { const result: T[] [...array]; // 创建副本避免修改原数组 for (let i result.length - 1; i 0; i--) { // 生成 [0, i] 范围内的随机整数 const j Math.floor(Math.random() * (i 1)); // 交换 i 和 j 位置的元素 [result[i], result[j]] [result[j], result[i]]; } return result; }算法原理详解从后向前遍历指针 i 从 n-1 递减到 1随机选取在 [0, i] 范围内选一个随机索引 j交换元素将 result[i] 与 result[j] 交换不重复选择位置 i 在交换后不会再参与后续随机概率分析总排列数n! 种等可能性排列第 1 步in-1n 种选择概率 1/n第 2 步in-2n-1 种选择最终每个排列出现的概率 1/n × 1/(n-1) × … × 1/1 1/n!时间复杂度O(n)仅需一次遍历空间复杂度O(n)需要副本数组3.3 翻牌与匹配逻辑// GameController.ets export function flipCard(model: GameModel, index: number): boolean { // 1. 边界检查锁定中、已匹配、已翻开、游戏结束 if (model.isLocked) return false; if (model.isGameOver) return false; if (model.cards[index].state ! CardState.Hidden) return false; // 2. 翻开卡片 model.cards[index].state CardState.Revealed; // 3. 第一张翻开 if (model.firstFlippedIndex -1) { model.firstFlippedIndex index; return true; } // 4. 第二张翻开 — 执行匹配检查 model.secondFlippedIndex index; model.attempts; model.isLocked true; // 锁定防止额外操作 const card1 model.cards[model.firstFlippedIndex]; const card2 model.cards[model.secondFlippedIndex]; if (card1.emoji card2.emoji) { // 匹配成功 handleMatchSuccess(model); } else { // 匹配失败 — 延时翻回 handleMatchFailure(model); } return true; } function handleMatchSuccess(model: GameModel): void { model.cards[model.firstFlippedIndex].state CardState.Matched; model.cards[model.secondFlippedIndex].state CardState.Matched; model.matchedPairs; model.isLocked false; model.firstFlippedIndex -1; model.secondFlippedIndex -1; // 检查游戏是否结束 if (model.matchedPairs model.totalPairs) { model.isGameOver true; } } function handleMatchFailure(model: GameModel): void { // 使用 setTimeout 延迟 1 秒后翻回 setTimeout(() { // 翻回前再次检查状态防止在延时期间被其他操作修改 if (model.cards[model.firstFlippedIndex].state CardState.Revealed) { model.cards[model.firstFlippedIndex].state CardState.Hidden; } if (model.cards[model.secondFlippedIndex].state CardState.Revealed) { model.cards[model.secondFlippedIndex].state CardState.Hidden; } model.isLocked false; model.firstFlippedIndex -1; model.secondFlippedIndex -1; }, 1000); }3.4 锁定机制详解锁定机制是防止玩家在动画播放期间快速连点导致状态错乱的关键设计场景玩家点击了两张不同卡片 → 两张卡片翻开 → isLocked true → 启动 1 秒定时器 → 在此 1 秒内所有点击被忽略 → 1 秒后卡片翻回isLocked false如果没有锁定机制玩家可能在卡片翻回前点击第三张卡片导致firstFlippedIndex和secondFlippedIndex的逻辑混乱。3.5 卡片初始化export function initializeGame(model: GameModel, pairCount: number): void { const emojis: string[] [ , , , , , , , , , , , , , , , ]; // 选择需要的图案对 const selectedEmojis emojis.slice(0, pairCount); // 为每个图案创建两张卡片 const cardPairs: Card[] []; for (let i 0; i selectedEmojis.length; i) { cardPairs.push(new Card(i * 2, selectedEmojis[i])); cardPairs.push(new Card(i * 2 1, selectedEmojis[i])); } // Fisher-Yates 洗牌 model.cards fisherYatesShuffle(cardPairs); model.totalPairs pairCount; model.matchedPairs 0; model.attempts 0; model.firstFlippedIndex -1; model.secondFlippedIndex -1; model.isLocked false; model.isGameOver false; model.timer 0; }四、卡片组件与翻转动画4.1 卡片组件实现// CardComponent.ets Component struct CardComponent { Prop card: Card; onClick?: () void; build() { Stack() { // 卡片背面默认显示 if (this.card.state CardState.Hidden) { Column() { Text(?) .fontSize(36) .fontWeight(FontWeight.Bold) .fontColor(Color.White) } .width(100%) .height(100%) .backgroundColor(#2B6CB0) .borderRadius(12) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } // 卡片正面已翻开或已匹配 if (this.card.state ! CardState.Hidden) { Column() { Text(this.card.emoji) .fontSize(40) } .width(100%) .height(100%) .backgroundColor( this.card.state CardState.Matched ? #C6F6D5 : #FFFFFF ) .borderRadius(12) .border({ width: this.card.state CardState.Matched ? 3 : 1, color: this.card.state CardState.Matched ? #38A169 : #CBD5E0 }) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } } .width(80) .height(100) .onClick(() { if (this.onClick) this.onClick(); }) } }4.2 翻牌动画实现HarmonyOS ArkTS 提供了animateTo函数来实现流畅的翻转动画// 翻牌动画 function flipAnimation(target: Component, duration: number): void { animateTo({ duration: duration, curve: Curve.EaseInOut }, () { // 在动画块内修改引起重绘的属性 target.scale({ x: 0, y: 1 }); // 沿 X 轴缩扁 }); // 中间状态切换卡片内容 setTimeout(() { animateTo({ duration: duration, curve: Curve.EaseInOut }, () { target.scale({ x: 1, y: 1 }); // 还原 }); }, duration); }实际开发中可以使用transition结合opacity和rotate实现更逼真的 3D 翻转效果// 使用 rotate 实现 3D 翻转 .rotate({ x: 0, y: 1, z: 0, angle: this.rotationAngle })五、计时器实现// TimerManager.ets export class TimerManager { private intervalId: number -1; private callback: (seconds: number) void; private seconds: number 0; constructor(callback: (seconds: number) void) { this.callback callback; } start(): void { this.seconds 0; this.intervalId setInterval(() { this.seconds; this.callback(this.seconds); }, 1000); } stop(): void { if (this.intervalId ! -1) { clearInterval(this.intervalId); this.intervalId -1; } } reset(): void { this.stop(); this.seconds 0; this.callback(0); } getSeconds(): number { return this.seconds; } }六、主页面完整代码// Index.ets import { GameModel, Card, CardState } from ./GameModel; import { initializeGame, flipCard } from ./GameController; import { TimerManager } from ./TimerManager; Entry Component struct Index { State model: GameModel new GameModel(); private timerManager: TimerManager new TimerManager( (seconds: number) { this.model.timer seconds; } ); aboutToAppear(): void { initializeGame(this.model, 6); // 6 对12 张卡片 this.timerManager.start(); } build() { Column() { // 标题 Text(记忆翻牌 ) .fontSize(28) .fontWeight(FontWeight.Bold) .margin({ top: 20, bottom: 10 }) // 状态面板 Row() { Column() { Text(计时) .fontSize(14) .fontColor(Color.Gray) Text(${Math.floor(this.model.timer / 60)}:${String(this.model.timer % 60).padStart(2, 0)}) .fontSize(20) .fontWeight(FontWeight.Bold) } .margin({ horizontal: 20 }) Column() { Text(步数) .fontSize(14) .fontColor(Color.Gray) Text(${this.model.attempts}) .fontSize(20) .fontWeight(FontWeight.Bold) } .margin({ horizontal: 20 }) Column() { Text(配对) .fontSize(14) .fontColor(Color.Gray) Text(${this.model.matchedPairs}/${this.model.totalPairs}) .fontSize(20) .fontWeight(FontWeight.Bold) } .margin({ horizontal: 20 }) } .width(100%) .justifyContent(FlexAlign.Center) .padding(10) // 游戏结束提示 if (this.model.isGameOver) { Text( 恭喜通关) .fontSize(24) .fontWeight(FontWeight.Bold) .fontColor(#38A169) .margin({ bottom: 15 }) } // 卡片网格 Grid() { ForEach(this.model.cards, (card: Card, index: number) { CardComponent({ card: card, onClick: () { if (!this.model.isGameOver) { flipCard(this.model, index); } } }) }, (card: Card) card.id.toString()) } .columnsTemplate(1fr 1fr 1fr 1fr) .rowsTemplate(1fr 1fr 1fr) .width(360) .height(340) .padding(10) // 操作按钮 Button(this.model.isGameOver ? 再来一局 : 重新洗牌) .fontSize(18) .backgroundColor(#2B6CB0) .fontColor(Color.White) .borderRadius(8) .width(200) .height(48) .margin({ top: 15 }) .onClick(() { this.timerManager.reset(); initializeGame(this.model, 6); this.timerManager.start(); }) } .width(100%) .height(100%) .backgroundColor(#F7FAFC) } }七、HarmonyOS 特性分析7.1 State 与响应式编程在记忆翻牌应用中State装饰器被应用于GameModel对象。当卡片状态、计时器、匹配数等属性发生变化时ArkTS 框架自动追踪依赖并触发局部重渲染。State model: GameModel new GameModel();关键在于只有被 State 标记的变量变化才会触发 UI 更新。因此 model 内部的 cards 数组变化、timer 变化都需要显式赋值以触发响应。7.2 Grid 网格布局Grid() { // 卡片组件 } .columnsTemplate(1fr 1fr 1fr 1fr) // 4 列等宽 .rowsTemplate(1fr 1fr 1fr) // 3 行等高对于 6 对12 张卡片4×3 网格是最佳布局。1fr单位自动均分可用空间。7.3 条件渲染if (this.card.state CardState.Hidden) { // 显示背面 } if (this.card.state ! CardState.Hidden) { // 显示正面 }ArkTS 的if条件渲染语法可以轻松实现卡片不同状态的 UI 切换。框架会智能管理组件的创建和销毁。7.4 生命周期函数aboutToAppear(): void { initializeGame(this.model, 6); this.timerManager.start(); }aboutToAppear是组件的生命周期回调在组件即将显示时调用。适合执行初始化逻辑。八、算法深入探讨8.1 Fisher-Yates vs 其他洗牌算法算法时间复杂度空间复杂度均匀性Fisher-YatesO(n)O(1) (原地)完美均匀排序洗牌 (Sort Random)O(n log n)O(n)取决于排序稳定性随机抽取 (Random Pick)O(n²)O(n)均匀Fisher-Yates 在效率和均匀性上都是最优选择。8.2 匹配判定优化对于大规模匹配游戏如 20 对以上可以使用哈希表预计算卡片位置将匹配判定优化到 O(1)// 预计算图案 → [索引列表] const emojiMap: Mapstring, number[] new Map(); model.cards.forEach((card, index) { if (!emojiMap.has(card.emoji)) { emojiMap.set(card.emoji, []); } emojiMap.get(card.emoji)!.push(index); });8.3 难度动态调整export function calculateGridSize(pairCount: number): { cols: number, rows: number } { const totalCards pairCount * 2; // 尽可能接近正方形 let cols Math.ceil(Math.sqrt(totalCards)); while (totalCards % cols ! 0) { cols; } const rows totalCards / cols; return { cols, rows }; }九、UI/UX 设计细节9.1 交互反馈点击反馈卡片点击时有缩放动画状态区分背面蓝色背景 问号翻开白色背景 图案配对成功绿色边框 浅绿色背景游戏结束弹窗或横幅庆祝信息锁定状态视觉上不可见但用户能感受到点不动9.2 无障碍设计Emoji 图案提供视觉辨识对色盲用户友好卡片尺寸足够大80×100dp易于点击清晰的状态面板显示进度9.3 视觉风格使用柔和的灰色背景#F7FAFC卡片圆角设计12dp统一的蓝色主色调匹配成功后使用绿色进行正面反馈十、最佳实践总结10.1 代码组织MVC 模式Model-View-Controller 分离模型只负责数据存储控制器处理业务逻辑视图组件保持无状态或最小状态10.2 状态管理集中管理游戏状态避免分散使用锁定机制防止并发操作问题定期重置状态避免内存泄漏10.3 性能优化卡片数量较少时无需特殊优化对于大量卡片考虑使用 LazyForEach 按需渲染动画使用硬件加速属性transform/opacity10.4 可扩展性支持自定义图案集可扩展为图片支持不同难度等级4×3、4×4、6×6支持多人对战模式十一、总结记忆翻牌游戏是一个完美的技术实践案例涵盖了Fisher-Yates 洗牌算法—— O(n) 时间复杂度、完美均匀的随机排列翻牌与匹配逻辑—— 状态机设计、锁定机制声明式 UI—— 条件渲染、网格布局、动画HarmonyOS 特性—— State 响应式编程、生命周期、计时器通过本文的详细分析开发者可以掌握记忆翻牌游戏从设计到实现的完整流程并将这些技术应用到更复杂的游戏和应用开发项目中。记忆翻牌虽小五脏俱全是学习 ArkTS 游戏开发的绝佳入门项目。