游戏刻与微时序#

本文档基于 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) 指的是一个游戏刻内部的事件执行顺序。在社区中,这个词通常特指:

  1. 方块 tick 的优先级差异:红石火把(HIGH)比中继器(NORMAL)先 tick
  2. 同优先级的执行顺序:由 subTickOrder 计数器决定(FIFO)
  3. 方块事件 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 具有相同的 triggerTickpriority 时,由 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 中触发,看起来像同一刻,但逻辑上有先后。