# HarmonyOS ArkTS 小游戏开发实战(一):弹弹球物理碰撞游戏

摘要:本文详细介绍如何使用 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 等关键技术。下一期我们将探索表单验证的实现技巧,敬请期待!
更多推荐



所有评论(0)