本部分是更新理论的入门。
基础部分
进阶部分(源码分析)
在 Minecraft 中,方块与方块通过互相 “通知” 来建立相互的联系。
有一天,你突发奇想 —— 在地上放置了一个音符盒。然后,你又紧邻着音符盒放置了一个红石块,音符盒发出了悦耳的声音。那么,音符盒是怎么知道自己应该恰好在这个时候发出声音的吗?难道音符盒会一直给玩家发消息:我要发出声音吗?我要发出声音吗?我要发出声音吗…… 这显然是不合理的。实际上,音符盒能够发出声音,是因为红石块在被放置时,“通知” 了音符盒:嘿,这里有点变化,你注意一下。
这种通知行为就称为广义上的更新。
需要注意的是,红石块并不会通知 “我是一个红石块” 或者 “这里从空气变成了红石块”,而是非常简单地通知:这里有点变化。也就是说,在 Minecraft 中,更新行为并不包含行为的具体信息。
在 Minecraft 中,更新有不止一种类型。
让我们继续上文中的例子。这天,你又灵光一现 —— 在地上先放置了一个红石块,然后间隔一个方块放置了一个栅栏门,在红石块和栅栏门的中间放置了音符盒。你突然发现,音符盒怎么没有发出声音呢?于是,你右键栅栏门,把栅栏门打开了,但音符盒还是没有发出声音。按理来说,上文中的 “放置红石块” 和 “栅栏门打开” 都是某种变化,它们都应该 “通知” 音符盒,为什么音符盒没有如愿地发出声音呢?带着疑惑,你直接把栅栏门破坏掉了,此时音符盒又发出声音了。
—— 这是为什么呢?
你于是猜想:“放置红石块” 和 “栅栏门打开” 的 “通知” 也许有什么差别。
实际上,这两个通知的确是不同的。现如今我们分别称之为NC 更新和PP 更新。
此时你又思考:为什么音符盒被放置的时候,不会立即发出声音呢?它难道在 “出生” 的时候不会看一看 “自己要不要说两句话” 吗?—— 没错,音符盒比较懒,它确实不会 “检查自己要不要发出声音”。
这种在被放置等情况下检查自身的行为,被称为自检。自检可以被视作一种特殊的更新。而音符盒不会进行自检。
你在先前的学习中又了解到:比较器是一种能够检测容器中物品数量的红石元件,根据容器中物品数量的多少输出 0-15 的不同强度高低的红石信号。你想到:当容器中物品数量发生变化时,容器并没有必要发出NC 更新或PP 更新,毕竟只是内部物品多少改变了,并不会对周围方块产生什么太大影响。但你知道比较器却能够及时地改变自身输出的红石信号强度,说明比较器还是能够收到这种变化的 “通知”。这是因为容器在容器中物品数量发生变化时,也会发出一种特殊的通知。
这样的通知被称为比较器更新。
方块在被放置、破坏、或发生能够显著影响周围方块的变化时,会发出NC 更新。例如:
官方提供的反混淆中 NC 更新为
neighborChanged,与 mcp 反混淆相同。yarn 反混淆中则称为updateNeighbors
但一些变化不会发出NC 更新,最为常见的有:
活塞的具体行为不在本篇讨论范围内。
一个发出 NC 更新的 “方块” 称为更新核1,大部分方块发出更新时,更新核是它本身。
更新核发出 NC 更新时,按照西东下上北南的顺序,依次对更新核西侧紧邻、东侧紧邻…… 的方块发出 NC 更新。这样的更新就是通常意义上的更新。
部分方块在发出 NC 更新时不只有一个更新核,它们具有特殊的更新核与范围:
以上是常见且较为典型的例子。更多的特殊情况可以参见Wiki - 方块更新
NC 更新最典型的性质是能够使BUD 装置响应。
一个方块当前的状态与它本应该的状态不同时,这个方块就可以称作是一个BUD 装置,即方块更新检测器(Block Update Detector)。这种方块状态也可以称作是BUD 态。
最典型的 BUD 装置是一个被充能(受到红石信号)但未被激活(未伸出)的活塞。活塞具有特殊的充能范围,当活塞本身和活塞上方的一个方块(可以为空气)被充能时,活塞就可以被认为被充能。通过充能活塞上方的空气充能活塞,这种充能方式就称为QC 充能。这样被充能的活塞在受到 NC 更新后伸出,就称作QC 激活。
QC (quasi connectivity) 称为半连接性,活塞、粘性活塞、投掷器、发射器都具有这种性质。
想象一个活塞的斜上方放置了一个红石块,此时这个活塞被 QC 充能。而红石块的放置只会更新它毗邻的六个方块,无法更新到活塞,所以没有任何方块通知活塞 “这里有一个红石信号”。此时这个活塞本应伸出,但却因为没有受到 NC 更新而没有伸出。也就是当前的状态与它本应该的状态不同,即此时这个活塞处于BUD 态,该活塞就构成了一个BUD 装置。
此时,如果在活塞边上放置一个方块,方块放置就会发出 NC 更新。活塞受到 NC 更新,解除 BUD 态。
一个最简单常用的可以自行复位的 BUD 装置如下:
图中的红石块QC 充能下方粘性活塞,当粘性活塞受到 NC 更新后伸出,红石块不再充能,而后粘性活塞收回,红石块再次 QC 充能粘性活塞。3
几乎所有的变化都会发出PP 更新。除了:
等极其特殊的情况;但还是有一个特例:
官方提供的反混淆中 PP 更新为
updateShape,即更新形状(例如,各种墙的形状、活板门的开关、红石元件的激活状体改变,都可以称作这里的 “形状”),该反混淆名更有益于理解 PP 更新。PP 更新的命名来源于 mcp 反混淆中是postPlacement方法。yarn 反混淆中则称为getStateForNeighborUpdate
和 NC 更新不同,PP 更新的顺序为西东北南下上。
在上文中提到的不会发出NC 更新的行为,都是较为典型的发出PP 更新但不发出NC 更新的行为。同样,上文中的BUD 装置无法用来检测PP 更新。
例如这里的栅栏门状态发生变化,发出 pp 更新,但并不会被活塞 BUD 装置检测到
需要注意的是,除了放置与破坏方块外,大部分红石元件的 NC 更新范围和 PP 更新范围是不一样的。例如中继器的 NC 更新范围是输出端及输出端的毗邻(除中继器自身),而其 PP 更新范围是自身的毗邻。
PP 更新可以理解为自身发生变化的更新,而 NC 更新可以理解为通知可能需要改变状态的方块的更新。
同样以中继器为例,中继器的亮灭属于自身发生了变化,通过 PP 更新通知自身毗邻的方块。而中继器亮起会充能它输出端的方块,如果输出端的方块能够传递红石信号,这个方块还会再向它的毗邻传递红石信号;所以说中继器的亮起有可能会影响它输出端的方块以及输出端毗邻的方块,因此中继器通过 NC 更新通知可能受到影响的方块检查是否要改变状态。
又例如,当投掷器被激活,投掷物品时,投掷物品这一行为并不会对周围方块造成影响4,因此投掷器激活并不发出 NC 更新。但是投掷器的确改变了激活状态,这种状态改变使得投掷器发出了 PP 更新。
这样,PP 更新与 NC 更新的差异以及它们更新范围的不同就易于理解了。
既然上文中的 BUD 装置无法检测 PP 更新,那么什么装置能够检测(响应)PP 更新呢?
答案是 —— 侦测器。侦测器长得像脸的一面为检测 PP 更新的一面,有红色小点的一面为输出红石信号的一面。
侦测器响应、且仅响应 PP 更新。5但由于大多数情况 NC 更新总是伴随 PP 更新的,所以这里的 “仅” 可能体现的并不明显。不过我们还是可以通过一些方法来体现:当侦测器直接面向中继器时,中继器亮灭会激活侦测器;但如果侦测器面向中继器输出端指向的方块(空气),中继器的亮灭则不会激活侦测器。这里也很好理解:当中继器指向一个空气时,空气不会有任何变化,因此侦测器自然也不会响应了。
通过 BUD 装置与侦测器的配合使用,我们可以很方便的在不翻阅源码的情况下测出各个红石元件的 NC 更新范围和 PP 更新范围。
顾名思义,比较器更新是专用于更新比较器的更新。
以下行为会发出比较器更新:
比较器既可以直接检测容器容量,也可以间隔一个可传递红石信号的方块检测容器容量。因此比较器更新的范围为容器水平方向的毗邻范围内的比较器,或二阶毗邻范围内间隔一个可传递红石信号的方块的比较器。比较器更新不会对比较器以外的方块造成更新。
对一般的容器:
每个物品槽位填充比例的平均值*14向下取整,如果容器不是空的,那么输出信号强度再+1。即:当容器为空,输出 0。当容器不为空,输出容器的平均填充比例到 1~15 的映射并向下取整。对讲台:
当前页码/总页码*14向下取整再+1。一个比较器当前的状态与它本应该的状态不同时,这个比较器就构成了一个比较器更新检测器(CUD,Comparator Update Detector)。一个最简单的例子是用比较器间隔一个方块检测一个有填充的堆肥桶,然后用活塞将堆肥桶推走。此时比较器依然有输出信号。
由于其他可复位的 CUD 结构与涉及原理较为复杂,在此处仅作展示,不作展开。
图为基于红石粉粉转向的 CUD。
可以看到 BUD 并未启动,而 CUD 检测到了漏斗发出的比较器更新。
红石元件在被放置时会进行自检,即检查自身是否应当改变状态。例如放置一个红石块,然后放置一个中继器,并且这个中继器的输入端紧邻着红石块。此时中继器没有受到更新,但还是亮起了,这就是因为中继器在被放置时进行了自检,发现自己应该亮起,于是亮起。
活塞自身到位也可以视作一种 “被放置”,因此活塞自身在伸出后到位和拉回后到位也都会进行一次自检。
setBlockState函数setBlockState通过一个bitflags即FLAG控制更新行为,在World.java的World类中:
在源码中,NC 更新主要通过两种方式调用:
updateNeighbor及其衍生flag中bit0 = 1的setBlockStateupdateNeighbor及其衍生,即updateNeighbor updateNeighborsExcept updateNeighborsAlways。
其中updateNeighbor是最根本的方法,传入pos, sourceBlock, sourcePos三个参数,分别为要更新的方块坐标pos,发出更新的方块类型sourceBlock和发出更新的方块坐标sourcePos。
updateNeighborsAlways和updateNeighborsExcept通过调用NeighborUpdater.updateNeighbors进行更新,在NeighborUpdater.java的NeighborUpdater实例中:
因此 NC 更新顺序为西东下上北南。
其中在Block.java中定义:
对于以上的调用方法,只有调用updateNeighborsAlways和updateNeighborsExcept的sourcePOS和setBlockState的pos可以称作 “更新核”,即以一个方块为中心对毗邻六个方块(或除去某个方向的方块)进行更新的 “核”。而updateNeighbor是简单直接的 “更新”。
仅 PP 更新时,FLAG通常为NOTIFY_LISTENERS;同时发出 PP 更新和 NC 更新时,FLAG通常为NOTIFY_ALL。部分覆写了onBlockAdded方法的方块具有不同的调用方式,见下文
在各个方块中,NC 更新由方块的neighborUpdate函数响应。
在源码中,PP 更新通过setBlockState调用。当FLAG的bit4 = 0时会产生 PP 更新。
state.updateNeighbors定义在AbstractBlock.java的AbstractBlockState类中:
其中在AbstractBlock中有:
因此 PP 更新为西东北南下上。
其中world.replaceWithStateForNeighborUpdate:
调用了NeighborUpdater中的replaceWithStateForNeighborUpdate方法:
最后调用了各个方块blockState的getStateForNeighborUpdate方法。也就是方块响应 PP 更新的方法。
而对于大多数的红石元件,它们在调用setBlockState时通常传入FLAG=2,NC 更新实际是由setBlockState中的worldChunk.setBlockState方法调用覆写@Override后的onBlockAdded方法发出 NC 更新:
在worldChunk.setBlockState中:
而 PP 更新受FLAG控制正常由setBlockState调用。
以红石中继器为例,在红石中继器继承的AbstractRedstoneGateBlock.java的AbstractRedstoneGateBlock中:
scheduledTick为中继器执行计划刻时调用的方法。在中继器执行计划刻时,它会调用setBlockState函数,而后由setBlockState调用worldChunk.setBlockState,在worldChunk.setBlockState调用了@Override的onBlockAdded,对中继器输出端指向的方块及指向方块的毗邻(除中继器自身)产生了 NC 更新。接着由setBlockState调用state.updateNeighbors产生了 PP 更新。
综上,对于大部分通过setBlockState发出更新的方块,都满足先 NC 更新、后 PP 更新的顺序。这一点极其重要,它影响了在后续更新中 NC 更新与 PP 更新的顺序问题。本篇不对 “连续的方块更新” 作展开。
侦测器是一个特例。它并没有调用onBlockAdded方法中的 NC 更新,而是直接在scheduledTick中先调用setBlockState发出 PP 更新,再调用updateNeighbors发出 NC 更新。因此,侦测器在亮起和熄灭是遵循先 PP 后 NC 的顺序。
在侦测器的onBlockAdded中:
其中第一个if中的state.isOf(oldState.getBlock())用于判断当前位置的方块在被改变状态之前是否还是当前的方块。例如,如果活塞推动了一个侦测器,那么侦测器到位的位置在改变前就是air,改变后就是observer,此时该函数返回true。而一个普通的侦测器在调用这一函数时总是返回false。
所以,侦测器的onBlockAdded方法在通常情况下都不会执行后续的内容,也就不会由onBlockAdded发出 NC 更新,而是如上文中所说,“直接在scheduledTick中先调用setBlockState发出 PP 更新,再调用updateNeighbors发出 NC 更新。”
方块在被放置时会被调用onPlaced函数,红石元件在该函数中完成自检。例如红石中继器:
活塞自检的源码不在本篇讨论范围内。
ServerWorld.setBlockState 方法中,一个整数参数被传入:flag
这个 flag 实际上被视为一个 9 位二进制码,这 9 位中,每一位都控制了不同的行为
Block.java 中定义了多个更新行为,他们分别是
红石粉具有更加复杂的更新行为,本篇中不作展开。可能后续会追加红石粉专题 DLC ↩
此处对 1.3.2 中的 BUD 装置涉及活塞原理部分作简单展开,读者仅作了解即可。粘性活塞在伸出时没有受到任何 NC 更新,因此不会中途丢下方块。粘性活塞伸出到位后触发自检,发现没有被充能于是执行收回。收回时由于粘性活塞的方块到位顺序,粘性活塞先到位,此时触发自检但红石块还未到位(处于方块实体状态),红石块后到位。因此粘性活塞不会由于自检而再次伸出。 ↩
不考虑投掷的物品被漏斗吸取。漏斗吸取属于漏斗自身的行为,不受更新控制。 ↩
消歧义:此处仅从更新类型的角度考虑,侦测器仅在受到 PP 更新时判断自身是否应当发生变化并执行特定的行为。若认为 “响应” 指广义的 “侦测器发生变化”,那么除侦测器的计划刻行为外,当侦测器被活塞推到位时,它会调用自身的onBlockAdded方法,仅当到位位置无侦测器计划刻,并且侦测器原先的状态为亮起时,侦测器会继续在其中调用setBlockstate方法,最终状态变为熄灭(不存在计划刻),而原先状态为熄灭的侦测器则不产生任何变化。熄灭的侦测器被活塞到位时添加的计划刻来自于 TE 对侦测器的 “更新”postProcessState,实际上最终仍旧调用的是侦测器响应 PP 的函数getStateForNeighborUpdate。 ↩