跳到正文

容易混淆的问题 ​

这一讲的坑大多不在语法,而在「同一个值被谁拥有、什么时候该重置」。下面按主题集中对比。

容易混淆区别
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 只该读取状态。在绘制阶段加分或切状态,会让同一帧的绘制内容前后不一致,调试时极难定位。