记忆翻牌游戏是一款经典的益智游戏,它能有效锻炼玩家的记忆力和观察能力。本文将详细介绍如何使用鸿蒙(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)
}
@Builder
CardView(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框架为开发者提供了强大的工具来构建响应式、高性能的应用。通过这个实战项目,希望能帮助开发者更好地理解鸿蒙应用开发的思路和方法。