主题
05 · AABB 碰撞 + 迷你街机原型(验收)
目标:实现轴对齐矩形碰撞(AABB);拼出无引擎可玩的迷你街机射击(移动、射击、敌机、得分);作为阶段一里程碑。
前置:04 · 输入与实体
本篇代码可全部贴进 02 的 Vite 项目;不要求资源图与音效。
1. AABB:两个矩形是否重叠
轴对齐包围盒(Axis-Aligned Bounding Box):边平行于坐标轴的矩形。
ts
type AABB = { x: number; y: number; w: number; h: number }
function hit(a: AABB, b: AABB): boolean {
return (
a.x < b.x + b.w &&
a.x + a.w > b.x &&
a.y < b.y + b.h &&
a.y + a.h > b.y
)
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
分离轴一句话:只要在 x 或 y 上投影分开,就不撞。
斜向旋转矩形、像素级碰撞留给引擎;阶段一 AABB 足够做街机判定。
2. 玩法规格(验收用)
| 项 | 规格 |
|---|---|
| 画布 | 480×640 |
| 玩家 | 底部,左右移动;空格向上发射子弹 |
| 子弹 | 匀速向上;出屏删除 |
| 敌人 | 从顶部随机 x 出现,匀速下落;出屏扣分或忽略(自选,需文档一致) |
| 命中 | 子弹与敌人 AABB 重叠 → 双方移除,分数 +1 |
| 目标 | 连续可玩约 30 秒不崩;能看见分数变化 |
失败条件可简化:敌人碰到玩家 → Game Over 文案 + 按 R 重置。也可第一版只做「打分无限模式」。
3. 状态结构(建议)
ts
type Bullet = AABB & { vy: number }
type Enemy = AABB & { vy: number }
const player = { x: 220, y: 560, w: 40, h: 24, vx: 0, vy: 0 }
const bullets: Bullet[] = []
const enemies: Enemy[] = []
let score = 0
let spawnTimer = 0
let fireCd = 0
let gameOver = false1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
数组 + 每帧过滤,阶段一够用;对象池见阶段三 16(待写)。
4. 核心 update 骨架
ts
const STEP = 1 / 60
function spawnBullet() {
bullets.push({
x: player.x + player.w / 2 - 3,
y: player.y - 10,
w: 6,
h: 12,
vy: -420,
})
}
function spawnEnemy() {
const w = 28
enemies.push({
x: Math.random() * (canvas.width - w),
y: -w,
w,
h: w,
vy: 80 + Math.random() * 60,
})
}
function update(fixedDt: number) {
if (gameOver) return
// 移动玩家(同 04)
// ...
fireCd -= fixedDt
if (keys.has('Space') && fireCd <= 0) {
spawnBullet()
fireCd = 0.25 // 冷却,避免空格边沿漏检时也可控
}
spawnTimer -= fixedDt
if (spawnTimer <= 0) {
spawnEnemy()
spawnTimer = 0.7
}
for (const b of bullets) b.y += b.vy * fixedDt
for (const e of enemies) e.y += e.vy * fixedDt
// 碰撞:子弹 × 敌人
for (let i = bullets.length - 1; i >= 0; i--) {
for (let j = enemies.length - 1; j >= 0; j--) {
if (hit(bullets[i], enemies[j])) {
bullets.splice(i, 1)
enemies.splice(j, 1)
score += 1
break
}
}
}
// 出屏清理
for (let i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].y + bullets[i].h < 0) bullets.splice(i, 1)
}
for (let i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].y > canvas.height) enemies.splice(i, 1)
}
// 可选:敌人撞玩家
for (const e of enemies) {
if (hit(player, e)) gameOver = true
}
}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
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
从后往前 splice,避免正序删除时跳过元素。
5. render 与 HUD
ts
function render() {
ctx.fillStyle = '#0b1220'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = '#fbbf24'
ctx.fillRect(player.x, player.y, player.w, player.h)
ctx.fillStyle = '#e2e8f0'
for (const b of bullets) ctx.fillRect(b.x, b.y, b.w, b.h)
ctx.fillStyle = '#f87171'
for (const e of enemies) ctx.fillRect(e.x, e.y, e.w, e.h)
ctx.fillStyle = '#f8fafc'
ctx.font = '16px sans-serif'
ctx.fillText(`Score: ${score}`, 12, 24)
if (gameOver) {
ctx.fillText('Game Over — press R', 140, 320)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
keydown 里若 gameOver && e.code === 'KeyR',重置数组与分数即可。
6. 阶段一总验收(打勾再进 Phaser)
- [ ] 固定步长循环仍在跑(03)
- [ ] 左右移动 + 边界钳制(04)
- [ ] 空格发射,子弹向上飞,出屏消失
- [ ] 敌机周期性生成并下落
- [ ]
hit()命中后分数增加,双方消失 - [ ] 可连续玩约 30 秒无明显泄漏(子弹/敌人数组不会无限涨)
- [ ] (可选)撞玩家 Game Over + R 重置
7. 接下来(阶段二)
同一玩法,换 Phaser 3 + TypeScript:用 Scene、Arcade Physics、Group / overlap 重做,并补 HUD 与一关闭环。
→ 06 · Phaser 3 + TypeScript 工程脚手架
练习代码默认留在本机 Vite 项目;若日后要进仓库,可建 blog/游戏/demos/(本批不建)。
官方入口
- MDN · 碰撞检测概述(AABB 一节)
- 下一引擎:Phaser 3 文档
