主题
04 · 输入与实体:键盘控制的可移动方块
目标:把键盘事件与游戏状态分离;用实体
{ x, y, w, h, vx, vy }驱动可移动方块;做好画布边界钳制。
前置:03 · 渲染帧与逻辑帧
1. 事件 ≠ 每帧逻辑
错误直觉:在 keydown 里直接改 x。
更好做法(前端也很熟——受控状态):
text
浏览器事件 ──写入──► keys 状态表
│
固定 update 每步 ──读取──┘──► 算 vx / 改 x1
2
3
2
3
好处:
- 长按移动稳定(不依赖系统按键重复间隔)
- 一帧内多次逻辑步读到的是同一套按键快照
- 暂停游戏时只需停止
update,不必卸监听
2. 按键状态表
ts
const keys = new Set<string>()
window.addEventListener('keydown', (e) => {
keys.add(e.code)
// 方向键滚动页面时可选:
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Space'].includes(e.code)) {
e.preventDefault()
}
})
window.addEventListener('keyup', (e) => {
keys.delete(e.code)
})1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
用 e.code(ArrowLeft)而不是 e.key(受布局影响)。触控以后可写成同一套 input.axisX,阶段一先键盘。
3. 实体:游戏里的「组件数据」
ts
type RectEntity = {
x: number
y: number
w: number
h: number
vx: number
vy: number
}
const player: RectEntity = {
x: 220,
y: 560,
w: 40,
h: 24,
vx: 0,
vy: 0,
}
const PLAYER_SPEED = 260 // px/s1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对照前端:更像 store 里的纯数据,而不是带着 DOM 的组件实例。渲染时:
ts
ctx.fillStyle = '#fbbf24'
ctx.fillRect(player.x, player.y, player.w, player.h)1
2
2
4. 在固定 update 里读输入
接 03 的 STEP:
ts
function update(fixedDt: number) {
let axis = 0
if (keys.has('ArrowLeft') || keys.has('KeyA')) axis -= 1
if (keys.has('ArrowRight') || keys.has('KeyD')) axis += 1
player.vx = axis * PLAYER_SPEED
player.x += player.vx * fixedDt
// 边界钳制(画布宽 480)
if (player.x < 0) player.x = 0
if (player.x + player.w > canvas.width) {
player.x = canvas.width - player.w
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
街机射击里玩家通常只左右移动;vy 留给子弹/敌人。
5. 输入边沿:开火只触发一次
长按空格若每逻辑帧都生成子弹,会射成机关枪。需要「按下边沿」:
ts
let spaceWasDown = false
function update(fixedDt: number) {
// ...移动...
const spaceDown = keys.has('Space')
if (spaceDown && !spaceWasDown) {
spawnBullet() // 下一篇实现
}
spaceWasDown = spaceDown
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
或使用冷却计时器(cooldown -= fixedDt),实战里两者常组合。完整开火见 05。
6. 常见坑
| 坑 | 处理 |
|---|---|
| 焦点在按钮/地址栏 | canvas 或 window 监听;提示用户点一下页面 |
keydown 不触发 preventDefault | 页面跟着滚 |
用 key 判断 a/A | 改用 code:KeyA |
| 把实体做成 class 过早抽象 | 阶段一 object 字面量足够 |
验收清单
- [ ] 左右方向键(或 A/D)可平滑移动方块
- [ ] 松开按键立即停,不依赖系统 repeat
- [ ] 方块不会移出画布左右边界
- [ ] 输入监听在 rAF 循环外只注册一次
- [ ] (可选)空格边沿检测能
console.log一次一下
