在简单游戏中设置计时器(附帧规则)
摘要
一篇博客文章,介绍了PICO-8游戏中使用单张表和循环的简单计时器系统,并附带说明了类似计时器在《超级马力欧兄弟》中如何导致了速通中的帧规则。
<p><a href="https://lobste.rs/s/1efcss/setting_timers_simple_games_feat_frame">评论</a></p>
查看缓存全文
缓存时间: 2026/07/29 17:57
# 在简单游戏中设置计时器(附:帧规则)
来源:https://lynn.github.io/blog/pico-timers/
2026年7月24日
最近我一直在阅读《超级马力欧兄弟》的反汇编源码(https://6502disassembly.com/nes-smb/SuperMarioBros.html),并且了解到,在它的代码中,未来的事件调度方式与我用 Lua 制作快速游戏 Jam 作品(比如 o moku e mun(https://rose.systems/omokuemun/))时的方式非常相似。
这简单得几乎算不上一个系统!但我喜欢它既容易理解又灵活的特点。我将用 Lua 来描述它,用于 PICO-8 或 Picotron 游戏。只需两行代码!读吧,然后尽情流泪吧。
## PICO-8 中的计时器
首先,定义一个表 `T`。我想添加一点 Lua 魔法,使得 `T.foo` 在未设置时返回 0。这有点奇怪,但在游戏 Jam 中可以节省宝贵的时间,不用来回跳到文件顶部添加 `T.foo = 0` 来防止 `nil` 错误。代码如下:
```lua
local T = setmetatable({}, {__index = function() return 0 end})
```
然后,在 `_update` 函数(每帧执行一次)中,将所有计时器减一:
```lua
for k,v in pairs(T) do T[k]=max(0,v-1) end
```
就是这样!在任何时候,`T.foo` 都表示 `foo` 计时器剩余的帧数。
### 1. 让某些事情持续发生
假设你想让玩家发光一秒钟,也就是 60 帧。
你只需写 `T.glow = 60` 来启动计时器,然后在其他地方检查 `T.glow > 0` 来决定是否绘制发光精灵。
要停止发光,写 `T.glow = 0`。要延长发光时间,再次触发 `T.glow = 60`。简单!
### 2. 让某些事情稍后发生
一只青蛙在左边空白处打盹。假设你想在两秒后安排一个 *嘀!* 音效。
你设置 `T.blip = 120` 来启动计时器,然后在更新函数中写:`if (T.blip == 1) sfx(2)`。
要取消计时器,只需设置 `T.blip = 0`。
你也可以写像 `if (T.blip % 10 == 1) sfx(2)` 这样的代码,以便在计时器运行时重复发出嘀声。
### 3. 非全局计时器
你也可以拥有多个 `T`。也许你的 `Enemy` 对象各自都有 `self.T`。只需确保你的 `Enemy:update` 将表中的所有项都减一即可。
## 超级马力欧兄弟中的计时器
现在,在编写 NES 游戏时,没有花哨的键值对表。所有计时器都连续存储在 RAM 中,然后由这个循环(https://6502disassembly.com/nes-smb/SuperMarioBros.html#SymDecTimersLoop)统一递减。
每个计时器只能保存一个字节的值,而 NES 以 60 fps 运行,因此我们无法通过这种方式设置很长时间的计时器。`0xff` 帧只有 4.25 秒。为了解决这个问题,大约一半的计时器被编程为只有在一种称为 *IntervalTimerControl*(在反汇编中这样称呼)的“上级计时器”指示时才会递减。
翻译成伪 Lua 代码,就像这样:
```lua
itc = 0
-- 每帧调用
function DecTimers()
last_timer = 20
itc = itc - 1
if itc < 0 then
itc = 20 -- 20,19,18...0 = 21 帧时钟
last_timer = 35
end
-- 递减这些计时器!
for i = 0, last_timer do
if timers[i] > 0 then
timers[i] = timers[i] - 1
end
end
end
```
大多数时候,`last_timer` 是 20,因此这个函数只递减“快速”计时器 `timer[0]` 到 `timer[20]`。但每 21 帧,*IntervalTimerControl* 到期,此时*所有*计时器 `timer[0]` 到 `timer[35]` 都会被递减。
特别地,*IntervalTimerControl* 在每个关卡开始时重置,而一个“慢速”计时器用于控制关卡结束时的动画。这就是超级马力欧兄弟速通中著名的帧规则(https://en.wikipedia.org/wiki/Super_Mario_Bros._speedrunning#Frame_rules)的起源!你的游戏操作无法影响 *IntervalTimerControl* 的值,而关卡结束动画计时器与之同步,因此你通常只能以 21 帧的增量来提升关卡时间。
相似文章
在PICO-8上模拟进化
一篇关于在PICO-8幻想游戏机上模拟进化过程的文章,可能展示了一个创意编码项目或教育工具。
基础振荡器的快速合成
一篇技术博客文章,详细介绍了面向GBA和rp2040的快速整数振荡器实现,涵盖锯齿波、方波、三角波和脉冲波,重点在于速度而非减少混叠。
@ClaudeDevs: https://x.com/ClaudeDevs/status/2074208949205881033
一份实用指南,解释了在 Claude Code 中配置编码代理的不同循环类型(基于回合、基于目标、基于时间),包括如何定义停止条件以及改进自我验证。
The Art of CHIP-8
A deep-dive technical article exploring the CHIP-8 programming language, its instruction set, interpreter quirks, and strategies for writing portable CHIP-8 programs.
用RP2350监视Z80
一篇博客文章,探讨如何使用带PIO的Raspberry Pi Pico RP2350来监视Z80微处理器的地址和数据总线,包括时序考虑和时钟速度限制。