鸿蒙HarmonyOS 5小游戏实践:记忆翻牌(附:源代码)
记忆翻牌游戏是一款经典的益智游戏,它能有效锻炼玩家的记忆力和观察能力。本文将详细介绍如何使用鸿蒙(HarmonyOS)的ArkUI框架开发一款完整的记忆翻牌游戏,涵盖游戏设计、核心逻辑实现和界面构建的全过程。
游戏设计概述
记忆翻牌游戏的基本规则很简单:玩家需要翻开卡片并找出所有匹配的卡片对。在我们的实现中,游戏包含以下特点:
- 4×4的棋盘布局(16张卡片,8对图案)
- 使用可爱的动物表情符号作为卡片内容
- 计时和计步功能
- 新游戏和重新开始功能
- 游戏胜利提示
游戏状态管理
在鸿蒙开发中,状态管理是关键。我们使用@State
装饰器来管理游戏的各种状态:
@State cards: Card[] = []; // 所有卡片数组
@State firstCard: number | null = null; // 第一张翻开的卡片索引
@State secondCard: number | null = null; // 第二张翻开的卡片索引
@State moves: number = 0; // 移动步数
@State gameOver: boolean = false; // 游戏是否结束
@State timer: number = 0; // 游戏用时
这种状态管理方式确保了当这些值发生变化时,UI能够自动更新。
核心游戏逻辑实现
1. 游戏初始化
游戏初始化包括创建卡片对、洗牌和设置初始状态:
startNewGame() {// 重置游戏状态this.moves = 0;this.timer = 0;this.gameOver = false;this.firstCard = null;this.secondCard = null;// 创建卡片对let cardValues: string[] = [];for (let i = 0; i < this.PAIRS_COUNT; i++) {cardValues.push(this.CARD_TYPES[i]);cardValues.push(this.CARD_TYPES[i]);}// 洗牌this.shuffleArray(cardValues);// 初始化卡片状态this.cards = cardValues.map(value => ({value,flipped: false,matched: false})).slice(0); // 使用slice(0)确保UI更新// 开始计时this.timerInterval = setInterval(() => {this.timer++;}, 1000);
}
2. 洗牌算法
我们使用经典的Fisher-Yates洗牌算法来随机排列卡片:
private shuffleArray(array: string[]) {for (let i = array.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));const temp = array[i];array[i] = array[j];array[j] = temp;}
}
3. 卡片点击处理
卡片点击是游戏的核心交互,需要处理多种情况:
handleCardClick(index: number) {// 检查是否可点击if (this.gameOver || this.cards[index].matched || this.cards[index].flipped) {return;}if (this.firstCard !== null && this.secondCard !== null) {return;}// 创建新数组触发UI更新let newCards = this.cards.slice(0);newCards[index].flipped = true;this.cards = newCards;// 设置第一张或第二张卡片if (this.firstCard === null) {this.firstCard = index;} else {this.secondCard = index;this.moves++;this.checkMatch(); // 检查匹配}
}
4. 匹配检查
匹配检查逻辑决定了游戏的胜负:
private checkMatch() {if (this.firstCard === null || this.secondCard === null) return;if (this.cards[this.firstCard].value === this.cards[this.secondCard].value) {// 匹配成功let newCards = this.cards.slice(0);newCards[this.firstCard].matched = true;newCards[this.secondCard].matched = true;this.cards = newCards;this.firstCard = null;this.secondCard = null;this.checkGameOver(); // 检查游戏是否结束} else {// 不匹配,1秒后翻回setTimeout(() => {let newCards = this.cards.slice(0);if (this.firstCard !== null) newCards[this.firstCard].flipped = false;if (this.secondCard !== null) newCards[this.secondCard].flipped = false;this.cards = newCards;this.firstCard = null;this.secondCard = null;}, 1000);}
}
界面构建
鸿蒙的ArkUI框架提供了声明式的UI构建方式,我们使用Grid布局来构建4×4的游戏棋盘:
build() {Column() {// 游戏标题和信息显示Text('记忆翻牌游戏').fontSize(24).fontWeight(FontWeight.Bold)Row() {Text(`步数: ${this.moves}`)Text(`时间: ${this.formatTime(this.timer)}`)}// 游戏棋盘Grid() {ForEach(this.cards, (card: Card, index) => {GridItem() {this.CardView(card, index)}})}.columnsTemplate('1fr 1fr 1fr 1fr').rowsTemplate('1fr 1fr 1fr 1fr')// 新游戏按钮Button('新游戏').onClick(() => this.startNewGame())// 游戏结束提示if (this.gameOver) {Text('恭喜通关!')}}
}
卡片视图使用Stack和Column组合实现:
@Builder
CardView(card: Card, index: number) {Stack() {Column() {if (!card.flipped) {Text('?') // 卡片背面} else {Text(card.value) // 卡片正面}}.backgroundColor(card.flipped ? (card.matched ? '#4CAF50' : '#FFFFFF') : '#2196F3').borderRadius(10)}.onClick(() => {this.handleCardClick(index);})
}
关键技术与注意事项
- 状态管理:在鸿蒙开发中,直接修改数组元素不会触发UI更新。我们需要使用
slice(0)
创建新数组,然后修改并重新赋值给状态变量。 - 定时器管理:游戏计时器需要在组件销毁或游戏重新开始时正确清理,避免内存泄漏。
- UI更新优化:通过将卡片视图提取为独立的
@Builder
方法,可以提高代码的可读性和维护性。 - 用户体验:
-
- 添加了1秒的延迟让玩家有机会记住不匹配的卡片
- 匹配成功的卡片变为绿色,提供视觉反馈
- 显示游戏时间和步数,增加挑战性
附:代码
// MemoryGame.ets
@Entry
@Component
struct MemoryGame {// 游戏配置private readonly CARD_TYPES = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼'];private readonly PAIRS_COUNT = 8;private readonly BOARD_SIZE = 4;// 游戏状态@State cards: Card[] = [];@State firstCard: number | null = null;@State secondCard: number | null = null;@State moves: number = 0;@State gameOver: boolean = false;@State timer: number = 0;private timerInterval: number | null = null;aboutToAppear() {this.startNewGame();}startNewGame() {if (this.timerInterval) {clearInterval(this.timerInterval);}this.moves = 0;this.timer = 0;this.gameOver = false;this.firstCard = null;this.secondCard = null;let cardValues: string[] = [];for (let i = 0; i < this.PAIRS_COUNT; i++) {cardValues.push(this.CARD_TYPES[i]);cardValues.push(this.CARD_TYPES[i]);}this.shuffleArray(cardValues);// 使用slice(0)创建新数组触发UI更新// 初始化卡片this.cards = cardValues.map(value => {let card: Card = {value: value,flipped: false,matched: false};return card}).slice(0);this.timerInterval = setInterval(() => {this.timer++;}, 1000);}private shuffleArray(array: string[]) {for (let i = array.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));const temp = array[i];array[i] = array[j];array[j] = temp;}}handleCardClick(index: number) {if (this.gameOver || this.cards[index].matched || this.cards[index].flipped) {return;}if (this.firstCard !== null && this.secondCard !== null) {return;}// 创建新数组触发UI更新let newCards = this.cards.slice(0);newCards[index].flipped = true;this.cards = newCards;if (this.firstCard === null) {this.firstCard = index;} else {this.secondCard = index;this.moves++;this.checkMatch();}}private checkMatch() {if (this.firstCard === null || this.secondCard === null) return;if (this.cards[this.firstCard].value === this.cards[this.secondCard].value) {// 匹配成功let newCards = this.cards.slice(0);newCards[this.firstCard].matched = true;newCards[this.secondCard].matched = true;this.cards = newCards;this.firstCard = null;this.secondCard = null;this.checkGameOver();} else {// 不匹配,1秒后翻回setTimeout(() => {let newCards = this.cards.slice(0);if (this.firstCard !== null) newCards[this.firstCard].flipped = false;if (this.secondCard !== null) newCards[this.secondCard].flipped = false;this.cards = newCards;this.firstCard = null;this.secondCard = null;}, 1000);}}private checkGameOver() {this.gameOver = this.cards.every(card => card.matched);if (this.gameOver && this.timerInterval) {clearInterval(this.timerInterval);}}private formatTime(seconds: number): string {const mins = Math.floor(seconds / 60);const secs = seconds % 60;return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;}build() {Column() {Text('记忆翻牌游戏').fontSize(24).fontWeight(FontWeight.Bold).margin({ bottom: 20 })Row() {Text(`步数: ${this.moves}`).fontSize(16).layoutWeight(1)Text(`时间: ${this.formatTime(this.timer)}`).fontSize(16).layoutWeight(1)}.width('100%').margin({ bottom: 20 })Grid() {ForEach(this.cards, (card: Card, index) => {GridItem() {this.CardView(card, index)}})}.columnsTemplate('1fr 1fr 1fr 1fr').rowsTemplate('1fr 1fr 1fr 1fr').width('100%').height(400).margin({ bottom: 20 })Button('新游戏').width(200).height(40).backgroundColor('#4CAF50').fontColor(Color.White).onClick(() => this.startNewGame())if (this.gameOver) {Text('恭喜通关!').fontSize(20).fontColor(Color.Red).margin({ top: 20 })}}.width('100%').height('100%').padding(20).justifyContent(FlexAlign.Center)}@BuilderCardView(card: Card, index: number) {Stack() {Column() {if (!card.flipped) {Text('?').fontSize(30)} else {Text(card.value).fontSize(30)}}.width('90%').height('90%').backgroundColor(card.flipped ? (card.matched ? '#4CAF50' : '#FFFFFF') : '#2196F3').borderRadius(10).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}.width('100%').height('100%').onClick(() => {this.handleCardClick(index);})}
}interface Card {value: string;flipped: boolean;matched: boolean;
}
总结
通过这个记忆翻牌游戏的开发,我们学习了鸿蒙应用开发中的几个重要概念:
- 使用
@State
管理应用状态 - 声明式UI构建方式
- 数组状态更新的正确方法
- 定时器的使用和管理
- 用户交互处理的最佳实践
这款游戏虽然简单,但涵盖了鸿蒙应用开发的许多核心概念。开发者可以在此基础上进一步扩展,比如添加难度选择、音效、动画效果、高分记录等功能,打造更加丰富的游戏体验。
鸿蒙的ArkUI框架为开发者提供了强大的工具来构建响应式、高性能的应用。通过这个实战项目,希望能帮助开发者更好地理解鸿蒙应用开发的思路和方法。