深度剖析Minecraft #3.4 计划刻元件

3.4 计划刻元件

阅前必读:

深度剖析Minecraft #2 方块更新

深度剖析Minecraft #3 计划刻

3.4.1 红石二极管

名称 信息
net.minecraft.block.BlockRedstoneDiode
基类 net.minecraft.block.BlockHorizontal
延迟 非定值
优先级 非定值
位置需求 其下方依附着的方块上表面完整
受方块更新时 判断掉落,更新信号
受状态更新时 不响应

这是一个抽象类,是红石中继器以及红石比较器的基类,也就是说中继器跟比较器的本质是一个二极管,有许多的机制是完全相同的

3.4.1.1 方块状态

作为一个可水平旋转的方块,二极管拥有一个 facing 标签表示其水平指向。除此之外二极管还具有以下标签:

标签名 取值 用途
powered false, true 其当前的亮起状态。我们称该标签为假时二极管熄灭,该标签为真时二极管亮起

3.4.1.2 响应方块更新

当二极管受到方块更新时,它会首先检测其所处位置是否满足位置需求,若不满足则掉落为物品,并先在所在位置发出方块更新,再在其毗邻 6 方块的位置发出方块更新(顺序:-y, +y, -z, +z, -x, +x [1]);若合法,则开始检测信号变化 [2]。具体的检测信号变化的方法见 红石中继器 以及 红石比较器

3.4.1.3 发出更新

当二极管的状态发生变化的时候,它会先给其指向方块一个方块更新(下图黄色玻璃),再于其指向方块的位置处(下图黄色玻璃)发出一个除其指向方向反方向的方块更新。图中所有的染色玻璃也即是所有的受到方块更新的方块的位置

二极管更新范围示意

具体的方法为 net.minecraft.block.BlockRedstoneDiode#notifyNeighbors

这一部分的调用之处是在 onBlockAdded 以及 onReplaced 中,因此只要涉及到二极管的方块状态的变化,均可以让这个二极管发出上述更新

3.4.2 红石中继器

名称 信息
net.minecraft.block.BlockRedstoneRepeater
基类 net.minecraft.block.BlockRedstoneDiode
延迟 非定值
优先级 非定值
位置需求 红石二极管
受方块更新时 红石二极管
受状态更新时 更新自身被锁的状态

3.4.2.1 方块状态

红石中继器(简称中继器)在红石二极管的基础上,新增了两个方块标签:

标签名 取值 用途
delay 1, 2, 3, 4 储存中继器的延迟。计划刻事件的延迟将为 delay * 2
locked false, true 储存中继器是否被锁

3.4.2.2 检测信号变化

中继器接受来自其输入端方块的弱充能信号,与其当前的 powered 标签进行对比,若发现其状态需要改变,则添加一个计划刻事件用于延迟更新状态。计划刻事件的延迟如上所述,优先级如下:

  • 若其指向方块为红石二极管,且红石二极管的朝向非反向,则优先级为 -3
  • 若中继器的原 powered 标签为假,也即中继器准备熄灭,则优先级为 -2
  • 否则优先级为 -1

3.4.2.3 计划刻事件执行

如果中继器目前被锁住,则跳过该计划刻事件的执行。由于计划刻事件并不储存该事件的实际功能,中继器需要再次计算其输入端方块的弱充能信号,并与其当前的 powered 标签作比较:

  • 若当前状态为亮起且输入端无信号,则改为熄灭状态
  • 若的前状态为熄灭,则改为亮起状态,再判断输入端是否有信号,若无信号则添加一个延迟为其自身的延迟,优先级为 -1 [3] 的计划刻事件用于熄灭。也就是说,即便在执行计划刻事件的时候输入信号已经消失,中继器也依然会亮起,这也是中继器可响应并输出任意短的正脉冲的原理

上述过程中,改变其亮起状态时,在其所在位置发出的更新仅含状态更新。中继器在其前方发出的大范围方块更新见 红石二极管 的发出更新小节

3.4.2.4 受状态更新时

当中继器受到状态更新时,若更新方向为水平方向,且方向为垂直于中继器的指向方向,则更新自身的被锁状态。中继器当且仅当被一个二极管指向侧面的时候会被锁住

中继器响应状态更新

实例

与 4gt 时钟交互

当将 4gt 时钟,如对脸侦测器,的输出接至中继器链时,常常会见到下图的现象:中继器链大部分都在 4gt 时钟下闪烁,可是最末端的中继器却处于常量状态

与 4gt 时钟交互

这是因为中继器添加事件的优先级所导致的。

4gtrepeater2

4gtrepeater3

3.4.3 红石比较器

名称 信息
net.minecraft.block.BlockRedstoneComparator
基类 net.minecraft.block.BlockRedstoneDiode
延迟 2
优先级 非定值
位置需求 红石二极管
受方块更新时 红石二极管
受状态更新时 不响应

3.4.3.1 方块状态

红石比较器(简称比较器)在红石二极管的基础上,新增了一个方块标签:

标签名 取值 用途
mode compare, subtract 储存着比较器当前的模式(比较模式、减法模式)

对于当前比较器的输出信号的储存,可能是因为一些历史遗留问题(1.13 以前的方块附加值储存能力不足),是储存于比较器方块实体之中的。也就是说,比较器其实是一个含有方块实体的方块。不过由于该方块实体并不是常运算的方块实体 [4],因此并不会带来额外的卡顿

3.4.3.2 信号强度计算

net.minecraft.block.BlockRedstoneComparator#calculateInputStrength

首先,需要计算输入端的信号强度

令输入端,即比较器后方一格处的方块为 A,比较器后方二格处的方块为 B

比较器输入信号

  1. 维护一个取值为 0 ~ 15 的整数 $n$ 作为比较器待确定的输入的信号强度
  2. 计算方块 A 的弱充能信号强度(与中继器相同),将 $n$ 设为该信号强度
  3. 判断方块 A 是否可输出比较器信号
  • 如果是,则将 $n$ 设为 A 的比较器输出值。如比较器直接读取毗邻容器
  • 如果不是,若 $n < 15$ 且方块 A 为实体方块 [5],则进行尝试使用 B 处可能的容器输出来覆盖当前的输入强度:
    1. 如果方块 B 可输出比较器信号,则将 $n$ 设方块 B 的比较器输出值
    2. 如果方块 B 为空气,则查找位于方块 B 处的可能依附于方块 A 的一个物品展示框实体,若找到,则将 $n$ 设置为物品展示架的角度
  1. $n$ 即为比较器的输入信号强度

随后,比较器会计算其来自侧面的输入的最大值。比较器会接受以下三种形式的输入:

  • 毗邻的红石块
  • 毗邻的红石粉
  • 毗邻的指向比较器的强充能元件

下图是一些常见的给予比较器侧面输入信号的方法

比较器侧面输入

不过,强充能元件并非只有中继器、比较器。当比较器前方有二极管时,比较器可响应侦测器的侧方输入。如果使用指令等方式得到了浮空方块,你甚至可以使用拉杆、按钮等元件进行输入:

comparetor_side_input_sp

现在,我们已经知道比较器的后方输入以及侧面输入的信号强度了,现在只需要根据其所处模式(比较模式/减法模式)再次计算,即可得出比较器的输出信号了:

  • 比较模式:输出信号 = 后方输入(大小比较的相关逻辑并不在此处)
  • 减法模式:输出信号 = max(0, 后方输入 - 侧面输入)

之后,比较器就可以判断是否应该输出信号了。这部分的实现很简单,直接结合代码说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected boolean shouldBePowered(World worldIn, BlockPos pos, IBlockState state)
{
int i = this.calculateInputStrength(worldIn, pos, state);
if (i >= 15) // 输入信号强度大于15
{
return true;
}
else if (i == 0) // 输入信号强度为0
{
return false;
}
else
{
return i >= this.getPowerOnSides(worldIn, pos, state);
}
}

这里也是比较模式大小相关逻辑的实现之处

3.4.3.3 检测信号变化

与中继器类似,比较器会计算其当前应该输出的信号强度,并与当前自身储存着的信号强度作对比。如果输出的信号强度有改变,或者其亮起状态有变化,则添加一个计划刻事件,优先级如下:

  • 若其指向方块为红石二极管,且红石二极管的朝向非反向,则优先级为 -1
  • 其他情况,优先级为 0

3.4.3.4 计划刻事件执行

与中继器类似,由于计划刻事件并不储存该事件的实际功能,比较器需要再次计算其当前应该输出的信号强度,并与当前自身储存着的信号强度作对比。若两者不同,或者其亮起状态有变化,则更新当前的亮起状态,并发出更新。可以注意到,比较器并非同中继器一样,当原状态是熄灭的时候,无论如何都会点亮。如果比较器的计划刻执行时输入信号已消失,比较器是不会输出的

注意到对于任意一个红石二极管,当其方块状态发生变化时也会发出一次更新,因此若比较器在上述步骤中改变了亮起状态时,它会发出更新两次 <3 mojang

实例

响应计划刻元件的 2gt 脉冲

下面以典型的侦测器为例。正常情况下,侦测器脉冲是无法激活比较器的,如下面这个例子

侦测器尝试激活比较器

时序分析:

游戏刻 游戏阶段 事件
0 TT.0.侦测器 侦测器点亮。侦测器添加位于 gt 2 的计划刻事件用于熄灭,优先级为 0(关于侦测器的执行计划刻事件时的逻辑,见 侦测器
0 TT.0.侦测器 侦测器发出方块更新。比较器受到方块更新,发现可输出信号,添加位于 gt 2 的计划刻事件,优先级为 0
2 TT.0.侦测器 侦测器熄灭
2 TT.0.比较器 比较器判断目前输出信号强度(0)与应当输出的信号强度(0),无变化,不执行任何操作

可见,比较器的计划刻事件执行的时候输入端信号已经消失,这导致了比较器不响应侦测器的脉冲


若想要使用侦测器来激活比较器,有两种方法

1. 提升比较器的优先级

观察时序分析表,容易发现如果让 gt 2 中比较器的事件先于侦测器执行,则可以让侦测器执行计划刻事件时侦测器仍处于激活状态。一种可行的方法就是提升比较器的优先级,更准确地说,提升比较器添加用于亮起的计划刻事件的优先级。我们可以通过在比较器前方添加中继器来实现

侦测器尝试激活比较器

时序分析:

游戏刻 游戏阶段 事件
0 TT.0.侦测器 侦测器 A 点亮,添加位于 gt 2 的计划刻事件。侦测器更新比较器,比较器发现自身可亮起,添加位于 gt 2 的计划事件
2 TT.-1.比较器 比较器判断目前输出信号强度(0)与应当输出的信号强度(15),不相等。比较器亮起
2 TT.0.侦测器 侦测器熄灭

2. 延长等优先级的 2gt 脉冲

为了让比较器在执行其计划刻事件的时候输入端仍有信号,除了提升比较器的计划刻事件的优先级外,是否还有方法?更新比较器,让比较器添加计划刻事件的那个侦测器肯定不能起到作用了,那么是否可以额外添加一个侦测器,让第二个侦测器在比较器事件执行完毕之后再熄灭?答案是可以,方案如下:

双侦测器激活比较器

时序分析如下:

游戏刻 游戏阶段 事件
0 TT.0.侦测器 A 侦测器 A 点亮,添加位于 gt 2 的计划刻事件。侦测器更新比较器,比较器发现自身可亮起,添加位于 gt 2 的计划事件
0 TT.0.侦测器 B 侦测器 A 点亮,添加位于 gt 2 的计划刻事件。侦测器更新比较器,比较器尝试添加位于 gt 2 的计划事件,不过由于计划刻队列中已有该事件,忽略操作
2 TT.0.侦测器 A 侦测器 A 熄灭
2 TT.0.比较器 虽然侦测器 A 熄灭了,但是侦测器 B 仍处于亮起状态。比较器判断目前输出信号强度(0)与应当输出的信号强度(15),不相等。比较器亮起
2 TT.0.侦测器 B 侦测器 B 熄灭

值得注意的是,该方案的铁轨不可用 2gt 的计划刻元件脉冲去激活,具体原因与侦测器的实现有关

使用 0gt 信号激活比较器

通过上述例子我们可发现,若想要激活一个比较器,仅需要在比较器检查输入端是否有信号的时候,也就是以下两个时刻:

  • 添加计划刻事件时
  • 执行计划刻事件时

让输入端存在信号就行了。这意味着,激活比较器的信号并不需要是一个连续的信号,我们只需要用两次脉冲分别覆盖上述两个时刻,就能让比较器点亮。这两个脉冲的时长可以任意短,可以短到 0gt 时长,只要覆盖到了上述两个时刻就行了

对于上述的第一个时刻“添加计划刻事件时”,可在任意时刻实现;对于第二个时刻“执行计划刻事件时”,需理清楚比较器的计划刻事件的执行时间点

下图是一个使用两次 0gt 信号激活比较器的例子

0gt脉冲激活比较器

时序分析:

该装置中含有两个 0gt 脉冲发生器,脉冲的持续时间为 TT.-1 ~ BE,可覆盖正常优先级的比较器的计划刻事件时刻 TT.0

游戏刻 游戏阶段 事件
0 TT.-1.上中继器 A 中继器 A 亮起,充能铁块激活红石粉,更新比较器。比较器添加位于 gt 2 的计划刻事件
0 BE 活塞 A 推出,红石粉熄灭
2 TT.-1.上中继器 B 中继器 B 亮起,充能铁块激活红石粉
2 TT.0.比较器 比较器亮起,信号强度为 14(信号强度源自中继器 B)
2 BE 活塞 B 推出,红石粉熄灭。红石粉更新比较器,比较器添加位于 gt 4 的计划刻事件
4 TT 比较器熄灭

如果将比较器输入信号是否存在视作 0 1,画出波形图的话,是这样子的

波形图

类似的,如果我们每 2gt 都发出一次上述 0gt 脉冲的话,比较器就能一直保持常亮状态。下方是上述例子示意装置的一个简单扩展,如果有必要的话甚至可以让比较器的输出信号强度也保持不变

0gt脉冲常激活比较器

无延迟比较器

在传统的使用比较器传输模拟信号的线路中,比较器逐个亮起,依次往前传递信号,传输速度较慢,如下图

有延迟比较器

让我们分析一下上图,为什么这个比较器链传输信号如此之慢。上图中,比较器 B 需要等待比较器 A 亮起后才可亮起;比较器 C 需要等待比较器 B 亮起后才可亮起;比较器 D 需要等待比较器 C 亮起后才可亮起……每次等待,都耗费了 2gt 的时间进行延迟

这些一个个的 2gt 延迟真的有必要存在吗?答案是不。我们可以仅在一个 gt 的计划刻阶段中就完成所有比较器的点亮,这就是无延迟比较器

下图是一个简单的无延迟比较器的例子

无延迟比较器

无延迟比较器的基本思路是,通过使用额外的计划刻元件生成的短脉冲,主动使所有比较器在同一个 gt 依次执行自己用于亮起的计划刻事件,使得比较器链在瞬间同时点亮。这种诱导比较器在指定时刻添加计划刻事件的脉冲,我们称之为诱导脉冲

上述操作将长距离传输信号的时间复杂度从 $\Theta(n)$ 降至了 $\Theta(1)$,从线性降为常数级,仿佛用于传输信号的比较器都是 0 延迟,因此我们称之为无延迟比较器

下面是上图中的无延迟比较器的亮起操作的时序分析。熄灭/信号改变的时序分析同理:

游戏刻 游戏阶段 事件
0 NU 拉杆拉下
2 TT 比较器 1 亮起。侦测器亮起,中继器从近到远依次添加位于 gt 4 的计划刻事件
4 TT.-1.中继器 A 亮起,更新比较器 A,比较器 A 添加位于 gt 6 的计划刻事件
4 TT.-1.中继器 B 亮起,更新比较器 B,比较器 B 添加位于 gt 6 的计划刻事件
…… …… ……
4 TT.-1.中继器 F 亮起,更新比较器 F,比较器 F 添加位于 gt 6 的计划刻事件
4 TT.0 比较器 2 亮起。侦测器熄灭
6 TT.-1 中继器 A ~ F 依次熄灭
6 TT.0.比较器 A 比较器 A 发现其输入端有来自比较器 2 的信号,亮起
6 TT.0.比较器 B 比较器 B 发现其输入端有来自比较器 A 的信号,亮起
…… …… ……
6 TT.0.比较器 F 比较器 F 发现其输入端有来自比较器 E 的信号,亮起

不过,上述无延迟比较器有一个缺点,就是在输入信号强度为 15 的情况下改变信号强度时不能维持无延迟。这是因为当输入的信号强度是 15 时,比较器链中都储存着强度为 15 的信号,而侧方位中继器发出的脉冲信号强度也是 15,两者大小相等,导致了诱导脉冲无法诱使比较器添加计划刻事件。如下图所示:

于输入15信号的下降沿失效

对此的解决方法也并不困难。我们需要的是诱使比较器添加计划刻事件,只要按顺序添加了就可以,至于比较器是因为什么原因而添加计划刻事件,我们并不关心

当比较器输出信号是 15 的时候,还有什么方法能诱使比较器添加计划刻事件呢?在减法模式下向比较器侧面输入脉冲!这样子,比较器会发现其输出信号需要减去其侧面输入,从而添加计划刻事件

在向其侧面输入脉冲的条件下,向其背面输入端的脉冲也不可去掉,如下图所示

侧面输入脉冲

设比较器的原信号强度为 $n$

  • 若 $n > 0$,则来自比较器侧面的脉冲可让比较器输出的信号强度减小,从而诱使比较器添加计划刻事件
  • 若 $n < 15$,则来自比较器后方的脉冲可让比较器输出的信号强度变为 15,从而诱使比较器添加计划刻事件

【疑似有虫】当侧面输入与后方输入的信号强度相等时,比如使用中继器或侦测器这类输出 15 强度信号的元件时,这两个脉冲的顺序也是有讲究的。对于无延迟链上的每一个比较器,我们需要首先向其后端输入脉冲,再向其侧面输入脉冲,才可在所有情况下无延迟地传递信号。如果顺序相反,则当后端输入脉冲时,由于比较器侧面已存在信号强度 15 的输入,比较器将会输出 0 强度信号。若比较器储存着的信号已经是 0 强度了,那么这次脉冲输入就无法诱使比较器添加计划刻事件,从而使无延迟比较器失效

脉冲顺序【此图有虫】

下面图这类兼容所有情况的信号变化的无延迟比较器的一个实现

全情况可用的无延迟比较器

以上图为例,在使用中继器或中继器时,所有需要以以下的顺序向比较器输入诱导脉冲:

  1. 比较器 A 的后方输入端
  2. 比较器 A 的侧面输入端
  3. 比较器 B 的后方输入端
  4. 比较器 B 的侧面输入端
  5. ……
  6. 比较器 F 的后方输入端
  7. 比较器 F 的侧面输入端

当然,在实际使用中,由于上述无延迟比较器设计都不能无限地延长。不过,如果配合无延迟信号线使用,即可实现可无限延伸的无延迟比较器链。下面是一个可行的例子

无限延伸的无延迟比较器链

比较器更新检测器

3.4.4 红石火把

计划刻事件执行

blabla

3.4.5 侦测器

3.4.5.1 方块状态

作为一个可旋转的方块,侦测器拥有一个 facing 标签表示其指向。除此之外侦测器还具有以下标签:

标签名 取值 用途
powered false, true 其当前的亮起状态。我们称该标签为假时侦测器熄灭,该标签为真时侦测器亮起

3.4.5.2 接受方块更新

当侦测器输入端的位置发出了状态更新,将引起侦测器的检测

注意,用于块更新红石粉方侧下方红石粉链接状态的状态更新是不会被侦测器响应的

3.4.5.3 计划刻事件执行

blabla

投掷器/发射器

计划刻事件执行

blabla

红石灯

计划刻事件执行

blabla

探测类元件

计划刻事件执行

blabla

其他红石元件

计划刻事件执行

blabla

非红石元件

计划刻事件执行

blabla

流体

计划刻事件执行

blabla


  1. net.minecraft.util.EnumFacing#values ↩︎

  2. net.minecraft.block.BlockRedstoneDiode#updateState ↩︎

  3. 在 1.15 及以后,该事件的优先级改为 -2 ↩︎

  4. 也就是比较器方块实体并未实现 net.minecraft.util.ITickable 这个接口 ↩︎

  5. net.minecraft.block.Block#isNormalCube ↩︎