
闲的没事干,分享一下对更新相关的研究吧,正好站内也没有详细的一些教程,代码为mojang映射,封面来自GTMC,阅读此专栏需要具备一定的java基础
首先要知道的的是,在1.13及其之后,更新分为了状态更新(pp)和方块更新(nc)站内有很多对这些的讲解,这里就不多说了
在1.19之前的版本,游戏的更新是通过递归来完成的,不过在之后,游戏使用了一个双端队列来处理更新,我将分析下图中的结构来介绍它的逻辑

这里先分析一下更新的逻辑,不管是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.19之后,游戏抛弃使用递归来进行更新的方法,而是使用了一个双端队列来管理更新,这就避免了递归会导致栈溢出的问题

逻辑
如果是首次更新(count为0)
则将传入的update加入stack,随后调用runUpdates处理
如果不是首次更新(count不为0)
则将传入的update加入addedThisLayer,随后返回

逻辑
如果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的原因

此时栈内的任务如图

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

随后从栈顶开始处理任务,每处理完成一个就弹出栈,后续更新到的B也是如此处理
在这些更新全部结束后,源的nc更新的更新链就结束了,栈和count清空
随后回到源的setBlock方法继续处理剩下的pp更新,由于count为零,所以每个更新都是一个独立的更新栈

在我的世界中,绝大多数对方块的修改都是调用该方法,该方法会判断一个传入的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的值被重置了
当地狱门的nc触发了更新跳略,count >= maxChainedNeighborUpdates
所以在处理该更新链的更新栈时,即使栈内更新任务触发了其他的方块也不会再入栈,也就是说游戏在更新跳略之后就只会处理更新栈中的更新,不过由于一阶毗邻范围内的门已经在更新栈中,所以也就破碎了
这里的黑曜石相当于上面说的源,当源的nc触发跳略后,count清零,后续的pp更新是独立的更新栈,这就导致门还是会碎
不过因为nc更新是所有方向打包在一起的,所以我们可以跳略源的nc更新来做到一些看似不可能的东西

注意:只能用仅接受nc的方块
我们先看侦测器计划刻逻辑的源码

解释
如果当前为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下的侦测器链只要侦测器数量够多也可以造成栈溢出(更新为瞬时更新)
