在这里插入图片描述

摘要:本文详细介绍如何使用 HarmonyOS ArkTS 开发一款弹弹球(Bouncing Ball)物理碰撞游戏。涵盖 Circle 绘制、边界碰撞检测、分数累积、颜色循环以及触摸加速等核心技术点,帮助开发者快速掌握 ArkTS 声明式 UI 与交互开发技巧。


一、项目概述

弹弹球是一款经典的休闲物理小游戏,核心玩法是小球在屏幕内自由弹跳,用户点击小球使其加速并获得分数。在 HarmonyOS ArkTS 框架下,我们利用 CanvasCircle 组件 的协同工作,配合 onTouch 事件 实现流畅的物理模拟与交互反馈。

本项目的技术亮点包括:

  • 使用 ArkTS 声明式语法构建 UI 层
  • Circle 组件实现圆形小球的绘制与动画
  • 矩形边界碰撞检测算法
  • 基于状态变量驱动的颜色循环
  • 触摸事件(onTouch)触发加速逻辑
  • 分数增量累积与实时显示

二、项目架构与目录结构

entry/src/main/ets/
├── pages/
│   ├── index.ets          // 主页面:游戏画布与UI
│   ├── BouncingBall.ets   // 弹弹球逻辑组件
│   └── GameEngine.ets     // 物理引擎(碰撞检测、运动计算)
└── common/
    └── Constants.ets      // 常量定义:颜色、速度、半径等

整个应用采用 分层架构

  1. 表现层:index.ets 负责界面布局与状态展示
  2. 逻辑层:BouncingBall.ets 封装小球对象的状态与行为
  3. 引擎层:GameEngine.ets 实现物理碰撞检测与运动计算
  4. 配置层: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 * 2
  • position 属性控制小球在父容器中的绝对位置
  • @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 事件处理流程

  1. 用户触摸屏幕,触发 onTouch 回调
  2. 判断事件类型为 TouchType.Down(手指按下)
  3. 获取触摸点坐标 event.touches[0].x / y
  4. 计算触摸点与圆心的距离
  5. 若距离 ≤ 半径,判定为点击到小球
  6. 执行加速(速度乘以系数)、加分、换色

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 是一个层叠布局容器,子组件按声明顺序从下到上堆叠。在游戏中非常适合:

  1. 底层放背景矩形
  2. 中间层放分数文本
  3. 顶层放小球 Circle

这种布局方式使得 z-order 管理变得直观,无需像传统游戏引擎那样手动处理绘制顺序。

5.3 生命周期管理

  • aboutToAppear():页面即将显示时调用,适合初始化计时器
  • aboutToDisappear():页面即将销毁时调用,必须清理计时器防止内存泄漏

六、UI/UX 设计建议

6.1 视觉风格

  1. 深色背景 + 亮色小球:深色背景(#1a1a2e)减少视觉疲劳,亮色小球形成对比
  2. 分数动画:得分时显示 “+1” 浮动文字,增强反馈
  3. 拖尾效果:在小球后方绘制半透明轨迹,增加运动感

6.2 交互反馈

  1. 触感反馈:点击小球时配合短震动(如调用 vibrator.vibrate(50)
  2. 速度显示:在画布角落显示当前速度值,让用户感知加速效果
  3. 碰撞音效:小球碰到边界时播放简单的「砰」声

6.3 优化体验

  1. 初始速度随机化:每次启动游戏时,小球的速度方向随机,增加可玩性
  2. 难度递增:随着分数增加,小球的基础速度逐渐提高
  3. 暂停机制:点击暂停按钮冻结物理模拟

七、最佳实践总结

7.1 性能优化

  • 避免频繁创建对象:在 updateBallPosition 中复用计算结果对象
  • 合理使用状态变量:只将对 UI 有影响的属性标记为 @State,内部计算变量保持为普通属性
  • 定时器清理:务必在 aboutToDisappear 中清理 setInterval

7.2 代码组织

  • 将物理引擎、小球逻辑、页面布局拆分到不同文件中
  • 使用 @Prop@State 明确组件间数据关系
  • 常量集中管理,避免硬编码

7.3 兼容性注意

  • Circle 组件的 fill 属性支持 Color 枚举和字符串色值
  • onTouch 事件在模拟器中与真机行为一致,但多点触摸支持需额外处理
  • 不同设备的屏幕尺寸差异建议使用 breakpoint 断点系统适配

八、扩展与演进

弹弹球游戏虽然简单,但可以扩展为更完整的物理引擎:

  1. 重力系统:增加竖直方向恒定加速度
  2. 多球碰撞:实现球与球之间的弹性碰撞
  3. 障碍物:在画布中随机生成矩形障碍物
  4. 粒子特效:点击小球时产生粒子爆炸效果

九、结语

通过弹弹球游戏的开发,我们深入实践了 HarmonyOS ArkTS 框架的核心能力:声明式组件、状态驱动、触摸事件与动画循环。麻雀虽小五脏俱全,这个小项目涵盖了游戏开发中最基础的物理模拟与交互模式,是学习 ArkTS 游戏开发的理想起点。

希望本文能帮助开发者快速上手 ArkTS 游戏开发,在实际项目中灵活运用 Circle、Stack、onTouch 等关键技术。下一期我们将探索表单验证的实现技巧,敬请期待!


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐