
【引言】
在本案例中,我们将使用鸿蒙NEXT框架开发一个简单的记忆翻牌游戏。该游戏的核心逻辑是玩家通过翻转卡片来寻找匹配的对。本文将详细介绍游戏的实现过程,包括卡片的展示、匹配逻辑以及用户交互。
【开发环境准备】
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI
【项目结构】
本项目主要由两个类组成:
GameCell:表示游戏中的单个卡片,负责卡片的状态管理和动画效果。
MemoryGame:游戏组件,管理游戏逻辑和用户界面。
GameCell类
GameCell类是游戏的基本单元,包含以下属性和方法:
属性:
value: 卡片的值(如"A", "B"等)。
isVisible: 控制卡片是否可见。
isFrontVisible: 控制卡片是否正面朝上。
isMatched: 标记卡片是否已被匹配。
isAnimationRunning: 动画是否正在运行。
rotationAngle: 卡片的旋转角度。
方法:
revealFace(animationTime, callback): 展示卡片正面并执行动画。
hideFace(animationTime, callback): 隐藏卡片正面并执行动画。
reset(): 重置卡片状态。
动画实现
在revealFace和hideFace方法中,我们使用animateToImmediately函数来实现卡片的翻转动画。通过设置动画的持续时间、迭代次数和曲线类型,确保用户体验流畅。
MemoryGame类
MemoryGame类是游戏的核心组件,负责管理游戏状态和用户交互。
属性:
gameCells: 存储所有卡片的数组。
cellSize: 卡片的大小。
cellSpacing: 卡片之间的间距。
transitionDuration: 动画过渡的持续时间。
firstSelectedIndex和secondSelectedIndex: 记录用户选择的卡片索引。
isGameOver: 游戏是否结束的标志。
方法:
aboutToAppear(): 初始化游戏卡片并洗牌。
shuffleCards(): 洗牌算法,随机打乱卡片顺序。
checkForMatch(): 检查用户选择的两张卡片是否匹配。
build(): 构建游戏界面。
游戏逻辑
在checkForMatch方法中,我们判断用户选择的两张卡片是否相同。如果相同,则将其标记为已匹配;如果不同,则在延迟后隐藏卡片。游戏结束时,弹出对话框提示用户胜利,并提供重新开始的选项。
用户界面
在build方法中,我们使用Column和Flex布局来组织卡片的展示。每张卡片通过Text组件显示其值,并根据状态调整背景颜色和旋转角度。用户点击卡片时,触发相应的翻转和匹配逻辑。
结论
通过本案例,我们展示了如何使用鸿蒙NEXT框架开发一个简单的记忆翻牌游戏。该项目不仅涵盖了基本的游戏逻辑,还展示了如何实现动画效果和用户交互。希望这个案例能为您在鸿蒙NEXT开发中提供一些启发和帮助。
【完整代码】
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;
});
}
}, () => {
this.isVisible = false;
this.rotationAngle = 90;
});
}
reset() {
this.isVisible = false;
this.rotationAngle = 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;
});
}
}, () => {
this.isVisible = true;
this.rotationAngle = 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`,
centerY: `${this.cellSize / 2}lpx`,
})
.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%');
}
}
- 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.