摘要:本文详细介绍如何使用 HarmonyOS ArkTS 开发一款弹弹球(Bouncing Ball)物理碰撞游戏。涵盖 Circle 绘制、边界碰撞检测、分数累积、颜色循环以及触摸加速等核心技术点,帮助开发者快速掌握 ArkTS 声明式 UI 与交互开发技巧。
一、项目概述
弹弹球是一款经典的休闲物理小游戏,核心玩法是小球在屏幕内自由弹跳,用户点击小球使其加速并获得分数。在 HarmonyOS ArkTS 框架下,我们利用Canvas与Circle 组件的协同工作,配合onTouch 事件实现流畅的物理模拟与交互反馈。
本项目的技术亮点包括:
- 使用 ArkTS 声明式语法构建 UI 层
- Circle 组件实现圆形小球的绘制与动画
- 矩形边界碰撞检测算法
- 基于状态变量驱动的颜色循环
- 触摸事件(onTouch)触发加速逻辑
- 分数增量累积与实时显示
二、项目架构与目录结构
entry/src/main/ets/ ├── pages/ │ ├── index.ets // 主页面:游戏画布与UI │ ├── BouncingBall.ets // 弹弹球逻辑组件 │ └── GameEngine.ets // 物理引擎(碰撞检测、运动计算) └── common/ └── Constants.ets // 常量定义:颜色、速度、半径等整个应用采用分层架构:
- 表现层:index.ets 负责界面布局与状态展示
- 逻辑层:BouncingBall.ets 封装小球对象的状态与行为
- 引擎层:GameEngine.ets 实现物理碰撞检测与运动计算
- 配置层:Constants.ets 统一管理游戏参数
三、核心技术分析
3.1 Circle 组件与自定义绘制
ArkTS 提供了丰富的组件库,其中Circle是绘制圆形的声明式组件。在弹弹球游戏中,我们使用 Circle 组件来渲染小球:
// BouncingBall.ets @Component export struct BouncingBall { @Prop ballX: number = 200 @Prop ballY: number = 300 @Prop ballRadius: number = 25 @Prop ballColor: Color = Color.Red build() { Circle({ width: this.ballRadius * 2, height: this.ballRadius * 2 }) .fill(this.ballColor) .position({ x: this.ballX, y: this.ballY }) .width(this.ballRadius * 2) .height(this.ballRadius * 2) } }关键点:
Circle组件的宽高决定其大小,传入直径ballRadius * 2position属性控制小球在父容器中的绝对位置@Prop装饰器使得小球属性可以由父组件驱动更新
3.2 边界碰撞检测算法
碰撞检测是物理游戏的核心。我们的场景是一个矩形画布,小球在其中运动,碰到四边则反弹。
// GameEngine.ets export class GameEngine { // 检测并处理边界碰撞 static checkBoundaryCollision( x: number, y: number, radius: number, vx: number, vy: number, canvasWidth: number, canvasHeight: number ): { x: number, y: number, vx: number, vy: number } { let newVx = vx let newVy = vy let newX = x let newY = y // 左右边界检测 if (x - radius <= 0) { newX = radius newVx = -vx } else if (x + radius >= canvasWidth) { newX = canvasWidth - radius newVx = -vx } // 上下边界检测 if (y - radius <= 0) { newY = radius newVy = -vy } else if (y + radius >= canvasHeight) { newY = canvasHeight - radius newVy = -vy } return { x: newX, y: newY, vx: newVx, vy: newVy } } }算法要点:
| 边界 | 碰撞条件 | 反弹处理 |
|---|---|---|
| 左边界 | x - radius <= 0 | x = radius, vx = -vx |
| 右边界 | x + radius >= canvasWidth | x = canvasWidth - radius, vx = -vx |
| 上边界 | y - radius <= 0 | y = radius, vy = -vy |
| 下边界 | y + radius >= canvasHeight | y = canvasHeight - radius, vy = -vy |
碰撞检测的关键在于将圆形简化为质点,判断质点位置与边界距离是否小于半径。当检测到碰撞时,不仅反转速度方向,还需修正位置防止小球卡在边界外。
3.3 触摸加速功能
用户点击小球时,小球获得瞬时加速度。我们通过onTouch事件监听触摸位置,判断是否与小球区域重合:
// index.ets — 主页面触摸事件 @Entry @Component struct GamePage { @State ballX: number = 200 @State ballY: number = 400 @State ballVx: number = 3 @State ballVy: number = -4 @State score: number = 0 @State ballColor: Color = Color.Red private readonly CANVAS_WIDTH: number = 360 private readonly CANVAS_HEIGHT: number = 780 private readonly RADIUS: number = 30 private readonly ACCELERATION: number = 2.5 build() { Stack() { // 分数显示 Text(`得分: ${this.score}`) .fontSize(24) .fontWeight(FontWeight.Bold) .position({ x: 20, y: 40 }) // 弹弹球 Circle({ width: this.RADIUS * 2, height: this.RADIUS * 2 }) .fill(this.ballColor) .position({ x: this.ballX, y: this.ballY }) .onTouch((event: TouchEvent) => { if (event.type === TouchType.Down) { // 判断触摸点是否在小球区域内 const touchX = event.touches[0].x const touchY = event.touches[0].y const dx = touchX - (this.ballX + this.RADIUS) const dy = touchY - (this.ballY + this.RADIUS) const distance = Math.sqrt(dx * dx + dy * dy) if (distance <= this.RADIUS) { // 触摸小球:加速并计分 this.ballVx *= this.ACCELERATION this.ballVy *= this.ACCELERATION this.score++ this.changeColor() } } }) } .width('100%') .height('100%') .backgroundColor('#F5F5F5') } }onTouch 事件处理流程:
- 用户触摸屏幕,触发
onTouch回调 - 判断事件类型为
TouchType.Down(手指按下) - 获取触摸点坐标
event.touches[0].x / y - 计算触摸点与圆心的距离
- 若距离 ≤ 半径,判定为点击到小球
- 执行加速(速度乘以系数)、加分、换色
3.4 颜色循环机制
为了让游戏更具视觉吸引力,每次点击小球时切换颜色。我们定义一个颜色数组并循环索引:
private readonly colors: Color[] = [ Color.Red, Color.Blue, Color.Green, Color.Orange, Color.Pink, Color.Purple, Color.Yellow, Color.Grey ] private colorIndex: number = 0 changeColor(): void { this.colorIndex = (this.colorIndex + 1) % this.colors.length this.ballColor = this.colors[this.colorIndex] }这种循环取模的方式简洁高效,无需额外的状态判断。也可以扩展为 HSL 颜色渐变,实现更平滑的过渡效果。
3.5 帧动画驱动
使用setInterval定时更新小球位置,实现连续动画:
aboutToAppear(): void { this.timerId = setInterval(() => { this.updateBallPosition() }, 16) // 约 60 FPS } updateBallPosition(): void { let newX = this.ballX + this.ballVx let newY = this.ballY + this.ballVy // 碰撞检测 const result = GameEngine.checkBoundaryCollision( newX, newY, this.RADIUS, this.ballVx, this.ballVy, this.CANVAS_WIDTH, this.CANVAS_HEIGHT ) this.ballX = result.x this.ballY = result.y this.ballVx = result.vx this.ballVy = result.vy }帧率说明:16ms间隔约等于 60 FPS,这是大多数游戏的标准刷新率。如果感觉性能开销大,可以调整为 30ms(约 33 FPS)。
四、完整代码整合
将以上各部分整合为一个完整可运行的主页面:
// index.ets — 完整游戏页面 import { GameEngine } from './GameEngine' @Entry @Component struct BouncingBallGame { @State ballX: number = 180 @State ballY: number = 400 @State ballVx: number = 4 @State ballVy: number = -5 @State score: number = 0 @State ballColor: Color = Color.Red private readonly CANVAS_W: number = 360 private readonly CANVAS_H: number = 780 private readonly R: number = 28 private readonly COLORS: Color[] = [ Color.Red, Color.Blue, Color.Green, Color.Orange, Color.Pink, Color.Purple, Color.Yellow, Color.Grey ] private colorIdx: number = 0 private timerId: number = -1 aboutToAppear(): void { this.timerId = setInterval(() => { let nx = this.ballX + this.ballVx let ny = this.ballY + this.ballVy const ret = GameEngine.checkBoundaryCollision( nx, ny, this.R, this.ballVx, this.ballVy, this.CANVAS_W, this.CANVAS_H ) this.ballX = ret.x this.ballY = ret.y this.ballVx = ret.vx this.ballVy = ret.vy }, 16) } aboutToDisappear(): void { clearInterval(this.timerId) } changeColor(): void { this.colorIdx = (this.colorIdx + 1) % this.COLORS.length this.ballColor = this.COLORS[this.colorIdx] } build() { Stack() { // 背景 Rect() .width('100%') .height('100%') .fill('#1a1a2e') // 分数 Text(`🏆 ${this.score}`) .fontSize(28) .fontColor(Color.White) .fontWeight(FontWeight.Bold) .position({ x: 20, y: 50 }) // 小球 Circle({ width: this.R * 2, height: this.R * 2 }) .fill(this.ballColor) .position({ x: this.ballX, y: this.ballY }) .onTouch((ev: TouchEvent) => { if (ev.type === TouchType.Down) { const tx = ev.touches[0].x const ty = ev.touches[0].y const dx = tx - (this.ballX + this.R) const dy = ty - (this.ballY + this.R) if (Math.sqrt(dx * dx + dy * dy) <= this.R) { this.ballVx *= 2.0 this.ballVy *= 2.0 this.score++ this.changeColor() } } }) // 操作提示 Text('点击小球加速得分!') .fontSize(16) .fontColor('#888888') .position({ x: 90, y: 700 }) } .width('100%') .height('100%') } }五、HarmonyOS 特性深度解析
5.1 声明式 UI 与状态驱动
ArkTS 采用声明式编程范式,开发者只需描述 UI 应处于的状态,框架自动处理渲染更新。在弹弹球中:
@State标记的变量(ballX, ballY, score, ballColor)发生变化时,框架自动重绘相关组件- 无需手动操作 DOM 或调用 invalidate 方法
- 数据流单向:状态 → UI,避免双向绑定带来的调试困难
5.2 Stack 布局的妙用
Stack是一个层叠布局容器,子组件按声明顺序从下到上堆叠。在游戏中非常适合:
- 底层放背景矩形
- 中间层放分数文本
- 顶层放小球 Circle
这种布局方式使得 z-order 管理变得直观,无需像传统游戏引擎那样手动处理绘制顺序。
5.3 生命周期管理
aboutToAppear():页面即将显示时调用,适合初始化计时器aboutToDisappear():页面即将销毁时调用,必须清理计时器防止内存泄漏
六、UI/UX 设计建议
6.1 视觉风格
- 深色背景 + 亮色小球:深色背景(
#1a1a2e)减少视觉疲劳,亮色小球形成对比 - 分数动画:得分时显示 “+1” 浮动文字,增强反馈
- 拖尾效果:在小球后方绘制半透明轨迹,增加运动感
6.2 交互反馈
- 触感反馈:点击小球时配合短震动(如调用
vibrator.vibrate(50)) - 速度显示:在画布角落显示当前速度值,让用户感知加速效果
- 碰撞音效:小球碰到边界时播放简单的「砰」声
6.3 优化体验
- 初始速度随机化:每次启动游戏时,小球的速度方向随机,增加可玩性
- 难度递增:随着分数增加,小球的基础速度逐渐提高
- 暂停机制:点击暂停按钮冻结物理模拟
七、最佳实践总结
7.1 性能优化
- 避免频繁创建对象:在
updateBallPosition中复用计算结果对象 - 合理使用状态变量:只将对 UI 有影响的属性标记为
@State,内部计算变量保持为普通属性 - 定时器清理:务必在
aboutToDisappear中清理setInterval
7.2 代码组织
- 将物理引擎、小球逻辑、页面布局拆分到不同文件中
- 使用
@Prop和@State明确组件间数据关系 - 常量集中管理,避免硬编码
7.3 兼容性注意
- Circle 组件的
fill属性支持Color枚举和字符串色值 onTouch事件在模拟器中与真机行为一致,但多点触摸支持需额外处理- 不同设备的屏幕尺寸差异建议使用
breakpoint断点系统适配
八、扩展与演进
弹弹球游戏虽然简单,但可以扩展为更完整的物理引擎:
- 重力系统:增加竖直方向恒定加速度
- 多球碰撞:实现球与球之间的弹性碰撞
- 障碍物:在画布中随机生成矩形障碍物
- 粒子特效:点击小球时产生粒子爆炸效果
九、结语
通过弹弹球游戏的开发,我们深入实践了 HarmonyOS ArkTS 框架的核心能力:声明式组件、状态驱动、触摸事件与动画循环。麻雀虽小五脏俱全,这个小项目涵盖了游戏开发中最基础的物理模拟与交互模式,是学习 ArkTS 游戏开发的理想起点。
希望本文能帮助开发者快速上手 ArkTS 游戏开发,在实际项目中灵活运用 Circle、Stack、onTouch 等关键技术。下一期我们将探索表单验证的实现技巧,敬请期待!