容易混淆的问题
这一讲的坑大多不在语法,而在「同一个值被谁拥有、什么时候该重置」。下面按主题集中对比。
| 容易混淆 | 区别 |
|---|---|
wasPressed / isDown | 本帧按下一次 vs 每帧轮询,跳跃必须用前者 |
dy 赋值 / 累减 | 跳跃是 dy = -300,累减会让长按无限加速 |
位置乘不乘 dt | 不乘则帧率越高掉得越快,跳跃高度随显示器变化 |
| 取模 / 清零 | 取模留下余数,倒计时与滚动都不漂移 |
单根 Pipe / PipePair | 一个管一个对象 vs 一对管共用一个 X |
scaleY = -1 / 加高度 | 翻转以绘制点为原点,补 PIPE_HEIGHT 才留在原位 |
pairs / ipairs | 遍历所有键(含字符串) vs 只遍历 1..n 的数组 |
迭代中 remove / 标记后删 | 前者会跳过相邻元素,后者安全 |
init / enter | 实例化一次 vs 每次切入都跑,参数只能从 enter 拿 |
scored / remove | 防止重复计分 vs 推迟删除,两个标记互不相干 |
输入与帧
为什么跳跃会连着触发。 wasPressed 读的是「本帧按下的键」集合,集合必须在每帧末尾清空。如果忘了清空,条件在鸟下落期间一直为真,dy 每帧被重置成负数,鸟会贴在天花板上抖。反过来,如果在 love.keypressed 里直接清空,按下事件和检测发生在同一帧的不同阶段,也可能读不到。
lua
function love.update(dt)
gStateMachine:update(dt)
love.keyboard.keysPressed = {}
love.mouse.buttonsPressed = {}
end键盘回调只能有一份。 love.keypressed 是全局回调,写在 Bird.lua 里也会被覆盖,所以课程用自建表转发。这是 LÖVE 的结构限制,不是 Lua 的限制。
表与迭代
迭代中删除会跳元素。 pairs 遍历时删掉当前项,后面的元素整体前移,迭代器接着访问下一个下标,等于跳过了一个。三种安全写法:先标记再统一删、倒序 for i = #t, 1, -1、或者收集下标最后批量删。管道用第一种,因为 scored、remove 这类标记本来就要挂在对象上。
lua
-- 倒序删除:下标不受前移影响
for i = #pipes, 1, -1 do
if pipes[i].remove then
table.remove(pipes, i)
end
end#t 只对连续数组可靠。 表里混了字符串键时,# 的结果不保证是最后一个整数下标,table.insert 仍然会追加到 #t + 1,但读的时候别依赖。
资源与生命周期
love.load只跑一次。 字体、图片、音源都该在这里创建。放进init会随着每次状态重建重复加载,几分钟后显存和内存一起涨;放进render更糟,每帧一次。- 状态对象是短命的。
change每次新建实例,所以状态里的字段不能指望跨局保留。要跨局的数据放存档文件或main.lua的局部变量,别放在某个状态里。 - 同一音源重播会打断自身。 想叠音就
clone(),否则第二声会把第一声掐掉 - 音源也是资源。
newSource会解析整个文件,重复调用得到的是独立副本,不是同一份缓存。 - 字体只该建一次。
love.graphics.newFont解析字体文件并有开销,setFont只是换引用。把newFont写进render是常见的性能自杀。 pairs的顺序不保证。 管道更新顺序不影响结果,但音效叠加、绘制层次如果依赖遍历顺序,就该换成数组加ipairs。顺序不保证意味着每次运行可能不同,测试时别依赖它。
绘制与缩放
push.start与push.finish必须成对。 中间漏掉finish,之后所有绘制都会落在虚拟画布上却不缩放,窗口一大整体偏移。- 翻转不是镜像到另一边。
love.graphics.draw的缩放以绘制点为原点,scaleY = -1让图片向上长,看起来整根管子飞到了屏幕上方。加PIPE_HEIGHT相当于先把原点挪到底部,翻转后正好填回原位置。 - 回绕点不等于图片宽。 回绕点是图案重复的周期,图片宽是纹理尺寸。两者相等时最省心,画一张就够;周期小于图片宽时必须保证「屏宽 + 周期 ≤ 图片宽」,否则回绕的瞬间右侧露白。
- 分数别在
draw里改。render只该读取状态。在绘制阶段加分或切状态,会让同一帧的绘制内容前后不一致,调试时极难定位。