用 Canvas 做一个消除类小游戏
最近在项目里做了个小玩法:屏幕上方不断掉字符,玩家在字符落到键位附近时按对应按键,命中就加分,漏掉就掉血。
我一开始没有先抠视觉,而是先把几件事定住:
- 一个下落字符要有哪些字段(出现时间、目标键位、当前位置、是否命中)
- 命中后特效怎么记录(位置、开始时间、类型)
- 难度怎么控制(掉落速度、同屏数量、容错)
这些先定好,后面就不容易乱。
先说最核心的一点:别用“每帧位移”,用“时间算位置”
我最开始就避开了“每帧 y + 速度”这套。
因为帧率一抖,手感就会变,低性能机器和高性能机器会像两个游戏。
所以我直接按时间算:
- 音符什么时候生成
- 当前时间离生成时间过去了多久
- 再换算成当前位置
这样即使偶尔掉帧,整体节奏也不会散。
1 | const getNoteY = useCallback((note: FallingNote, currentMs: number, keyPositions: Map<string, any>) => { |
位置计算逻辑是线性的插值:
- 起点:
top = -NOTE_SIZE(音符刚出现时在屏幕上方) - 终点:
targetY = 对应键位的 y - 进度:
progress = (currentMs - spawnAtMs) / dropMs - 最终:
y = top + progress * (targetY - top)
也就是说:
progress = 0时,音符在起点progress = 1时,音符到达键位- 中间按比例移动
所以你掉帧时,下一帧会直接按“真实经过的时间”算到正确位置,不会因为少渲染几帧就整体变慢。
输入判定也要提前设计,不然会很难补
键盘事件和字符不是天然一一对应的,尤其是大小写、符号、空格这些细节。
我的做法是先把“字符 -> 可触发按键”这层映射建好,按下去时只做筛选:
- 先找这次按键可能命中的目标
- 再从场上挑“在判定区里、并且最近的那个”
- 命中就给
Perfect/Good,没打中就Miss
这样做的好处是:同屏多个字符时,不会乱判。
真正踩坑的是暂停恢复
这个坑我中招过一次:
暂停时看起来停住了,但系统时间没停。恢复那一瞬间,很多字符会“直接穿越到底部”,然后连着触发 Miss。
后来我的修法很直接:恢复时统一补时间,把暂停这段加回去。
为什么我最后还是选 Canvas
这类玩法如果用 DOM 也能做,但对象一多、运动一复杂,Canvas 会更稳一些。
我现在的拆法是:
- 运动对象(下落字符、特效)走 Canvas
- UI(分数、血条、按钮)继续放在 React/DOM
这样调试和维护都比较顺手,性能也更可控。
