03 特殊的更新行为
本部分将介绍一些方块的特殊的更新行为
1 红石粉的更新行为
1.1 二阶毗邻更新的概念
在#01中,我们引入了 NC 更新的 “更新源” 的概念,最普通常见的方块更新:例如放置、破坏方块产生的更新,都是以变化方块的位置为更新源,向六个方向发出 NC 更新的,这种就可以称作 “一阶毗邻更新”。
而红石粉则不然。举例来说,在红石粉的能量等级发生变化时,会发出 “二阶毗邻更新”,指的是:红石粉以自身位置和自身毗邻的六个方块分别为更新源,分别向六个方向发出 NC 更新。
“一阶” 与 “二阶” 的命名,是由最远受到 NC 更新的方块距离发出更新方块的曼哈顿距离1得来的。一阶毗邻更新即最远受到 NC 更新的方块距离发出更新方块的曼哈顿距离为 1,二阶毗邻更新即该距离为 2。
1.2 红石粉二阶毗邻更新的更新源顺序(红石粉更新的位置性)
红石粉在发出二阶毗邻更新时,7 个更新源的先后顺序是基于红石粉坐标的哈希信息随机排列的,其顺序见下表。这也就是红石粉更新具有位置性的由来。则 7 个更新源有约 97.067% 的情况可以分成三组:-Y, +Z, +X、O、+Y, -Z, -X。三组内部顺序固定,并以如下顺序发出更新:
注:表中 O 表示源红石粉,-X 等表示更新源相对于源红石粉的方位。
除上表六种主要序列以外,还有少量其他序列,合计约 2.933%;其中单个序列的概率均低于 0.2%。数据实际上取决于 JDK 中集合的实现;如果或 JDK 产生变化,计算得出的概率也将不再适用。
1.2.1 红石粉更新顺序的具体概率分析
红石粉会把自身和六个相邻位置放入一个HashSet,然后遍历这个集合:
也就是说,红石粉二阶毗邻更新的 7 个更新源顺序来自HashSet的迭代顺序,而HashSet顺序又受BlockPos哈希值、表容量和 Java 集合实现影响。由于BlockPos的哈希值与坐标有关,这就表现为红石粉更新具有 “位置性”。
在 1.20.1 中,BlockPos继承的Vec3i.hashCode()为:
按标准 JDK HashSet/HashMap的迭代行为计算,并枚举一个完整的低位周期,则可以得出上述表格中的概率分布。
2 斜侧铁轨的更新行为
2.1 简述
斜放的动力铁轨(PoweredRailBlock)的激活状态(powered=true|false)变化时,它会按如下顺序依次发出两组更新:
- 第一组:依次以上方方块、自身、下方方块为更新核发出 NC 更新。
- 第二组:依次以自身、以下方方块、以上方方块为更新核发出 NC 更新。
2.2 代码走读
2.2.1 调用栈如下
2.2.2 源码简析如下
3 红石粉的更新及其代码解析
-
当红石粉重新计算出的能量等级与当前
POWER不同,并且当前位置仍然是这条红石粉时,它会用Block.NOTIFY_LISTENERS改变自身状态:java1 LINES||// SYNTAX_HIGHLIGHT这里的
flags=2只负责状态同步和 PP 更新,不会通过setBlockState自身发出 NC 更新。随后红石粉把自身与六个相邻位置放入HashSet,并对集合中的每个位置调用world.updateNeighborsAlways(blockPos, this),这就是上文所说的二阶毗邻更新。 -
红石粉还有一个私有的
updateNeighbors(World world, BlockPos pos)辅助方法:若传入位置本身是红石粉,它会先以该位置为更新核发出 NC 更新,再分别以该位置六个相邻方块为更新核发出 NC 更新。这个方法常用于处理红石粉放置、移除后周围粉线连接状态的同步。 -
红石粉放置和移除时还会额外更新偏移位置:
onBlockAdded会先调用能量更新,再更新上下方向,最后调用updateOffsetNeighbors。onStateReplaced在红石粉被替换且不是活塞移动时,会先更新六个相邻方块,再调用能量更新,最后调用updateOffsetNeighbors。updateOffsetNeighbors会检查水平方向的相邻位置;若相邻方块为实体方块,则继续更新其上方,否则继续更新其下方。
-
红石粉还覆写了
prepare。在setBlockState触发 PP 更新前后,prepare会根据红石粉与周围粉线的连接状态,对上方或下方的相邻红石粉调用replaceWithStateForNeighborUpdate。因此红石粉不仅 NC 范围特殊,PP 阶段也有专门的连接修正逻辑。
4 铁轨链的递归检查行为
4.1 简述
铁轨在接受到 NC 更新时会检测自身充能状态。当满足以下两个条件之一时,它将会判断自己被激活powered=true:
- 直接接收到红石信号,即被直接充能
- 由相连的其他铁轨充能,即被间接充能
铁轨检查自身是否被间接充能并不是检测相连铁轨是否被充能,而是递归寻找整条铁轨链上距离自身 8 个铁轨及以内的 (若认为直接相连的两个铁轨的距离为 1 个铁轨)、被直接充能的铁轨,过程如下:
- 初始时距离变量为
0。若距离=8,则终止检查并返回False,即不被间接充能。 - 铁轨具有两个方向,当开始检查时,铁轨先检查哪一个方向由一个布尔值控制。当布尔值为
True时,且当铁轨形状为南北/东西朝向时,它检查南/西侧铁轨;当铁轨形状是上下斜向shape=ascending_*时,它检查南侧上方 / 西侧上方的铁轨。当布尔值为False时反之。此布尔值在开始检查时先为True,后为False。且由于逻辑运算短路2:若第一次布尔值为True时检查成功,则不会进行第二次布尔值为False的检查。在递归检查过程中 (见步骤 6) 此布尔值不会改变。 - 移动坐标
(i, j, k)到接下来要检查的铁轨位置。 - 判断当前铁轨能否可能在当前检查方向上与当前方向的下方的一个铁轨相连。若当前铁轨为向东/西/南/北侧向上,且检查方向为东/西/南/北,则认为不能。此步骤是为步骤 8 准备的。例如:当前铁轨为北侧向上,此时在向南侧检查,则此铁轨可能与南侧下方的铁轨相连;当前铁轨为北侧向上,此时在向北侧检查,则此铁轨不可能与南侧下方的铁轨相连。
- 若当前铁轨形状为南侧向上/北侧向上或东侧向上/西侧向上,则认为当前铁轨形状为南北朝向或东西朝向 —— 这一形状更改是为下一步骤 (6.b.) 准备的。
- 检查坐标
(i, j, k):- 检查当前位置是否是一个铁轨,如果不是,则返回
False,结束当前步骤检查。 - 检查当前位置是否与原铁轨相连。此步骤并不检查 “相连”,而是检查 “不相连”:若原铁轨形状为东西朝向,则若当前铁轨形状为南北/北侧向上/南侧向上,则不相连,返回
False,结束步骤 6 检查。若原铁轨为南北朝向,则若当前铁轨形状为东西/东侧向上/西侧向上,则不相连,返回False,结束当前步骤检查。 - 步骤 i.ii. 实际都是 iii.iv. 的前置条件。此步骤检查当前位置的铁轨是否被直接充能,如果是,则返回
True,结束当前步骤检查。 - 跳至步骤 1,距离增加
1,并按照开始检查的布尔值进行下一个铁轨的检查 (递归) 。
- 检查当前位置是否是一个铁轨,如果不是,则返回
- 若步骤 6 返回
True,则终止检查并返回True。 - (见步骤 4 判断) 若当前铁轨可能在当前检查方向上与当前方向的下方的一个铁轨相连,那么检查坐标
(i, j, k)下方的方块,即跳至步骤 6,但检查的坐标为(i, j-1, k)。在步骤 4 的例子中,若步骤 3 返回False,那么此时将检查原铁轨南侧下方的铁轨。
4.2 代码走读
一个平铁轨可能与看似不相连的斜铁轨判断为相连。同样的结构若红石块直接充能平铁轨则斜铁轨不会被充能
5 亮起侦测器到位无毗邻 NC 更新
当活塞推动一个亮起的侦测器到位、且到位位置无侦测器计划刻时,它不会对毗邻方块(除输出端指向的方块)发出常规意义上由活塞推动触发的 NC 更新(指通常情况下活塞推动一般方块到位后,会在方块到位位置发出由flags=3的setBlockState触发的 NC 更新),原理如下:
- 到位分两种情况:
- 一般到位:调用
PistonBlockEntity.tick,其中调用setBlockState,flags=67 - 强制到位:调用
PistonBlockEntity.finish,其中调用setBlockState,flags=3
- 一般到位:调用
- 在
setBlockState中调用worldChunk.setBlockState,然后调用onBlockAdded - 在
ObserverBlock.onBlockAdded中:- 判断当前位置的新旧方块是否为同一种方块。由于旧方块是
moving_piston(b36),新方块是observer,state.isOf(oldState.getBlock())为false,因此不会提前return,继续下一步 - 判断自身是否激活且当前位置无侦测器计划刻,由于自身亮起
powered=true且当前位置无侦测器计划刻,判断true,下一步 setBlockState,flags=18,将自身激活状态变为powered=true -> powered=false,且发出 PP 更新- 发出常规的侦测器的 NC 更新,即对输出端指向的方块及指向方块的毗邻方块发出 NC 更新
- 判断当前位置的新旧方块是否为同一种方块。由于旧方块是
- 由于在步骤 3.iii. 中,侦测器
powered属性改变,此时继续步骤 1 的setBlockState,但由于blockState2 == state即前后状态是否一致为false,setBlockState不再发出常规的 NC 更新,而是直接结束
调用栈如下:





