鸿蒙NEXT小游戏开发:影象翻牌

打印 上一主题 下一主题

主题 1878|帖子 1878|积分 5634

1. 弁言

在本案例中,我们将使用鸿蒙NEXT框架开发一个简朴的影象翻牌游戏。该游戏的核心逻辑是玩家通过翻转卡片来寻找匹配的对。本文将详细先容游戏的实现过程,包括卡片的展示、匹配逻辑以及用户交互。



2. 开发环境预备

电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:Mate 60 Pro
语言:ArkTS、ArkUI

3. 项目结构

本项目主要由两个类构成:
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组件显示其值,并根据状态调解背景颜色和旋转角度。用户点击卡片时,触发相应的翻转和匹配逻辑。

4. 结论

通过本案例,我们展示了怎样使用鸿蒙NEXT框架开发一个简朴的影象翻牌游戏。该项目不仅涵盖了根本的游戏逻辑,还展示了怎样实现动画效果和用户交互。希望这个案例能为您在鸿蒙NEXT开发中提供一些启发和帮助。

5. 完整代码

  1. import { promptAction } from '@kit.ArkUI' // 导入用于显示对话框的模块
  2. // 使用装饰器来观察数据变化
  3. @ObservedV2
  4. class GameCell { // 定义单元格类
  5.   @Trace value: string; // 单元格的值,即卡片上的图案
  6.   @Trace isVisible: boolean = false; // 控制卡片是否可见
  7.   isFrontVisible: boolean = false; // 控制卡片是否正面朝上
  8.   isMatched: boolean = false; // 标记卡片是否已被匹配
  9.   isAnimationRunning: boolean = false; // 动画是否正在运行
  10.   @Trace rotationAngle: number = 0; // 卡片的旋转角度
  11.   constructor(value: string) { // 构造函数,初始化单元格
  12.     this.value = value; // 设置单元格的值
  13.   }
  14.   // 展示卡片正面的方法
  15.   revealFace(animationTime: number, callback?: () => void) {
  16.     if (this.isAnimationRunning) { // 如果已经有动画在运行,则返回
  17.       return;
  18.     }
  19.     this.isAnimationRunning = true; // 设置动画状态为运行中
  20.     animateToImmediately({ // 开始动画
  21.       duration: animationTime, // 动画持续时间
  22.       iterations: 1, // 动画迭代次数
  23.       curve: Curve.Linear, // 动画曲线类型
  24.       onFinish: () => { // 动画完成后的回调
  25.         animateToImmediately({ // 再次开始动画
  26.           duration: animationTime, // 动画持续时间
  27.           iterations: 1, // 动画迭代次数
  28.           curve: Curve.Linear, // 动画曲线类型
  29.           onFinish: () => { // 动画完成后的回调
  30.             this.isFrontVisible = true; // 设置卡片为正面朝上
  31.             this.isAnimationRunning = false; // 设置动画状态为停止
  32.             if (callback) { // 如果有回调函数,则执行
  33.               callback();
  34.             }
  35.           }
  36.         }, () => { // 动画开始时的回调
  37.           this.isVisible = true; // 设置卡片为可见
  38.           this.rotationAngle = 0; // 设置旋转角度为0
  39.         });
  40.       }
  41.     }, () => { // 动画开始时的回调
  42.       this.isVisible = false; // 设置卡片为不可见
  43.       this.rotationAngle = 90; // 设置旋转角度为90度
  44.     });
  45.   }
  46.   // 重置卡片状态的方法
  47.   reset() {
  48.     this.isVisible = false; // 设置卡片为不可见
  49.     this.rotationAngle = 180; // 设置旋转角度为180度
  50.     this.isFrontVisible = false; // 设置卡片为背面朝上
  51.     this.isAnimationRunning = false; // 设置动画状态为停止
  52.     this.isMatched = false; // 设置卡片为未匹配
  53.   }
  54.   // 隐藏卡片正面的方法
  55.   hideFace(animationTime: number, callback?: () => void) {
  56.     if (this.isAnimationRunning) { // 如果已经有动画在运行,则返回
  57.       return;
  58.     }
  59.     this.isAnimationRunning = true; // 设置动画状态为运行中
  60.     animateToImmediately({ // 开始动画
  61.       duration: animationTime, // 动画持续时间
  62.       iterations: 1, // 动画迭代次数
  63.       curve: Curve.Linear, // 动画曲线类型
  64.       onFinish: () => { // 动画完成后的回调
  65.         animateToImmediately({ // 再次开始动画
  66.           duration: animationTime, // 动画持续时间
  67.           iterations: 1, // 动画迭代次数
  68.           curve: Curve.Linear, // 动画曲线类型
  69.           onFinish: () => { // 动画完成后的回调
  70.             this.isFrontVisible = false; // 设置卡片为背面朝上
  71.             this.isAnimationRunning = false; // 设置动画状态为停止
  72.             if (callback) { // 如果有回调函数,则执行
  73.               callback();
  74.             }
  75.           }
  76.         }, () => { // 动画开始时的回调
  77.           this.isVisible = false; // 设置卡片为不可见
  78.           this.rotationAngle = 180; // 设置旋转角度为180度
  79.         });
  80.       }
  81.     }, () => { // 动画开始时的回调
  82.       this.isVisible = true; // 设置卡片为可见
  83.       this.rotationAngle = 90; // 设置旋转角度为90度
  84.     });
  85.   }
  86. }
  87. // 定义组件入口
  88. @Entry
  89. @Component
  90. struct MemoryGame { // 定义游戏组件
  91.   @State gameCells: GameCell[] = []; // 存储游戏中的所有单元格
  92.   @State cellSize: number = 150; // 单元格的大小
  93.   @State cellSpacing: number = 5; // 单元格之间的间距
  94.   @State transitionDuration: number = 150; // 过渡动画的持续时间
  95.   @State firstSelectedIndex: number | null = null; // 记录第一次选择的卡片索引
  96.   @State secondSelectedIndex: number | null = null; // 记录第二次选择的卡片索引
  97.   @State isGameOver: boolean = false; // 游戏是否结束
  98.   @State startTime: number = 0; // 游戏开始时间
  99.   aboutToAppear(): void { // 组件即将显示时触发
  100.     let cardValues: string[] = ["A", "B", "C", "D", "E", "F", "G", "H"]; // 定义卡片的值
  101.     for (let value of cardValues) { // 遍历卡片值
  102.       this.gameCells.push(new GameCell(value)); // 添加到游戏单元格数组中
  103.       this.gameCells.push(new GameCell(value)); // 每个值添加两次以形成对
  104.     }
  105.     this.shuffleCards(); // 洗牌
  106.   }
  107.   // 洗牌方法
  108.   shuffleCards() {
  109.     this.firstSelectedIndex = null; // 清除第一次选择索引
  110.     this.secondSelectedIndex = null; // 清除第二次选择索引
  111.     this.isGameOver = false; // 游戏未结束
  112.     this.startTime = Date.now(); // 设置游戏开始时间为当前时间
  113.     for (let i = 0; i < 16; i++) { // 重置所有单元格状态
  114.       this.gameCells[i].reset();
  115.     }
  116.     for (let i = this.gameCells.length - 1; i > 0; i--) { // 洗牌算法
  117.       const randomIndex = Math.floor(Math.random() * (i + 1)); // 随机索引
  118.       let tempValue = this.gameCells[i].value; // 临时保存值
  119.       this.gameCells[i].value = this.gameCells[randomIndex].value; // 交换值
  120.       this.gameCells[randomIndex].value = tempValue; // 交换值
  121.     }
  122.   }
  123.   // 检查卡片是否匹配的方法
  124.   checkForMatch() {
  125.     if (this.firstSelectedIndex !== null && this.secondSelectedIndex !== null) { // 确保两个索引都不为空
  126.       const firstCell = this.gameCells[this.firstSelectedIndex]; // 获取第一个选中的单元格
  127.       const secondCell = this.gameCells[this.secondSelectedIndex]; // 获取第二个选中的单元格
  128.       if (firstCell.value === secondCell.value) { // 如果两个单元格的值相同
  129.         firstCell.isMatched = true; // 标记为已匹配
  130.         secondCell.isMatched = true; // 标记为已匹配
  131.         this.firstSelectedIndex = null; // 清除第一次选择索引
  132.         this.secondSelectedIndex = null; // 清除第二次选择索引
  133.         if (this.gameCells.every(cell => cell.isMatched)) { // 如果所有单元格都已匹配
  134.           this.isGameOver = true; // 游戏结束
  135.           console.info("游戏结束"); // 打印信息
  136.           promptAction.showDialog({ // 显示对话框
  137.             title: '游戏胜利!', // 对话框标题
  138.             message: '恭喜你,用时:' + ((Date.now() - this.startTime) / 1000).toFixed(3) + '秒', // 对话框消息
  139.             buttons: [{ text: '重新开始', color: '#ffa500' }] // 对话框按钮
  140.           }).then(() => { // 对话框关闭后执行
  141.             this.shuffleCards(); // 重新开始游戏
  142.           });
  143.         }
  144.       } else { // 如果两个单元格的值不同
  145.         setTimeout(() => { // 延迟一段时间后
  146.           if (this.firstSelectedIndex !== null) { // 如果第一个索引不为空
  147.             this.gameCells[this.firstSelectedIndex].hideFace(this.transitionDuration, () => { // 隐藏卡片
  148.               this.firstSelectedIndex = null; // 清除第一次选择索引
  149.             });
  150.           }
  151.           if (this.secondSelectedIndex !== null) { // 如果第二个索引不为空
  152.             this.gameCells[this.secondSelectedIndex].hideFace(this.transitionDuration, () => { // 隐藏卡片
  153.               this.secondSelectedIndex = null; // 清除第二次选择索引
  154.             });
  155.           }
  156.         }, 400); // 延迟时间
  157.       }
  158.     }
  159.   }
  160.   // 构建游戏界面的方法
  161.   build() {
  162.     Column({ space: 20 }) { // 创建一个垂直布局
  163.       Flex({ wrap: FlexWrap.Wrap }) { // 创建一个可换行的弹性布局
  164.         ForEach(this.gameCells, (gameCell: GameCell, index: number) => { // 遍历游戏单元格
  165.           Text(`${gameCell.isVisible ? gameCell.value : ''}`) // 显示单元格的值或空字符串
  166.             .width(`${this.cellSize}lpx`) // 设置宽度
  167.             .height(`${this.cellSize}lpx`) // 设置高度
  168.             .margin(`${this.cellSpacing}lpx`) // 设置边距
  169.             .fontSize(`${this.cellSize / 2}lpx`) // 设置字体大小
  170.             .textAlign(TextAlign.Center) // 文本居中
  171.             .backgroundColor(gameCell.isVisible ? Color.Orange : Color.Gray) // 设置背景颜色
  172.             .fontColor(Color.White) // 设置字体颜色
  173.             .borderRadius(5) // 设置圆角
  174.             .rotate({ // 设置旋转
  175.               x: 0,
  176.               y: 1,
  177.               z: 0,
  178.               angle: gameCell.rotationAngle, // 旋转角度
  179.               centerX: `${this.cellSize / 2}lpx`, // 中心点X坐标
  180.               centerY: `${this.cellSize / 2}lpx`, // 中心点Y坐标
  181.             })
  182.             .onClick(() => { // 单击事件处理
  183.               if (this.isGameOver) { // 如果游戏已结束
  184.                 console.info("游戏已结束"); // 打印信息
  185.                 return;
  186.               }
  187.               if (gameCell.isMatched) { // 如果单元格已匹配
  188.                 console.info("当前已标记"); // 打印信息
  189.                 return;
  190.               }
  191.               if (this.firstSelectedIndex == null) { // 如果没有第一次选择
  192.                 this.firstSelectedIndex = index; // 设置第一次选择索引
  193.                 if (!gameCell.isFrontVisible) { // 如果不是正面朝上
  194.                   gameCell.revealFace(this.transitionDuration); // 展示正面
  195.                 }
  196.               } else if (this.firstSelectedIndex == index) { // 如果与第一次选择相同
  197.                 console.info("和上一次点击的是一样的,不予处理"); // 打印信息
  198.               } else if (this.secondSelectedIndex == null) { // 如果没有第二次选择
  199.                 this.secondSelectedIndex = index; // 设置第二次选择索引
  200.                 if (!gameCell.isFrontVisible) { // 如果不是正面朝上
  201.                   gameCell.revealFace(this.transitionDuration, () => { // 展示正面
  202.                     this.checkForMatch(); // 检查是否匹配
  203.                   });
  204.                 }
  205.               }
  206.             });
  207.         });
  208.       }.width(`${(this.cellSize + this.cellSpacing * 2) * 4}lpx`); // 设置宽度
  209.       Button('重新开始') // 创建“重新开始”按钮
  210.         .onClick(() => { // 按钮点击事件
  211.           this.shuffleCards(); // 重新开始游戏
  212.         });
  213.     }.height('100%').width('100%'); // 设置高度和宽度
  214.   }
  215. }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

老婆出轨

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表