世界是如何运行的--MCBE方块更新机制不完整分析

先打广告:

摸完村庄机制,这次填一下方块更新的坑
前排提示:
所有内容来自1.14.21的BDS反编译后的伪源码,但是全文不会直接出现代码,大可放心阅读
部分内容参考了国外玩家earthcomputer和Floan的一篇文档:https://docs.google.com/document/d/17B0ecrI8BSU1VWpfUNQOoco9l6rhkx1U6S4J1ihFRNg/edit
(免责声明)所有内容不保证完全正确,仅供参考
下面是正文:

世界(存档)每隔特定的时间就会更新一次数据,这更时间叫做游戏刻(gametick),也就是常说的gt。反映到游戏上就是你能看到游戏内植物的生长,红石的运作,生物的移动等等。这篇文章会聚焦在方块相关的数据更新上(所以不会包括spawn和despawn行为)。
区块之间的更新
世界的更新是以区块为单位的,当模拟距离为4 的时候每gt 有57 个区块发生了更新(57个是绝大多数情况下),更新的区块围绕在玩家周围,下图是某个gt内的区块更新:

玩家位于中心区块中,数字大小展示了这一gt 区块更新的顺序.很不幸的是这个区块间更新的顺序没有什么很好的规律,mojang的预期行为是每20gt就把这个顺序随机打乱一次,也就是说每过20gt这个区块间的更新顺序会变化一次。
但是根据我的实测,这个变化周期不是恒定的20gt,而是在17-20gt之间反复横跳,但是这还不是最要命的,最要命的是每次更新顺序变化的时候都会有一次不均匀的区块更新,也就是在那个游戏刻某些区块会获得两次更新,而某些区块不会得到更新.
下面是一个这样的例子:

区块内的更新
区块内的更新的产生往大了讲主要有两个类别,一种触发性的(比如玩家在某一时刻破坏一个方块而更新它周围的六个方块),一种周期性的(比如在特定条件下每gt 都会尝试让水结冰)。
触发性的更新
触发性更新目前唯一已知的来源是周围方块的更新导致当前方块进行的更新。这时候会(检测玩家操作和区块更新行为的次序未知,有兴趣的可以合理猜测)调用该方块的neighborChanged 函数(所以简称NC更新?)。比如你挖掉了一个插了火把的木板的时候,木板周围的六个方块会立即调用它们的neighborChanged函数,而附着于它的火把方块也会因为调用了neighborChanged 函数而立即做掉落检查(在其NC函数内部调用了_checkDoPop() ).
根据测试,以下是已知的能产生NC更新的事件
方块的放置和破坏
拉杆的拉动
铁轨的激活
等等
而调用周围6个方块的更新的顺序为:
-x 西
+x 东
+z 南
-z 北
+y 上
-y 下
知道了更新源头就要知道接收到更新后会做什么。下面是常见的响应NC更新的方块以及可能发生的事件:

可以注意到有些方块在受到neighborChanged更新的时候有可能把实际更新行为加入到队列,这里的队列指的是一个缓冲区,上面的方块受到NC更新后并不会马上更新自己的状态,而是把相关信息放到队列中,稍后再更新。
更新队列
更新队列(BlockTickingQueue)是一个优先队列,也就是一个更新项( BlockTick )的列表,会暂存还没有实际更新的更新项目,这样的队列每个区块都有一个。
更新项
每个更新项有下面几个字段:
该更新的坐标(x,y,z)
(游戏内)时间戳
方块状态
方块名字
其它状态信息(依据不同方块的不同而不同)
更新项的数据为后面的实际更新行为提供了数据
更新队列的消耗
随着游戏的进行,队列里面的更新项目会越来越多(因为更新一直在产生),但是这个队列里面的更新项也在不断地被消耗,
下面是消耗的过程:
获取更新队列的长度
从队列中获取前100 个方块更新项(不足100就全部获取)然后放置到另外一个缓冲队列中(目的未知)
按照特定顺序依次取出更新队列中的更新项目然后执行其对应的tick() 函数。顺序是这样的:每次优先更新时间戳小(也就是更新项里面的游戏内时间)的更新项,如果两个更新的时间相同,先后顺序未知。(当然不排除针对某些更新的特意的随机行为)
这个过程每gt每个区块会执行两次
当退出游戏的时候每个区块的更新队列会被完整地写入到存档文件(你可以用mcctool[http://mcctoolchest.com/]在存档的区块数据文件内看到这个更新队列的信息,包括所有更新项的具体数据),加载世界的时候这些数据也会被完整读取出来.
不难发现,当更新的速度大于消耗的速度的时候,更新队列越来越长越来越长越来越长的情况,轻则出现更新延迟,当然这个特性已经被云龙大叔发现了:

重则导致崩档。
更新顺序
bb这么久终于到达更新顺序问题了,以下描述的是每个区块一个gt 内依次发生的事情(忽略Spawn 和despawn ):
随机刻更新
更新队列更新
更新队列更新(对应上面说的两次)
方块实体更新
红石更新
上面说了更新队列,下面介绍随机刻更新(tickBlocks) ,方块实体更新(tickBlockEntities)和红石
更新( tickRedstoneBlocks )
随机刻更新
这个事件里面的更新都是基于概率的,因此才有"随机"二字,其内部会依次发生如下事情:
尝试改变天气
如果在下雨或者打雷就尝试生成闪电来劈实体,然后尝试生成骷髅马陷阱
进行一次结冰尝试
进行一次给地表铺上雪片的尝试
根据当前随机刻计算更新次数,选择一些方块进行随机刻更新(调用特定方块的tick() 函数),当然选择的方块要合适,更新的次数和如何选择暂时未知,这里一般都是方块的自然更新行为.
方块实体更新
每个区块都维护了一个方块实体( BlockActor )列表,每gt 这个列表都会被有意地完全随机地打乱一次,打乱后遍历这个列表,对列表内的每个方块实体执行tick() 函数,括号内是猜测的行为(不能保证100%准确,仅供参考)

其中玩家最为熟悉的大概就是活塞的随机推出顺序了吧,这也是一些活塞互推随机数生成器的原理。
红石更新
红石组件以及继承树
下面这个继承树主要来自于国外玩家earthcomputer和Floan的一篇文档,链接见开头:
各种红石组件有一个相同的父类(理解成都是同一个爸生出来的,有不同点也有相同点),下面是继承树(理解成父子关系),括号内是翻译,中文仅供参考
Component(组件)
Consumers(消费者)
Piston Consumers(活塞消费者)
Producers(生产者)
Capacitors (= producer with a direction)(特定方向的生产者)
Repeater Capacitors(中继器电容器)
Comparator Capacitors(比较器电容器)
Pulse Capacitors (observers?)(脉冲电容器(观察者?))
Redstone Torch Capacitors(红石火把电容器?)
Transporters(运输者)
Rail Transporters(铁轨运输者)
Powered Blocks(充能方块)
这里“生产和消费”的猜测是红石信号,生产者产生红石信号,消费者会感应红石信号(仅限猜测),红石火把闪一段时间后会熄灭就是这个Redstone Torch Capacitors的锅。
更新行为
每个维度有个电路系统( Circuit System ),而这个系统会维护一个红石组件(Component)的哈希表:每当一个红石组件被放置的时候这个列表就会记录下这个原件的信息以及位置,当然原件也会在合适的时候被移除(比如被玩家破坏)。
每个区块都会获取这个存着所有原件的哈希表,然后对位于该区块内的红石原件执行一次它们对应的onRedstoneUpdate 函数。几乎所有方块都有onRedstoneUpdate 函数,只不过绝大部分的方块这个函数什么都不做的,下面这些特定方块才会有具体的事件(更新行为不保证100%准确):

总结与展望
在一定的粒度上更新的次序是随机的或者未知的,在一定的粒度上更新次序又是固定的
机制应该回归到游戏,空谈是万万不行的,上面的内容部分受到了实践的检验,但是部分没有被玩家检验过,因此如果你发现这些理论的某些部分和你的实际游戏行为不一致欢迎抓虫
毕竟不是每一行代码都能看懂,因此我无法得到完整详细的逻辑,也就不可能做到完全客观,我当然可能会潜意识地把代码行为往我希望的方向描述,也可能遗漏了某些关键部分
希望有更多的玩家能在自己擅长的领域对MCBE社区发展做贡献