08 计划刻与区块运行级别
在 MicroTiming:计划刻与计划刻元件 中,我们已经从微时序角度讨论过计划刻(Scheduled Tick)的信息结构、排序和计划刻抑制器。本篇只补一个问题:当计划刻所在的区块没有进入 BLOCK_TICKING(弱加载)时,它会发生什么?
一句话回答:计划刻可以继续存在于区块层计划器中,但世界层计划器不会把这个区块的计划刻取出来执行。它不是 “时间一到就无条件执行”,而是还要经过区块运行级别的许可。
1 基础部分
1.1 计划刻只是一张预约单
计划刻可以理解为 “在某个世界时间,让某个位置的某种方块或流体执行一次预约逻辑”。至于 TickPriority、subTickOrder、triggerTick 和具体元件行为,MicroTiming 篇已经讲过,本篇不再重复。
这里要抓住的是另一层关系:计划刻不是直接挂在整个世界上的孤立对象,而是先按区块归属,放进对应区块的 ChunkTickScheduler(区块层计划器)里;WorldTickScheduler(世界层计划器)每刻再从可以运行的区块层计划器中挑出计划刻执行。
1.2 计划刻如何与区块绑定
每个 WorldChunk 都持有自己的方块和流体 ChunkTickScheduler。当区块真正作为 WorldChunk 加载进世界时,它的区块层计划器会挂到世界层计划器上:
卸载实体和清理区块时,挂载关系会被移除:
这说明 “有一个区块层计划器被世界层计划器认识” 与 “这个区块此刻能执行计划刻” 不是同一件事。前者表示区块数据已经作为 WorldChunk 参与世界管理;后者还要看区块是否达到方块运算需要的运行级别。
在第 3 篇 级别与运行态分层 中,我们已经把 BLOCK_TICKING 定义为弱加载:它允许方块刻、红石运算、随机刻和计划刻运行,但不运行实体。低于这个资格的 FULL 只保证方块数据可访问,不保证主动运算。
1.3 世界层计划器如何判断 “这个区块可以执行”
WorldTickScheduler 不会只看 “队列里有没有到期计划刻”。它还持有一个 tickingFutureReadyPredicate(ticking 就绪判定)。当某个区块的最早计划刻已经到期时,世界层计划器会先问这个判定:这个区块现在是否已经进入 ticking 状态?
如果答案是 true,该区块的 ChunkTickScheduler 会被放入本刻可执行队列;如果答案是 false,它会留在 nextTriggerTickByChunkPos 中,等待之后某一刻重新检查。
因此,未达到 BLOCK_TICKING 的区块不会 “补偿式地立刻执行”。它只是把已经到期的计划刻继续留在队列里。等区块重新进入弱加载或强加载后,世界层计划器才会重新把这些计划刻拿出来,并按照 MicroTiming 篇描述的排序规则执行。
1.4 计划刻如何跨越区块边界
计划刻的位置决定了它属于哪个区块。WorldTickScheduler.scheduleTick() 会用 OrderedTick.pos() 计算区块坐标,再把这个 OrderedTick(计划刻对象)交给对应区块的 ChunkTickScheduler:
所以,当活塞把方块推过区块边界后,后续计划刻的归属要看 “新的计划刻被安排在哪个坐标”。如果移动过程清除了原位置范围内的计划刻,并在新位置重新安排计划刻,那么新计划刻会进入目标区块的区块层计划器;如果只是原区块里已有的计划刻还没执行,它仍然记录着原来的 pos,不会因为方块后来移动而自动改绑到新坐标。
这也是跨区块装置需要额外小心的原因:计划刻本身是 “坐标上的预约”,不是 “跟随方块实体移动的标签”。一旦坐标跨过区块边界,它是否能在当前 gt 执行,就取决于新坐标所在区块是否达到 BLOCK_TICKING。
1.5 计划刻抑制器与区块运行级别
MicroTiming 篇已经讲过计划刻抑制器:它利用世界层计划器从多个区块层计划器取任务时的排序和每刻上限,使其他区块的计划刻被延后。
从本篇角度看,抑制器还有一个前提:抑制器所在区块必须先通过 tickingFutureReadyPredicate。如果抑制器区块没有达到 BLOCK_TICKING,它的区块层计划器不会进入本刻可执行队列,也就无法用高优先级、大量计划刻去挤占其他区块的执行额度。
换言之,计划刻抑制器不是 “离线生效” 的机制。它依赖世界层计划器正常收集该区块的计划刻,而这一步本身就被区块运行级别拦住。
2 进阶部分
2.1 collectTickableChunkTickSchedulers 源码走读
WorldTickScheduler.tick() 的第一阶段是收集本刻可执行的计划刻:
collectTickableTicks() 先调用 collectTickableChunkTickSchedulers(time)。这个方法只处理 “哪个区块层计划器有资格被取出来”,还没有真正执行计划刻:
这段代码可以分成四层过滤:
m <= time:这个区块记录的下一次计划刻时间已经到达当前世界时间。chunkTickScheduler == null:如果区块层计划器已经不存在,移除这条索引。orderedTick == null或orderedTick.triggerTick() > time:如果队首计划刻不存在,或队首计划刻其实还没到时间,就修正或清理索引。this.tickingFutureReadyPredicate.test(l):时间到了、队列也存在之后,最后检查区块是否真的可以 ticking。
关键就在最后一层。只有通过 ticking 就绪判定,区块层计划器才会进入 tickableChunkTickSchedulers。如果没有通过,代码不会 remove(),也不会 add();它会保留在 nextTriggerTickByChunkPos 中,下次世界层计划器收集时继续尝试。
2.2 tickingFutureReadyPredicate 的实现
ServerWorld 创建方块和流体世界层计划器时,把同一个方法引用传给了构造器:
这个方法在 ServerWorld 中先确认区块实体管理侧仍认为它已加载,再交给 ServerChunkManager:
真正的区块运行级别检查在 ServerChunkManager.isTickingFutureReady():
这里有两个门槛:
world.shouldTickBlocksInChunk(pos)会走到加载票系统的shouldTickBlocks(chunkPos),也就是第 3 篇讲过的方块运算资格;chunkHolder.getTickingFuture().getNow(null)必须已经得到Either.left(WorldChunk),表示ChunkHolder的tickingFuture已完成,并且完成结果是可用的WorldChunk,不是Unloaded。
所以,tickingFutureReadyPredicate 本质上就是在问:这个区块是否已经有可用的 ticking Future? 换成更直白的话,它就是检查 ChunkHolder.getTickingFuture().isDone() 这一类条件是否成立,并且完成结果必须是 WorldChunk。在 Yarn 1.20.1+build.10 的实际实现中,它没有直接调用 isDone(),而是用 getNow(null) 取当前结果,再检查结果是否为 Either.left(WorldChunk)。
注意这里不是阻塞等待 Future 完成。getNow(null) 的意思是 “现在完成了吗?没完成就给我 null”。因此计划刻系统不会为了某个区块等待加载完成;它只在每刻收集时顺手检查,没 ready 就跳过。
2.3 ServerWorld.tick 中的执行位置
方块计划刻真正执行的位置在 ServerWorld.tick() 的 tickPending 阶段:
这里的 65536 是本次调用最多取出的计划刻数量。this::tickBlock 才是计划刻对象最终触发方块 scheduledTick 行为的入口;但在执行到它之前,计划刻已经先经过了世界层计划器的收集、区块层计划器筛选,以及 tickingFutureReadyPredicate 的区块运行级别检查。
这解释了一个常见现象:如果某个计划刻在区块离开弱加载时已经到期,它不会在离线期间持续执行,也不会在世界时间到达 triggerTick 时强行运行。等区块重新进入 BLOCK_TICKING,它才会在之后的 ServerWorld.tick() 中被重新收集,并作为已经到期的计划刻执行。
3 小结
- 计划刻按坐标归属到区块层计划器,世界层计划器只负责跨区块收集和执行。
WorldChunk加载进世界时挂载ChunkTickScheduler,卸载清理时移除挂载。BLOCK_TICKING(弱加载)是计划刻执行的最低运行级别;FULL只保证数据可访问,不保证计划刻运行。tickingFutureReadyPredicate是计划刻执行前的区块 ticking 门槛,它检查ChunkHolder.getTickingFuture()当前是否已经给出可用的WorldChunk。- 跨区块移动时,计划刻跟随的是坐标和重新安排的位置,不是自动跟随被移动的方块。
- 计划刻抑制器也必须处在可 ticking 区块中;未 ticking 的区块不能参与世界层计划器的本刻竞争。
4 参考
- MicroTiming:计划刻与计划刻元件
- ChunkMechanics:级别与运行态分层
net.minecraft.world.tick.WorldTickSchedulernet.minecraft.world.tick.ChunkTickSchedulernet.minecraft.server.world.ServerWorldnet.minecraft.server.world.ServerChunkManagernet.minecraft.server.world.ChunkHolder
