鸿蒙开发案例:通过三杯猜球学习显示动画

【引言】
“三杯猜球”是一个经典的益智游戏,通常由一名表演者和多名参与者共同完成。表演者会将一个小球放在一个杯子下面,然后将三个杯子快速地交换位置,参与者则需要猜出最终哪个杯子下面有小球。本文将介绍如何使用HarmonyOS NEXT技术,如装饰器、状态管理和动画,来实现一个“三杯猜球”游戏。
【实现目标】
创建一个交互式的游戏,让玩家能够:
1. 开始游戏:通过点击“开始游戏”按钮启动游戏,触发杯子间的随机交换。
2. 调整动画速度:允许用户通过界面上的控制器来调整游戏过程中杯子交换的速度。
3. 调整混合次数:让用户可以设置每局游戏中杯子的混合次数。
4. 显示杯子内容:当动画停止后,玩家可以通过点击任意一个杯子来查看其下面是否有小球。
5. 自动重置:如果所有预定的交换次数完成,游戏会自动重置,等待下一轮开始。
【开发逻辑】
1. 定义杯子类:创建 Cup 类,定义杯子的属性和构造函数。
2. 实现游戏逻辑:
• 初始化游戏状态。
• 实现 startGame() 方法,用于开始游戏。
• 实现 moveCups() 方法,用于移动杯子。
• 实现 swapBalls() 方法,用于交换杯子内的球。
• 实现 resetCupPosition() 方法,用于重置杯子的位置。
3. 动画效果:使用动画库(animateToImmediately)实现杯子的动画效果。
【完整代码】
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 | // 使用装饰器来追踪对象的变化 @ObservedV2 class Cup { // 使用装饰器来追踪属性的变化 @Trace positionX: number; // 杯子的X轴位置 @Trace positionY: number; // 杯子的Y轴位置 @Trace containsBall: boolean; // 杯子内是否有球 @Trace isRevealed: boolean; // 杯子是否打开 // 构造函数初始化杯子的状态 constructor(hasBall: boolean) { this .positionX = 0; this .positionY = 0; this .containsBall = hasBall; this .isRevealed = true ; } } // 游戏入口组件 @Entry @Component struct ThreeCupGame { // 游戏状态变量 @State gameCups: Cup[] = [ // 初始化三个杯子,其中一个有球 new Cup( true ), new Cup( false ), new Cup( false ) ]; @State cupWidth: number = 200; // 杯子宽度 @State cupSpacing: number = 10; // 杯子之间的间距 @State animationDurationMs: number = 140; // 动画持续时间(毫秒) @State isGameAnimating: boolean = false ; // 是否正在动画中 @State mixingCount: number = 5; // 每局游戏混合次数 @State currentMixingCount: number = 0; // 当前正在进行的混合次数计数 // 开始游戏的方法 startGame() { this .currentMixingCount--; // 减少混合次数 const cupPairs = [[0, 1], [0, 2], [1, 2]]; // 可能的杯子对组合 const selectedPair = cupPairs[Math.floor(Math.random() * cupPairs.length)]; // 随机选择一对 this .moveCups(selectedPair[0], selectedPair[1]); // 开始移动选定的两个杯子 } // 移动指定的两个杯子 moveCups(cupIndex1: number, cupIndex2: number) { const direction: number = Math.random() < 0.5 ? -1 : 1; // 随机方向 const distanceFactor: number = Math.abs(cupIndex1 - cupIndex2); // 距离因子 const adjustedDistanceFactor: number = distanceFactor === 1 ? 2 : 1; // 根据距离调整因子 animateToImmediately({ delay: 0, duration: this .animationDurationMs }, () => { this .gameCups[cupIndex1].positionY = -direction * ( this .cupWidth + this .cupSpacing * 2) / adjustedDistanceFactor }) animateToImmediately({ delay: this .animationDurationMs, duration: this .animationDurationMs }, () => { this .gameCups[cupIndex1].positionX = ( this .cupWidth + this .cupSpacing * 2) * distanceFactor this .gameCups[cupIndex1].positionY = -direction * ( this .cupWidth + this .cupSpacing * 2) / adjustedDistanceFactor }) animateToImmediately({ delay: this .animationDurationMs * 2, duration: this .animationDurationMs }, () => { this .gameCups[cupIndex1].positionX = ( this .cupWidth + this .cupSpacing * 2) * distanceFactor this .gameCups[cupIndex1].positionY = 0 }) animateToImmediately({ delay: 0, duration: this .animationDurationMs }, () => { this .gameCups[cupIndex2].positionY = direction * ( this .cupWidth + this .cupSpacing * 2) / adjustedDistanceFactor }) animateToImmediately({ delay: this .animationDurationMs, duration: this .animationDurationMs }, () => { this .gameCups[cupIndex2].positionX = -( this .cupWidth + this .cupSpacing * 2) * distanceFactor this .gameCups[cupIndex2].positionY = direction * ( this .cupWidth + this .cupSpacing * 2) / adjustedDistanceFactor }) animateToImmediately({ delay: this .animationDurationMs * 2, duration: this .animationDurationMs, onFinish: () => { this .swapBalls(cupIndex1, cupIndex2) } }, () => { this .gameCups[cupIndex2].positionX = -( this .cupWidth + this .cupSpacing * 2) * distanceFactor this .gameCups[cupIndex2].positionY = 0 }) } // 重置杯子的位置 resetCupPosition(cupIndex: number) { this .gameCups[cupIndex].positionX = 0; this .gameCups[cupIndex].positionY = 0; } // 交换两个杯子内的球 swapBalls(cupIndex1: number, cupIndex2: number) { this .resetCupPosition(cupIndex1); this .resetCupPosition(cupIndex2); let temporaryBallStatus = this .gameCups[cupIndex1].containsBall; this .gameCups[cupIndex1].containsBall = this .gameCups[cupIndex2].containsBall; this .gameCups[cupIndex2].containsBall = temporaryBallStatus; if ( this .currentMixingCount <= 0) { this .isGameAnimating = false ; } else { setTimeout(() => { this .startGame(); }, 10); } } // 构建游戏界面 build() { Column({ space: 20 }) { // 游戏标题 Text( '猜小球游戏' ) .fontSize(24) .margin({ top: 20 }); // 动画速度控制器 Counter() { Text(`当前速度${ this .animationDurationMs}毫秒`) .fontColor(Color.Black) .fontSize( '26lpx' ); }.width( '400lpx' ).onInc(() => { this .animationDurationMs += 10; }).onDec(() => { this .animationDurationMs -= 10; this .animationDurationMs = this .animationDurationMs < 10 ? 10 : this .animationDurationMs; }); // 混合次数控制器 Counter() { Text(`每局混合${ this .mixingCount}次`) .fontColor(Color.Black) .fontSize( '26lpx' ); }.width( '400lpx' ).onInc(() => { this .mixingCount += 1; }).onDec(() => { this .mixingCount -= 1; this .mixingCount = this .mixingCount < 1 ? 1 : this .mixingCount }); // 杯子布局 Row() { ForEach( this .gameCups, (cup: Cup) => { Text(cup.isRevealed ? (cup.containsBall ? '小球' : '空' ) : '' ) .width(`${ this .cupWidth}lpx`) .height(`${ this .cupWidth}lpx`) .margin(`${ this .cupSpacing}lpx`) .backgroundColor(Color.Orange) .fontSize(`${ this .cupWidth / 4}lpx`) .textAlign(TextAlign.Center) .fontColor(Color.White) .borderRadius(5) .translate({ x: `${cup.positionX}lpx`, y: `${cup.positionY}lpx` }) .onClick(() => { if (! this .isGameAnimating) { cup.isRevealed = true ; } }); }); }.justifyContent(FlexAlign.Center).width( '100%' ).height( '720lpx' ).backgroundColor(Color.Gray); // 开始游戏按钮 Button( '开始游戏' ).onClick(() => { if (! this .isGameAnimating) { this .currentMixingCount = this .mixingCount; this .isGameAnimating = true ; this .gameCups.forEach(cup => cup.isRevealed = false ); this .startGame(); } }); }.width( '100%' ).height( '100%' ); } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了