高版本更新逻辑和一些常见问题
小名er_
编辑于 2026年05月24日 11:39
收录于文集
共2篇
我的世界生电

闲的没事干,分享一下对更新相关的研究吧,正好站内也没有详细的一些教程,代码为mojang映射,封面来自GTMC,阅读此专栏需要具备一定的java基础

首先要知道的的是,在1.13及其之后,更新分为了状态更新(pp)和方块更新(nc)站内有很多对这些的讲解,这里就不多说了

1.更新栈

在1.19之前的版本,游戏的更新是通过递归来完成的,不过在之后,游戏使用了一个双端队列来处理更新,我将分析下图中的结构来介绍它的逻辑

玩家打破红石块,左侧音符盒为A,右侧为B,红石块为源,上方为北方

这里先分析一下更新的逻辑,不管是1.19之前还是之后,宏观上的逻辑都是不变的

玩家破坏源后,按照西东下上北南的顺序发出nc更新,若被更新到的方块可以响应nc更新,则执行该方块的neighborChanged方法

A被更新到后,执行它自己的neighborChanged方法,A会先发出nc更新,更新到B后,B再发出nc更新

B的nc更新完成后,会再按照西东北南下上的顺序发出pp更新,当被pp更新到的方块可以响应pp,则执行该方块的updateShape方法

B的pp更新完成后,会再计算A剩余方向的nc和它的pp,最后是源,如果中间又更新到了其他的分支则继续上面的逻辑

这是最终逻辑为

源nc(西)

源nc(东)

源nc(下)

源nc(上)

源nc(北)

--A nc(西)

--A nc(东)

----B nc(西)

----B nc(东)

----B nc(下)

----B nc(上)

----B nc(北)

----B nc(南)

----B pp(西)

----B pp(东)

----B pp(北)

----B pp(南)

----B pp(下)

----B pp(上)

--A nc(下)

--A nc(上)

--A nc(北)

--A nc(南)

--A pp(西)

--A pp(东)

--A pp(北)

--A pp(南)

--A pp(下)

--A pp(上)

源nc(南)

源pp(西)

源pp(东)

源pp(北)

源pp(南)

源pp(下)

源pp(上)

1.1手工栈

在1.19之后,游戏抛弃使用递归来进行更新的方法,而是使用了一个双端队列来管理更新,这就避免了递归会导致栈溢出的问题

addAndRun方法

逻辑

如果是首次更新(count为0)

则将传入的update加入stack,随后调用runUpdates处理

如果不是首次更新(count不为0)

则将传入的update加入addedThisLayer,随后返回


runUpdate方法

逻辑

如果stack或addedThisLayer不为空

则将addedThisLayer中的任务逆序压入stack,随后清空addedThisLayer

之后调用runNext处理这些任务,顺序是从栈顶到栈底


当源被破坏时,游戏会调用CollectingNeighborUpdater.updateNeighborsAtExceptFromFacing,将所有方向的nc更新做为一个MultiNeighborUpdate对象传入addAndRun方法

由于是首次更新,count为0,加入stack,调用runUpdates处理,在MultiNeighborUpdate内部通过一个for循环遍历所有的方向并发出nc更新

在遍历到北方时,更新到了A,A调用setBlock方法,先进行nc更新的逻辑,由于count已经不为0,则加入addedThisLayer,随后回到setBlock方法进行pp更新的相关逻辑

与nc更新不同的是,pp更新并不会把所有方向的更新作为一个对象传入addAndRun方法,而是通过一个for循环遍历每个方向,并把该方向的pp更新作为一个ShapeUpdate对象传入,这就导致了一个正常方块的更新中,pp更新会占用6的深度,而nc只占用1的原因

pp更新的遍历

此时栈内的任务如图

w:西,e:东,s:南,n:北,u:上,d:下

之后回到外层的处理上,runNext返回true,不运行if语句内的代码,随后游戏发现addedThisLayer已经不为空,则跳出内层循环,再次将addedThisLayer内的任务逆序压入stack,之后清空addedThisLayer

此时栈内任务如图

w:西,e:东,s:南,n:北,u:上,d:下

随后从栈顶开始处理任务,每处理完成一个就弹出栈,后续更新到的B也是如此处理

在这些更新全部结束后,源的nc更新的更新链就结束了,栈和count清空

随后回到源的setBlock方法继续处理剩下的pp更新,由于count为零,所以每个更新都是一个独立的更新栈

1.2setBlock方法

setBlock方法

在我的世界中,绝大多数对方块的修改都是调用该方法,该方法会判断一个传入的updateFlags来判断需要进行什么更新

如果要发出nc和pp,updateFlags通常会传入3

如果只发出pp,通常会传入2

注意到,该方法还有一个传入的值——updateLimit,这个值初始通常为512

在计算pp更新时,会传入updateLimit-1的值,且如果updateLimit为0,则不会进行pp更新,但仍然会进行nc更新

1.19以下的版本,在仅靠pp更新传递的更新链中,每更新到一个方块,updateLimit都会-1

当该值为0时,会返回上一层继续进行剩下的pp更新,但上一层的updateLimit并不是0,当更新到其他的方块时,会继续将它自己接收到的updateLimit-1传入下一层

在1.19之后的版本,游戏使用了手工栈,而非递归,ojang为了保证这部分的逻辑,每个ShapeUpdate对象都会传入当前的updateLimit,并放入栈中等待处理

然而,如果更新链中出现了需要只响应nc更新的方块(如一排地狱门中间有一个bud音符盒)

因为会先处理nc更新,方块的状态需要发生变化,会调用setBlock方法,传递给下一个地狱门的updateLimit会是默认的512

这就导致updateLimit的值被重置了

2.一些常见的问题

2.1跳略器相关

2.1.1跳略器切门为何周边的门也会破碎

当地狱门的nc触发了更新跳略,count >= maxChainedNeighborUpdates

所以在处理该更新链的更新栈时,即使栈内更新任务触发了其他的方块也不会再入栈,也就是说游戏在更新跳略之后就只会处理更新栈中的更新,不过由于一阶毗邻范围内的门已经在更新栈中,所以也就破碎了

2.1.2为什么跳略器链子不能接黑曜石

这里的黑曜石相当于上面说的源,当源的nc触发跳略后,count清零,后续的pp更新是独立的更新栈,这就导致门还是会碎

不过因为nc更新是所有方向打包在一起的,所以我们可以跳略源的nc更新来做到一些看似不可能的东西

通过跳略nc更新来做到更改铁轨状态而门不碎

注意:只能用仅接受nc的方块


2.2瞬时计划刻下为什么侦测器不会激活方块

我们先看侦测器计划刻逻辑的源码

解释

如果当前为false,则状态更改为true,发出pp更新,添加计划刻,发出nc更新

如果当前为true,则状态更改为false,发出pp更新,发出nc更新


当开启瞬时计划刻后,level.scheduleTick相当于调用tick方法它自己

当玩家放下源后,pp更新更新到了侦测器,将POWER改为true,并将它亮起要发出的pp更新加入addedThisLayer,之后添加计划刻,将POWER改为false,将熄灭要发出的pp更新加入addedThisLayer,随后将熄灭和亮起的nc更新依次加入addedThisLayer

这些操作完成后,进入外层依次处理:

亮起的pp更新,熄灭的pp更新,熄灭和亮起的两次nc更新

不过在进入外层之前,POWER就已经变为了false,被nc更新的原件进行检查,发现侦测器是熄灭的,所以也就不会被激活了

顺便说一下,itt下的侦测器链只要侦测器数量够多也可以造成栈溢出(更新为瞬时更新)

瞬时计划刻下造成更新抑制的侦测器链