游戏刻与微时序#
本文档基于 Minecraft Java 版 yarn 映射源码,深度剖析一个"游戏刻"内部究竟发生了什么,以及技术社区中常说的"微时序"到底指什么。
什么是游戏刻 (Game Tick)#
Minecraft 以 20 TPS(每秒 20 刻)运行,每刻 50ms。如果服务器卡顿导致 50ms 内处理不完,刻就会"拉长",TPS 下降。
一个游戏刻不是单一操作,而是一整套有序的事件流水线。理解各阶段的执行顺序,是红石机械设计和性能优化的基础。
一个游戏刻的完整执行顺序#
以下是 ServerWorld.tick() 中定义的完整刻流程:
┌─────────────────────────────────────────────────────────────┐
│ 一个游戏刻的执行顺序 │
├─────────────────────────────────────────────────────────────┤
│ 1. worldBorder.tick() 世界边界检查 │
│ 2. tickWeather() 天气更新(下雨/雷电) │
│ 3. 睡眠处理 玩家睡觉、跳过黑夜 │
│ 4. calculateAmbientDarkness() 环境黑暗度计算 │
│ 5. tickTime() 推进游戏时间 │
├─────────────────────────────────────────────────────────────┤
│ 6. blockTickScheduler.tick() 方块 tick(★微时序核心) │
│ ├─ EXTREMELY_HIGH (-3) 最先执行 │
│ ├─ VERY_HIGH (-2) │
│ ├─ HIGH (-1) 红石比较器、红石火把 │
│ ├─ NORMAL ( 0) 中继器、活塞、大多数方块 │
│ ├─ LOW ( 1) │
│ ├─ VERY_LOW ( 2) │
│ └─ EXTREMELY_LOW ( 3) 最后执行 │
│ ※ 同优先级按 subTickOrder 顺序执行(先调度先执行) │
├─────────────────────────────────────────────────────────────┤
│ 7. fluidTickScheduler.tick() 流体 tick(水/熔岩流动) │
│ 8. raidManager.tick() 袭击事件处理 │
│ 9. chunkManager.tick() 区块生成与加载 │
│ 10. processSyncedBlockEvents() 方块事件同步(活塞动画等) │
├─────────────────────────────────────────────────────────────┤
│ 11. entity.tick() 实体 tick │
│ 12. player.tick() 玩家 tick │
│ 13. world.getChunkManager().tick() 区块管理杂项 │
└─────────────────────────────────────────────────────────────┘微时序是什么#
微时序 (Microtiming) 指的是一个游戏刻内部的事件执行顺序。在社区中,这个词通常特指:
- 方块 tick 的优先级差异:红石火把(HIGH)比中继器(NORMAL)先 tick
- 同优先级的执行顺序:由
subTickOrder计数器决定(FIFO) - 方块事件 vs 方块 tick 的时序差:方块事件在 blockTicks 之后处理
为什么微时序重要#
考虑一个经典场景:0-tick 脉冲。早期版本中,利用"方块先设置状态再通知邻居"的时序差可以构造瞬间脉冲。理解微时序可以帮助你:
- 设计不依赖位置的稳定红石电路
- 调试时序敏感的机械(如刷沙机、刷铁轨机)
- 预判方块更新在不同位置的执行先后
TickPriority 枚举#
源码中 TickPriority 定义了 7 个优先级等级:
public enum TickPriority {
EXTREMELY_HIGH(-3), // 极高:立即执行
VERY_HIGH(-2), // 很高
HIGH(-1), // 高:红石比较器、红石火把
NORMAL(0), // 普通:中继器、活塞等大多数方块
LOW(1), // 低
VERY_LOW(2), // 很低
EXTREMELY_LOW(3); // 极低
}常见组件的优先级#
| 组件 | 优先级 | 说明 |
|---|---|---|
| 红石比较器 | HIGH (-1) | 比中继器先响应 |
| 红石火把 | HIGH (-1) | 熄灭/亮起早一拍 |
| 红石中继器 | NORMAL (0) | 标准时序 |
| 活塞方块事件 | NORMAL (0) | 但在 blockTicks 之后处理 |
| 红石线 | NORMAL (0) | 与中继器同优先级 |
| 随机作物生长 | NORMAL (0) | 与红石同优先级 |
subTickOrder:同优先级的执行顺序#
当多个方块 tick 具有相同的 triggerTick 和 priority 时,由 subTickOrder(64 位递增计数器)决定顺序:
调度顺序:A → B → C → D
subTickOrder:1 2 3 4
执行顺序: A 先,然后 B,然后 C,然后 D这意味着:先被安排 tick 的方块,先执行。
这怎么影响红石#
假设同一刻中两根红石火把被同时安排了 tick:
- 位置 A 先被邻居更新触发调度 → subTickOrder = 100
- 位置 B 后被触发 → subTickOrder = 101
那么 A 会先执行,执行后的状态变化可能会影响其他方块,而 B 看到的是变化后的世界。这就是"位置依赖"的根源。
OrderedTick:完整的排序键#
一个有序 tick 事件由四个维度完全定义:
public record OrderedTick<T>(
T type, // 方块类型
BlockPos pos, // 位置
long triggerTick, // 触发时刻(第几刻)
TickPriority priority, // 优先级
long subTickOrder // 同优先级内的调度顺序
) {}比较器排序规则:
TRIGGER_TICK_COMPARATOR = (first, second) -> {
int i = Long.compare(first.triggerTick, second.triggerTick);
if (i != 0) return i; // 1. 先比触发刻
i = first.priority.compareTo(second.priority);
return i != 0 ? i // 2. 再比优先级
: Long.compare(first.subTickOrder, // 3. 最后比调度顺序
second.subTickOrder);
};方块事件(SyncedBlockEvent)#
这是一个独立的执行通道,在 blockTicks 之后运行。典型用途:
- 活塞伸缩动画(调用
PistonBlock.onSyncedBlockEvent) - 音符盒发声
- 漏斗 Minecart 交互
- 箱子打开/关闭动画
活塞的实际推动计算发生在方块事件阶段,这解释了为什么:
- 活塞不会影响同 tick 中 blockTicks 阶段的红石更新
- 但活塞实体推动会在下一 tick 的 entity.tick() 中生效
实用知识#
问:能不能强制让某个方块比另一个先 tick?#
可以通过位置控制间接实现:相邻更新的传播顺序会影响调度顺序。但这种方式极度脆弱,跨世界种子、跨版本都可能失效。
问:1 gt 的红石延迟到底对应什么?#
1 gt 延迟 = 在 blockTicks 阶段安排一个 NORMAL 优先级、延迟为 1 tick 的 OrderedTick。中继器的 1 档就是这个。
问:为什么有时候活塞看起来 0-tick?#
因为你观察到的是"输入信号变化"到"活塞开始推动"没有明显延迟,但实际上它们发生在不同阶段:输入在 blockTicks 变化,活塞动画在随后的 processSyncedBlockEvents 中触发,看起来像同一刻,但逻辑上有先后。