打砖块是一款经典的游戏,它简单易懂却又充满挑战性。本文将介绍如何使用ArkUI框架开发一个完整的打砖块游戏,涵盖游戏逻辑设计、UI实现和交互处理等核心内容。
游戏架构设计
我们的打砖块游戏采用了组件化设计,主要分为两个部分:
- BrickBreakerGame组件:负责游戏的核心逻辑和渲染
- BrickBreakerPage组件:作为游戏的主页面容器
游戏状态管理使用了枚举类型GameState
来区分三种状态:
enum GameState {
READY, // 准备中
PLAYING, // 游戏中
OVER // 游戏结束
}
核心游戏逻辑实现
1. 游戏初始化
在initGame()
方法中,我们初始化了游戏的所有元素:
- 创建砖块矩阵(5行6列)
- 设置挡板和球的初始位置
- 重置分数和生命值
initGame() {
this.bricks = [];
for (let row = 0; row < BRICK_ROWS; row++) {
let colArray: boolean[] = [];
for (let col = 0; col < BRICK_COLS; col++) {
colArray.push(true);
}
this.bricks.push(colArray);
}
// 其他初始化代码...
}
2. 游戏主循环
游戏主循环通过setInterval
实现,每16毫秒执行一次,负责:
- 更新球的位置
- 检测碰撞(墙壁、挡板、砖块)
- 处理游戏结束条件
gameLoop() {
// 移动球
this.ballX += this.ballDX;
this.ballY += this.ballDY;
// 碰撞检测逻辑...
}
3. 碰撞检测
碰撞检测是游戏的核心逻辑之一,我们实现了:
- 球与墙壁碰撞:改变球的运动方向
- 球与挡板碰撞:根据击中位置改变反弹角度
- 球与砖块碰撞:消除砖块并增加分数
挡板碰撞处理特别加入了根据击中位置改变反弹角度的逻辑,增加了游戏的可玩性:
const hitPos = (this.ballX - this.paddleX) / PADDLE_WIDTH;
this.ballDX = (hitPos - 0.5) * 10; // -5到5之间的值
this.ballDY = -Math.abs(this.ballDY);
UI设计与实现
1. 游戏元素渲染
使用ArkUI的声明式语法,我们实现了各种游戏元素的渲染:
砖块渲染:
ForEach(this.bricks, (row: boolean[], rowIndex:number) => {
ForEach(row, (brickExists: boolean, colIndex: number) => {
if (brickExists) {
Column()
.width(BRICK_WIDTH - 2)
.height(BRICK_HEIGHT - 2)
.backgroundColor(this.colors[rowIndex])
// 其他样式...
}
})
})
挡板和球:
// 挡板
Column()
.width(PADDLE_WIDTH)
.height(PADDLE_HEIGHT)
.backgroundColor('#4CAF50')
.position({ x: this.paddleX, y: GAME_HEIGHT - PADDLE_HEIGHT - 10 })
// 球
Column()
.width(BALL_SIZE)
.height(BALL_SIZE)
.backgroundColor('#FFC107')
.position({ x: this.ballX, y: this.ballY })
2. 游戏状态UI
根据游戏状态显示不同的UI元素:
准备界面:
if (this.gameState === GameState.READY) {
Column() {
Text('打砖块')
.fontSize(32)
Text('点击开始游戏')
.fontSize(20)
}
// 其他样式...
}
游戏结束界面:
} else if (this.gameState === GameState.OVER) {
Column() {
Text('游戏结束')
.fontSize(28)
Text(`得分: ${this.score}`)
.fontSize(22)
Text('点击重新开始')
.fontSize(18)
}
// 其他样式...
}
交互处理
游戏通过触摸事件来控制挡板移动:
.handleMove(event: TouchEvent) {
if (this.gameState === GameState.PLAYING) {
const touchX = event.touches[0].x;
this.paddleX = Math.max(
0,
Math.min(GAME_WIDTH - PADDLE_WIDTH, touchX - PADDLE_WIDTH / 2)
);
}
}
点击事件用于开始游戏或重新开始:
.onClick(() => {
if (this.gameState === GameState.READY) {
this.startGame();
} else if (this.gameState === GameState.OVER) {
this.gameState = GameState.READY;
}
})
附:源代码
import { promptAction } from "@kit.ArkUI";
import { Color } from "@ohos.graphics.scene";
// 游戏常量定义
const GAME_WIDTH = 360; // 游戏区域宽度
const GAME_HEIGHT = 600; // 游戏区域高度
const PADDLE_WIDTH = 80; // 挡板宽度
const PADDLE_HEIGHT = 15; // 挡板高度
const BALL_SIZE = 15; // 球大小
const BRICK_WIDTH = 60; // 砖块宽度
const BRICK_HEIGHT = 20; // 砖块高度
const BRICK_ROWS = 5; // 砖块行数
const BRICK_COLS = 6; // 砖块列数
const PADDLE_SPEED = 8; // 挡板移动速度
const BALL_SPEED = 4; // 球初始速度
// 游戏状态
enum GameState {
READY, // 准备中
PLAYING, // 游戏中
OVER // 游戏结束
}
// 游戏主逻辑
@Component
struct BrickBreakerGame {
@State gameState: GameState = GameState.READY
@State paddleX: number = GAME_WIDTH / 2 - PADDLE_WIDTH / 2
@State ballX: number = GAME_WIDTH / 2
@State ballY: number = GAME_HEIGHT - 100
@State ballDX: number = BALL_SPEED
@State ballDY: number = -BALL_SPEED
@State bricks: boolean[][] = []
@State score: number = 0
@State lives: number = 3
private gameLoopId: number = 0
@State colors:string[] = ['#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3']
// 初始化游戏
initGame() {
// 初始化砖块
this.bricks = [];
for (let row = 0; row < BRICK_ROWS; row++) {
let colArray: boolean[] = [];
for (let col = 0; col < BRICK_COLS; col++) {
colArray.push(true);
}
this.bricks.push(colArray);
}
this.paddleX = GAME_WIDTH / 2 - PADDLE_WIDTH / 2;
this.ballX = GAME_WIDTH / 2;
this.ballY = GAME_HEIGHT - 100;
this.ballDX = BALL_SPEED;
this.ballDY = -BALL_SPEED;
this.score = 0;
this.lives = 3;
}
// 开始游戏
startGame() {
this.gameState = GameState.PLAYING;
this.initGame();
// 启动游戏循环
this.gameLoopId = setInterval(() => {
this.gameLoop();
}, 16);
}
// 游戏结束
gameOver() {
this.gameState = GameState.OVER;
clearInterval(this.gameLoopId);
promptAction.showToast({
message: `游戏结束! 得分: ${this.score}`,
duration: 2000
});
}
// 游戏主循环
gameLoop() {
// 移动球
this.ballX += this.ballDX;
this.ballY += this.ballDY;
// 检测球与墙壁碰撞
if (this.ballX < 0 || this.ballX + BALL_SIZE > GAME_WIDTH) {
this.ballDX = -this.ballDX;
}
if (this.ballY < 0) {
this.ballDY = -this.ballDY;
}
// 检测球与挡板碰撞
if (this.ballY + BALL_SIZE > GAME_HEIGHT - PADDLE_HEIGHT &&
this.ballY + BALL_SIZE < GAME_HEIGHT &&
this.ballX + BALL_SIZE > this.paddleX &&
this.ballX < this.paddleX + PADDLE_WIDTH) {
// 根据击中挡板的位置改变反弹角度
const hitPos = (this.ballX - this.paddleX) / PADDLE_WIDTH;
this.ballDX = (hitPos - 0.5) * 10; // -5到5之间的值
this.ballDY = -Math.abs(this.ballDY); // 确保向上反弹
}
// 检测球与砖块碰撞
for (let row = 0; row < BRICK_ROWS; row++) {
for (let col = 0; col < BRICK_COLS; col++) {
if (this.bricks[row][col]) {
const brickX = col * BRICK_WIDTH;
const brickY = row * BRICK_HEIGHT + 50; // 顶部留出空间
if (this.ballX + BALL_SIZE > brickX &&
this.ballX < brickX + BRICK_WIDTH &&
this.ballY + BALL_SIZE > brickY &&
this.ballY < brickY + BRICK_HEIGHT) {
// 创建新数组以确保UI刷新
const newBricks = [...this.bricks];
newBricks[row] = [...newBricks[row]];
newBricks[row][col] = false;
this.bricks = newBricks;
this.score += 10;
// 根据碰撞位置决定反弹方向
if (this.ballX + BALL_SIZE / 2 < brickX ||
this.ballX + BALL_SIZE / 2 > brickX + BRICK_WIDTH) {
this.ballDX = -this.ballDX;
} else {
this.ballDY = -this.ballDY;
}
}
}
}
}
// 检测球是否落到底部
if (this.ballY + BALL_SIZE > GAME_HEIGHT) {
this.lives--;
if (this.lives <= 0) {
this.gameOver();
} else {
// 重置球位置
this.ballX = GAME_WIDTH / 2;
this.ballY = GAME_HEIGHT - 100;
this.ballDX = BALL_SPEED;
this.ballDY = -BALL_SPEED;
}
}
// 检查是否所有砖块都被消除
const allBricksGone = this.bricks.every(row =>
row.every(brick => !brick)
);
if (allBricksGone) {
promptAction.showToast({
message: `恭喜通关! 得分: ${this.score}`,
duration: 2000
});
this.initGame(); // 重新开始新一局
}
}
// 处理触摸移动事件
handleMove(event: TouchEvent) {
if (this.gameState === GameState.PLAYING) {
const touchX = event.touches[0].x;
this.paddleX = Math.max(
0,
Math.min(GAME_WIDTH - PADDLE_WIDTH, touchX - PADDLE_WIDTH / 2)
);
}
}
build() {
Stack() {
// 游戏背景
Column()
.width(GAME_WIDTH)
.height(GAME_HEIGHT)
.backgroundColor('#1a1a1a')
.border({ width: 2, color: '#4a4a4a' })
// 绘制砖块
ForEach(this.bricks, (row: boolean[], rowIndex:number) => {
ForEach(row, (brickExists: boolean, colIndex: number) => {
if (brickExists) {
Column()
.width(BRICK_WIDTH - 2)
.height(BRICK_HEIGHT - 2)
.backgroundColor(this.colors[rowIndex])
.borderRadius(6)
.shadow({ color: '#00000040', radius: 4 })
.position({
x: colIndex * BRICK_WIDTH + 1,
y: rowIndex * BRICK_HEIGHT + 50
})
}
}, (brickExists: boolean, colIndex: number) => {
return colIndex.toString();
})
})
// 绘制挡板
Column()
.width(PADDLE_WIDTH)
.height(PADDLE_HEIGHT)
.backgroundColor('#4CAF50')
.borderRadius(12)
.shadow({ color: '#00000040', radius: 4 })
.position({ x: this.paddleX, y: GAME_HEIGHT - PADDLE_HEIGHT - 10 })
// 绘制球
Column()
.width(BALL_SIZE)
.height(BALL_SIZE)
.backgroundColor('#FFC107')
.borderRadius(BALL_SIZE / 2)
.shadow({ color: '#00000040', radius: 4 })
.position({ x: this.ballX, y: this.ballY })
// 游戏状态提示
if (this.gameState === GameState.READY) {
Column() {
Text('打砖块')
.fontSize(32)
.fontColor('#fff')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 24 })
Text('点击开始游戏')
.fontSize(20)
.fontColor('#ffffffCC')
.padding({ top: 8, bottom: 8, left: 24, right: 24 })
.borderRadius(20)
.border({ width: 1, color: '#ffffff40' })
}
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.position({ x: GAME_WIDTH / 2, y: GAME_HEIGHT / 2 - 40 })
} else if (this.gameState === GameState.OVER) {
Column() {
Text('游戏结束')
.fontSize(28)
.fontColor('#fff')
.fontWeight(FontWeight.Bold)
.margin({ bottom: 12 })
Text(`得分: ${this.score}`)
.fontSize(22)
.fontColor('#ffd700')
.margin({ bottom: 20 })
Text('点击重新开始')
.fontSize(18)
.fontColor('#ffffffCC')
.padding({ top: 6, bottom: 6, left: 20, right: 20 })
.borderRadius(18)
.border({ width: 1, color: '#ffffff40' })
}
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.position({ x: GAME_WIDTH / 2, y: GAME_HEIGHT / 2 - 20 })
}
// 分数和生命显示
Row() {
Column({ space: 4 }) {
Row() {
Image($r("app.media.star"))
.width(16)
.height(16)
Text(`得分: ${this.score}`)
.fontSize(16)
.fontColor('#fff')
.margin({left: 4})
}
.alignItems(VerticalAlign.Center)
Row() {
Image($r("app.media.background"))
.width(16)
.height(16)
Text(`生命: ${this.lives}`)
.fontSize(16)
.fontColor('#fff')
.margin({left: 4})
}
.alignItems(VerticalAlign.Center)
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#00000066')
.borderRadius(12)
}
.position({ x: 16, y: 16 })
}
.width(GAME_WIDTH)
.height(GAME_HEIGHT)
.onClick(() => {
if (this.gameState === GameState.READY) {
this.startGame();
} else if (this.gameState === GameState.OVER) {
this.gameState = GameState.READY;
}
})
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Move) {
this.handleMove(event)
}
})
}
}
// 主页面
@Entry
@Component
struct BrickBreakerPage {
build() {
Column() {
// 游戏标题
Row() {
Image($r("app.media.background"))
.width(32)
.height(32)
.margin({ right: 10 })
Text('打砖块')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#2c2c2c')
}
.alignItems(VerticalAlign.Center)
.margin({ top: 24, bottom: 16 })
// 游戏区域
BrickBreakerGame()
.width(GAME_WIDTH)
.height(GAME_HEIGHT)
.margin({ top: 12 })
.border({ width: 2, color: '#e0e0e0' })
.borderRadius(16)
.shadow({ color: '#00000020', radius: 8 })
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.padding({ top: 12, bottom: 12 })
.backgroundColor('#f0f0f0')
}
}
总结
通过这个打砖块游戏的开发实践,我们展示了如何使用ArkUI框架实现一个完整的游戏应用。关键点包括:
- 游戏状态管理
- 游戏主循环实现
- 碰撞检测算法
- 声明式UI渲染
- 用户交互处理
这个项目不仅演示了ArkUI的基本用法,也展示了如何将游戏逻辑与UI框架结合。开发者可以在此基础上进一步扩展,如添加音效、更多关卡、特殊道具等功能,使游戏更加丰富有趣。
ArkUI的声明式开发模式使得游戏UI的实现变得简洁直观,而TypeScript的强类型特性则帮助我们在开发复杂游戏逻辑时减少错误。这种组合为HarmonyOS应用开发提供了强大的工具集。