02 活塞
1 机制
基础部分
- 活塞的组成
- 无头活塞的创建
- 活塞的自检触发方式与红石信号判定 (QC 半连接性)
- 活塞尝试移动方块的结构分析与通俗逻辑
- 活塞的更新顺序与方块事件响应
进阶部分
- 活塞自检与信号判定的源码调用
- b36 转化顺序与推拉结构分析
- 方块移动底层实现与哈希表运用
- b36 到位与实体运算
1.1 活塞的组成
完整意义上的活塞由两部分组成,分别是活塞底座与活塞头。
活塞底座完全控制活塞头,并且活塞由两种状态:收回与伸出。通常情况下,当活塞处于被激活状态时,其会同时更改活塞底座为伸出状态,并出现活塞头。当活塞处于收回状态时,其活塞头也随即消失。也有部分手段可以创造出只有活塞底座,没有活塞头的无头活塞;也可以通过某些手段创造出只有活塞头没有活塞底座的活塞。
1.2 无头活塞
1.2.1 无头活塞的创建

所有无头活塞的创建本质上只有两种办法,分别是:
- 在活塞伸出时,活塞头处于 b36 状态时使用 TNT 等手段移除 b36
- 通过特定手段让未伸出的活塞触发收回事件。本内容会在后续更详细的阐述
1.2.2 无头活塞的基本机制
活塞头其实在活塞底座看来依然存在,它就位于活塞底座的正前方。注意,活塞底座认为自己正前方是活塞头与自己正前方是否真的是活塞头无关。如果更新未被充能的无头活塞,无头活塞就会自检并收回。
1.3 活塞的自检机制
1.3.1 触发方式
活塞当且仅当以下两种情况发生时触发自检:
- 被放置 (包括被玩家放置,或被其他活塞推拉到位放置)
- 受到 NC 更新
必须强调的是,活塞的自检是可以发生在任意阶段的。换言之,其自检是瞬时的。当上述情况发生时,活塞会立即自检并通过一定的逻辑尝试推出或收回。这里的 “尝试” 并不代表一定会成功执行。
1.3.2 红石信号的检查与 QC 充能
自检的本质是为了确认活塞是否需要动作,因此它会先判断自己是否被充能。

活塞检查信号的范围包括:
- 自身一阶毗邻 (除推动方向外) 的所有方块是否正在输出红石信号
- 自身正上方一格方块为核心的一阶毗邻方块是否正在输出红石信号
由于活塞会检查其上方方块的毗邻充能情况,这就导致了活塞可以通过上方的空间被 “隔空” 充能。这种充能无法直接对活塞发出 NC 更新,因此需要额外的 NC 更新来更新它,这就是活塞的半连接性 (Quasi-Connectivity, 简称 QC) 的来源。这也是为什么 BUD 只能检查 NC 更新。
1.3.3 自检逻辑通俗解释
活塞自检时,总是会遵循以下行为树:
- 应伸出且未伸出:创建一个分析实例尝试分析前方的推动结构。如果分析失败 (超过推动上限,有不可推动方块阻挡等),则不动作;如果分析成功,则给自己当前位置添加一个伸出的方块事件
- 不应伸出且已伸出:活塞需要收回
- 如果前方是一个正在伸出的同向移动的 "移动的活塞"(Moving_Piston, 或称 B36,36 号方块, 后续均简称 b36),且当前刻不会到位 (进度 < 50%),则触发瞬推收回,添加对应方块事件
- 否则,执行普通收回,添加对应方块事件。
1.4 活塞的推拉与可推动方块
1.4.1 可推动方块判定
一个方块要是能被活塞推动,必须同时满足以下要求:
- 在世界有效高度范围内
- 不能是黑曜石,哭泣的黑曜石,重生锚,或强化深板岩等硬编码不可推动方块
- 方块硬度不能为 -1
- 如果是带釉陶瓦,被推动方向必须与活塞推动方向一致
- 该方块不能带有方块实体 (如箱子、熔炉、漏斗)
1.4.2 尝试移动方块与结构分析
活塞在尝试推动时,会动态维护两个列表:推动方块列表与破坏方块列表。
它会以一定的顺序去顶前方将要推动的方块和将要破坏的方块结构。活塞只会注意在推动轴方向的可破坏方块,若方块附着在推动结构的侧面,则会在稍后被更新并掉落,而不会进入破坏列表。
提示:由于结构分析极其复杂 (直线分析与分支分析交替入栈),人工分析极为困难。强烈建议使用Fallen_Breath 的PistOrder 模组来直接在游戏内查看活塞尝试推动的方块到位顺序。
1.5 活塞的更新顺序
活塞的更新顺序总的来说分为五种情况:
- 活塞 / 粘性活塞只伸出
- 同活塞 / 粘性活塞推动方块,只是移动方块为空
- 活塞 / 粘性活塞只收回
- 创建活塞底座 b36,发出 PP 更新和 NC 更新
- 移除活塞头,发出 PP 更新和 NC 更新
- 活塞 / 粘性活塞推动方块
- 移除破坏方块位置的方块
- 在移动的目标位置创建 b36,发出 PP 更新
- 创建活塞头 b36,发出红石粉 prepare 更新与 PP 更新
- 在破坏方块位置发出红石粉 prepare 更新和 NC 更新
- 在移动原位置发出 NC 更新
- 在活塞头位置发出 NC 更新
- 在活塞底座发出 PP 更新和 NC 更新。
- 粘性活塞收回方块
- 创建活塞底座 b36,发出 PP 更新和 NC 更新
- 移除破坏方块位置的方块
- 在移动目标位置创建 b36,发出 PP 更新
- 移除未被覆盖的方块,发出红石粉 prepare 更新,PP 更新
- 移动原位置,发出 NC 更新
- 粘性活塞收回失败
- 创建活塞底座 b36 方块,发出 PP 更新和 NC 更新
我们发现,这个行为中不具备任何在活塞头的更新,也就是我们常说的粘性收回方块失败无更新。这里给几个实例:


1.6 b36 与方块到位
1.6.1 常规到位
在每个游戏刻的方块时阶段,b36 会给自己的推动进度 +0.5
- 0gt BE:开始推动,创建 b36,此时进度为 0
- 0gt TE:进度 +0.5,进行实体位移运算
- 1gt TE:进度 +0.5,进行实体位移运算,此事总进度达到 1.0
- 2gt TE:发现进度已经大于等于 1.0,b36 变回原来方块,发出最终的 PP 和 NC 更新。
若我们从 0gt AT 开始观测的话,在 3gt AT 才算完全完成方块到位,因此就有了所谓活塞 3gt 延迟的说法
1.6.2 瞬时到位
当活塞触发瞬推收回时,方块不会经理常规的 TE 阶段累加,而是直接强制到位。触发条件为:
- 收回事件,且活塞头位置当前方块时 b36.
- 收回事件,粘性活塞伸出方向一格外为 b36 且该 b36 正在朝活塞推动轴同方向移动。
这里举两个例子做演示:
活塞头位置为 b36
我们看如下的一个实例:

在这个实例中,红色玻璃被瞬间到位,这是从观测角度而言的,而从理论上,我们给出如下解释:
中继器的优先级高于红石火把,所以普通活塞先添加方块事件,无头粘性活塞再添加方块事件,尝试拉回更前方 1 格的方块(实际上此时 actionType=1)
但是,在 BE 阶段,由于 b36 的插入,粘性活塞拉回受阻,并不会破坏这个 b36,此时活塞头位置为 b36,触发瞬时收回,红色玻璃瞬时到位。
粘性活塞伸出方向一格外为 b36 且 b36 移动方向与活塞移动方向一致

在以上实例中,红色玻璃被瞬时到位,是因为:
活塞推动粘液棒子,红色玻璃被移动,发出 NC 更新,触发无头粘性活塞的自检。此时,粘性活塞可以成功拉回,并且在伸出方向一格外存在 b36,该 b36 推动方向与粘性活塞推动方向是一致的,因此被瞬时到位。
当然,值得注意的是,这种瞬时到位只会影响一格方块,也就是粘性活塞面前的方块,其余方块仍然会被转化为 b36,所以,我们可以利用这一点稍微解读一下一种在许多人看来是魔法的装置:

在这个装置中,当拉杆被拉下,粘性活塞会在方块到位 1gt 后移动它侧翼的方块,此时这个方块被粘液块带着向前推动,但由于粘性活塞是被侦测器激活的,该粘液块被提前到位,被活塞回推,而此时这个方块尚处于 b36 的状态,因此被留在了被带过去的位置,这样就实现了对方块流的延长。
瞬时到位会将进度直接设为 1.0,移除 b36 方块实体,直接替换为原本的方块并发出更新。值得注意的是,瞬推不会附带对周围实体的位移推拉运算。
值得注意的是,由于笨蛋麻将并没有在这部分写任何实体运算或对含水方块的特殊判断,所以瞬推不会影响实体,也不会消除含水状态。但是值得注意的是,如果在此前经历了 TE 阶段的实体运算,在此前的实体运算会被保留,仅有当前 gt 的 TE 阶段的实体运算会被忽略。

1.7 活塞自检逻辑与源码分析
1.7.1 自检的三个入口
活塞有三个自检入口:被玩家放置、受到 NC 更新,以及被其他方式放入世界。三个入口虽然对应不同的源码方法,但职责相同:只在服务端发起一次活塞动作检查(源码方法PistonBlock.tryMove)。首次阅读时不需要记住三个方法名,只需记住它们最终会进入同一套判断。
1.7.2 充能状态检查(shouldExtend)
充能状态检查负责回答一个问题:** 活塞此刻是否应该伸出?** 它先检查活塞自身周围,再检查活塞上方一格周围;任意受检位置能够向活塞供电,检查结果便为 “应当伸出”。后一部分检查就是 QC 充能的来源。源码将这项职责写在shouldExtend方法中。
1.7.3 活塞动作检查(PistonBlock.tryMove)
活塞动作检查比较 “现在是否应当伸出” 和 “当前是否已经伸出”,再决定要不要登记方块事件。这里还没有真正搬动方块:
- 应当伸出但尚未伸出:先检查前方结构能否推动;成功后登记伸出事件。
- 不应伸出但已经伸出:登记收回事件;若活塞前方满足瞬推条件,则改为登记瞬推收回事件。
- 供电状态与当前状态一致:不登记事件。
源码使用事件类型actionType区分三种结果:0 表示伸出,1 表示普通收回,2 表示瞬推收回。
1.8 b36 转化与结构分析源码
温馨提示,这部分是本章节最为复杂的一部分之一,如果比较懒可以直接使用 pistOrder 模组而并不需要真正学习本部分在说的内容
这一部分只解决一个问题:活塞怎样把相互连接的方块整理成一条可以执行的推动结构?游戏先汇总正前方的直线结构,再从粘液块和蜂蜜块向侧面展开黏附分支,最终得到待移动列表与待破坏列表。源码中的movedBlocks和brokenBlocks分别对应这两个列表。
1.8.1 推动结构汇总(PistonHandler.calculatePush)
calculatePush是整个推动结构分析的编排者。它的职责是清空旧结果,先分析正前方的直线推动链,然后在遍历已添加方块时持续展开粘液块和蜂蜜块侧面黏附的分支,最终得到一份完整的待移动方块列表。列表中方块的排列顺序,决定了后续创建 b36 时的逆序遍历顺序。
movedBlocks和brokenBlocks在正文中分别称为待移动列表和待破坏列表。它们是源码中的列表对象,不需要 Java 知识也能理解行为。
编排流程:
- 清空待移动列表和待破坏列表
- 检查起始位置 (活塞头前方) 是否可推动
- 不可推动且可破坏: 加入待破坏列表, 返回成功
- 不可推动且不可破坏: 返回失败
- 可推动: 调用
tryMove建立直线推动链- 直线链建立失败: 返回失败
- 直线链建立成功: 遍历
movedBlocks列表 (注意, 遍历过程中列表还在增长)- 遇到粘性方块: 调用
tryMoveAdjacentBlock检查侧面黏附分支- 侧面分支添加失败: 返回失败
- 所有粘性方块的侧面分支均成功: 返回成功
- 遇到粘性方块: 调用
游戏分别使用推动链直线检查(PistonHandler.tryMove) 和黏附分支检查(tryMoveAdjacentBlock) 完成后续分析。1
1.8.2 推动链直线检查(PistonHandler.tryMove)
tryMove负责分析某个方块能否移动, 并将整条相连的直线推动链追加到movedBlocks列表。它会在两种情况下被调用:
- 直线推动:从活塞头前方或粘性方块侧面, 沿推动方向继续向前检查直线结构
- 黏附拉动:粘性方块侧面黏附的方块需要被分析时
返回值含义:
- 返回
false:这个方块 (或这条分支) 的引入会导致整个推动失败 - 返回
true:这个方块和它的直线推动链可以安全添加, 或这条分支不需要扩展但也不阻塞推动
1.8.2.1 早期退出判断
这些提前退出的true不表示 "方块一定会被移动", 而是表示 "当前分支不需要继续扩展, 也不会导致推动失败"。具体情况:
- 空气:直线推动链遇到空气时自然终止, 不需要继续向前
- 不可移动方块:从黏附侧面遇到时, 该分支停止扩展, 但不阻塞主推动链
- 活塞底座:检查回到活塞自身时停止, 避免把活塞加入推动链
- 已在列表:已经被其他分支添加过, 避免重复处理
1.8.2.2 向后粘性扫描 (拉动部分)
当目标方块是粘性方块时, 游戏会向推动方向的反方向扫描后方黏附的方块。这是为了处理黏性方块拉动后方方块的情况。扫描会在遇到空气、不互粘的方块、不可移动的方块或活塞底座时停止。
此段代码的完整实现会将扫描到的后方黏附链追加到movedBlocks列表, 但完整追加逻辑的源码未在本文展示。如果加上后方黏附链后总方块数超过 12, 则推动失败。
1.8.2.3 向前直线扫描与碰撞处理
完成后方黏附扫描后, 游戏继续向推动方向向前扫描。关键行为:
- 碰撞检测:如果正前方方块已经在
movedBlocks列表中 (索引l > -1), 说明当前分支的推动链与之前已添加的结构发生碰撞 - 列表重排:游戏调用
setMovedBlocks(succeededCount, l)重排当前列表。根据D:\guardian中的完整实现,它把列表拆成 “汇合点之前的前缀”“本次刚加入的尾段”“汇合点至新尾段之间的中段”,再按 “前缀 → 新尾段 → 中段” 拼回 - 后续检查:重排后遍历受影响范围, 对所有粘性方块调用侧面黏附检查, 任何侧面分支失败则整体推动失败
- 正常扩展:如果正前方不是碰撞, 则检查是否是空气 (成功终止)、不可移动 / 活塞底座 (失败)、可破坏方块 (加入破坏列表并成功终止), 或者继续将方块加入列表并向前推进
列表顺序与 b36 创建顺序的关系:这里追加到movedBlocks的方块顺序, 会在后续PistonBlock.move中被逆序遍历, 因此列表中靠后的方块会先创建 b36。
1.8.3 黏附分支检查(tryMoveAdjacentBlock)
tryMoveAdjacentBlock只在calculatePush遍历到粘液块或蜂蜜块时才会被调用。它负责检查粘性方块侧面黏附的四个方向 (垂直于推动轴的方向), 对每个黏附的方块调用tryMove进行直线结构分析。
关键设计:
- 只检查垂直方向:排除与推动轴平行的前后两个方向, 只检查垂直于推动轴的四个侧面
- 逐个调用直线检查:对每个互粘的侧面方块调用
tryMove, 触发完整的直线推动链分析 - 任一失败即整体失败:如果任何一个侧面黏附的方块无法移动, 整个推动结构分析失败
遍历顺序与 NC 更新顺序一致 (下、上、北、南、西、东), 但由于只保留垂直于推动轴的四个方向, 实际顺序依赖推动轴方向。
1.8.4 分析结果列表与 b36 创建顺序
待移动列表 (movedBlocks) 和待破坏列表 (brokenBlocks) 的逆序, 就是PistonBlock.move中实际创建 b36 和破坏方块时采用的顺序。为什么是逆序? 因为move方法会从列表末尾向前遍历 (for (int k = blocksToMove.size() - 1; k >= 0; --k)), 远端方块先变成 b36, 近端方块后变成 b36。这样设计是为了避免方块覆盖冲突。
在正常连续运行中,b36 创建顺序也就是 b36 到位顺序。PistonBlock.move按待移动列表的逆序创建 b36,并按同样的顺序注册方块实体调用器;方块实体刻随后按调用器列表顺序执行。区块卸载、重载会重新建立调用器,因此重载后的顺序仍可能变化。
1.8.5 简单示例: 理解 movedBlocks 顺序与 b36 创建顺序
1.8.5.1 示例 1: 直线推动链
假设活塞向 + x 方向推动,活塞头前方依次是 A、B、C,之后是空气:

分析过程:
calculatePush从 A 开始调用推动链直线检查。- 检查依次把 A、B、C 加入待移动列表;遇到空气后,这条直线结束。
movedBlocks列表为[A, B, C],按从近到远排列。
b36 到位顺序:
PistonBlock.move从列表末尾向前遍历。- 创建顺序为 C、B、A,即远端方块先创建 b36,近端方块后创建。
movedBlocks顺序: [A, B, C] (索引0,1,2)
b36到位顺序: C, B, A (逆序遍历)
1.8.5.2 示例 2: 黏附侧面分支
假设活塞向 + x 方向推动,活塞头前方是粘液块 S,S 的 + y 方向黏附普通方块 A:

分析过程:
calculatePush调用tryMove(S),S 加入列表:[S]calculatePush遍历列表, 遇到粘性方块 S, 调用tryMoveAdjacentBlock(S)tryMoveAdjacentBlock检查 S 的四个垂直方向, 发现 + y 方向的 A 与 S 互粘, 调用tryMove(A, +y)tryMove(A, +y)分析 A 的直线推动链, A 加入列表, A 前方是空气, 终止- 最终
movedBlocks列表:[S, A]
b36 到位顺序:
- 逆序遍历: A 先创建 b36,S 后创建 b36
movedBlocks顺序: [S, A]
b36到位顺序: A, S
1.8.5.3 示例 3:三层黏附结构的完整分析
下面分析一个三层粘液块结构。三张图使用相同的水平坐标:**Layer 1 中的粘液块 1、Layer 2 中的粘液块 2、Layer 3 中的粘液块 3 位于同一列,只是高度不同。** 活塞的实际推动方向为 + Z,也就是图中的向下方向。

这八个方块的实际 b36 到位顺序如下:
由于PistonBlock.move逆序读取待移动列表,先把到位顺序反过来,就能得到最终的movedBlocks:
下面按照源码逐步还原这份列表。+Z 推动时,tryMoveAdjacentBlock会排除 Z 轴上的北、南,只按下 → 上 → 西 → 东检查侧面。
- 建立起点:活塞正前方是 Layer 1 的粘液块 1,也就是顺序图中的 8。推动链直线检查先把 8 加入列表:
[8]。 - 检查 8 的下方:侧面检查的第一个方向是下。8 的正下方是 Layer 2 的粘液块 2,也就是 7,因此加入 7:
[8, 7]。 - 检查 8 的水平方向:下方分支结束后,检查继续走到水平方向,并找到与 8 相连的中心粘液块 5。对 5 调用推动链直线检查时,游戏会先沿推动反方向 - Z 扫描:先找到 5 上方的 6,再回到 5;随后沿 + Z 继续找到 5 下方的 4。因此这一整条直线按
6 → 5 → 4加入:[8, 7, 6, 5, 4]。 - 继续遍历新增方块:
calculatePush使用的循环条件会实时读取列表长度,所以刚加入的 7、6、5、4 随后也会被逐个检查。轮到 7 时,它的下方是 Layer 3 的粘液块 3,于是加入 3:[8, 7, 6, 5, 4, 3]。 - 展开第一层右枝:轮到 5 时,侧面检查找到右侧的 2,将其加入:
[8, 7, 6, 5, 4, 3, 2]。 - 展开第三层右枝:轮到 3 时,侧面检查找到右侧的 1,将其加入,得到最终列表:
[8, 7, 6, 5, 4, 3, 2, 1]。
这个结构中并没有发生 “向前扫描再次撞上已在列表中的方块”,因此不会调用setMovedBlocks。它的顺序完全来自三条规则:侧面方向按下、上、西、东检查;黏性直线先沿 - Z 回溯,再沿 + Z 前进;calculatePush会继续遍历分析过程中追加到列表末尾的新方块。
最后,PistonBlock.move从列表末尾向前创建 b36,调用器也按这个顺序注册,所以正常连续运行时的到位顺序就是1 → 2 → 3 → 4 → 5 → 6 → 7 → 8。
1.9 活塞响应事件与移动方块源码
1.9.1 方块事件响应入口(onSyncedBlockEvent)
动作检查只负责登记事件;真正执行伸出、普通收回或瞬推收回的是方块事件响应入口。它先复核当前供电状态,避免执行已经过时的动作,然后按照事件类型进入伸出或收回流程。以下源码用于定位这些分支,正文结论见代码后的分类说明。
服务端
如果不是客户端,那么会执行几个服务端特有的事件:
- 检查激活情况,如果激活,并且方块事件为收回或者瞬推收回,将活塞设置为伸出状态,并进行寻路更新,方块事件执行失败。
- 如果没有激活并且方块事件为伸出,方块事件执行失败。
伸出事件
注意,本部分无论客户端,服务端都必须执行
活塞会尝试移动前方方块,这是通过move方法实现的,我稍后会补充这个方法的细节。如果移动失败,则活塞执行事件失败。
接下来,将活塞设置为伸出状态,此处bitflag为0b01000011放置移除更新,寻路更新,NC 更新
最后,播放活塞伸出的声音,方块事件执行成功
瞬间收回与收回事件
首先获取活塞头的方块实体,如果是 b36,那么使其瞬间到位。
接下来,将活塞设置为 b36,此处bitflag为0b00010100,无 PP 更新,寻路更新
再将活塞设置成 b36 方块实体,先发出 NC 更新,再发出 PP 更新
如果这个活塞是一个粘性活塞,那么将活塞头伸出 1 格外的地方,如果它试图放置活塞头的位置的一格外是一个 b36,并且该 b36 的推动方向与活塞同向,那么让方块瞬间到位。
如果活塞行为不为瞬推收回,并且该方块不是空气,活塞可以移动前方一格的方块,而且这个方块可以被正常拉回或者这个方块是活塞或者粘性活塞,那么尝试拉回前方方块。如果前方一格的方块无法被拉动,那么删除活塞头位置的方块。
如果是普通活塞,直接删除活塞头位置的方块。
无论何种情况,播放活塞收回的声音。方块事件执行成功。
1.9.2 实际搬移流程(PistonBlock.move)
方块事件入口决定 “做哪种动作”,实际搬移流程则负责 “怎样搬”。它重新检查推动结构,依次处理待破坏方块、把待移动方块转换为 b36、清空没有被新方块覆盖的原位置,最后集中发出更新。
实际搬移流程同时服务于伸出和收回。源码参数retract的命名容易误导:在这段实现中,值为true表示伸出,值为false表示收回。正文后续直接使用 “伸出流程” 和 “收回流程”,不再依赖这个布尔变量理解行为。
当活塞尝试收回时,游戏会首先检查预期是活塞头的位置是不是活塞头,如果是活塞头才执行收回。每次执行收回时,游戏都会先将活塞头的位置设置为空气(删除活塞头)。此时的setBlockState的传入bitflag为0b00010100,即无 PP 更新,和客户端的寻路更新。
在正式开始移动前,无论收回或伸出,游戏都会调用5.4.2部分分析的calculatePush方法,再次分析是否可以移动,如果不能成功移动,则活塞执行推动 / 拉动失败。
接下来,游戏会正式开始移动方块,它首先会逆序地处理破坏方块列表 (brokenBlocks),如果被破坏的方块具备掉落物品的方块实体,那么掉落物品,再将被锚定破坏的方块(也就是当前正在处理的方块)设置为空气,此时无 PP 更新,发出寻路更新。
破坏了应该破坏的方块后,游戏会开始移动方块。从后往前遍历移动方块列表 (movedBlocks),将该方块设置为 b36,bitflag为0b01101000,也就是放置移除更新,再向该方块添加 b36 的方块实体,并添加到blockStates列表中备用。
如果是伸出,就在活塞头位置创建 b36,放置移除更新,再向活塞头位置添加 b36 方块实体。
完成以上操作后,将未被覆盖到的方块全部设置为空气,并放置移除更新,寻路更新。此时无 PP 更新。再统一发出更新,对于哈希表中所有的方块,都先统一发出红石粉 prepare 更新,再给出方块更新(在方块原位置),如果是伸出,活塞头完成方块更新。
以上代码解释了为什么游戏会按待移动列表的逆序创建 b36。结合D:\guardian中方块实体调用器的注册与执行逻辑,正常连续运行时,这个逆序同时也是 b36 到位顺序。区块卸载、重载会重新建立调用器,原有顺序可能丢失。
1.10 到位的源码解析
1.10.1 普通到位tick
这东西太简单了没什么好讲的,所以通俗解释都扔到前面去了 (笑),因此这里只放个代码供参考
1.10.2 瞬时到位finish
大部分内容前面也解释过了,所以这里直接略过。我们只看后半部分的 b36 特判看起。
在 1.20 中,只有活塞臂的source属性是true,其它都是false。因此当该方法被调用时,若该位置是目标方块为活塞臂的 b36,会直接变成空气。其余部分就不细说了,前面都讲过了。
1.11 活塞头
本章其实没什么难度,但是由于除了前阵子Menggui233在PetrisAFE的视频中演示的装置外,未见得其它用途,故在这里放在了进阶的位置。
我们首先观察如下装置:

现在,拉下拉杆

观察到活塞底座被换成了侦测器,而活塞头被保留。为什么?
我们首先回顾一下 b36 到位时发生了什么:
- 给出自身位置的 PP 更新
- 给出自身位置的一阶毗邻的 NC,PP 更新
- 给出自身位置的 NC 更新
因此,我们分别查看活塞头受到 PP 更新和 NC 更新会发生什么
1.11.1 活塞头存在的条件判定canSurvive
isFittingBase方法本质上说明,当且仅当一个活塞头的后方 (更确切地说,是推动方向的反方向 1 格,后续均以后方简称) 是正在伸展的活塞,且该活塞的伸出方向与活塞头一致活塞头可以被称之为适配。而canSurvive则要求一个活塞头的存在是合法的,当且仅当该活塞头是适配的,或其后方是一个与活塞头推动方向相同的 b36。
1.11.2 活塞头的移除onRemove
一句话总结:移除时如果状态为适配,则连带着底座一起移除,并掉落活塞。
1.11.3 活塞头受到 PP 更新,NC 更新
PP 更新
活塞头实际上只关心来自后方的 PP 更新。当后方发生 PP 更新时,活塞头会调用canSurvive来检查自己是否合法,若不合法则立刻消失。
NC 更新
在任何情况下,活塞头均不会因受到 NC 更新而消失。当受到 NC 更新时,若自身合法,则将更新视为后方方块收到的 NC 更新。
1.11.4 案例分析
因此,对于在章首提到的案例:
- 0gt AT: 玩家拉下拉杆,活版门触发
- 2gt BE 深度 0: 粘性活塞被激活,更新无头活塞
- 2gt BE 深度 1: 无头活塞吃掉粘性活塞底座,粘液块 & 亮起侦测器被推动
- 4gt TE: 粘性活塞头先到位,此时检查后方是和自己同方向的 b36,自检通过。随后亮起侦测器 b36 到位,无更新。
因此侦测器成功取代了原本的活塞底座。
1.12 b36 相关的实体运算
pushEntities:处理活塞推动
该方法是活塞与实体交互的核心。它会获取 b36 在当前游戏刻的移动方向和距离,并找出路径上所有可能接触的实体。然后,它会精确计算每个实体与 b36 在移动方向上的最大重叠部分,并据此推动实体(同时增加微小位移 0.01 防卡入)。此过程还会特殊处理黏液块的弹射效果以及 b36 收回时对实体的影响。
其工作流程如下:
- 确定移动参数:首先,获取活塞头在当前游戏刻(tick)的移动方向和精确距离。
- 粗略筛选实体:通过计算活塞头在这一帧内扫过的空间,快速找出所有可能被推动的实体。
- 精确计算重叠:遍历每一个可能被影响的实体,并将其碰撞箱与活塞头模型的每一个子碰撞箱进行比较。它会精确计算出在活塞移动方向上,实体与活塞头之间发生的最大重叠距离。
- 执行推动:根据计算出的最大重叠距离,对实体施加一个位移。为了防止实体在下一帧被卡住,推动的距离会额外增加一个极小值(0.01)。
- 特殊情况处理:
- 黏液块:如果推动的方块是黏液块,它会对非玩家实体产生一个弹射效果,将其沿移动方向的速度设置为一个固定值。
- 活塞收回:如果活塞正在收回,它会调用 push 方法来特殊处理可能被拉动的实体,确保它们不会卡在活塞臂消失后的空间里。
push:解决实体卡入问题
这个方法是一个辅助函数,专门用于解决实体意外卡入活塞臂所在方块的问题。
其工作流程如下:
- 检查重叠:首先,检查实体的碰撞箱是否与活塞臂目标位置的方块碰撞箱有重叠。如果没有重叠,则说明实体没有卡住,方法直接结束。
- 计算推出距离:如果检测到重叠,它会计算需要将实体沿反方向推出多远才能恰好分离。它会进行两次计算和比较,以确保推出的距离精确且最小,避免过度位移。
- 执行位移:最后,它会沿活塞移动的相反方向,将实体推出计算好的距离,从而平滑地解决卡入问题。
moveEntitiesInHoneyBlock:实现蜂蜜块粘滞效果
这个方法专门用于实现移动中的蜂蜜块对上方实体的 “粘滞” 效果。
其工作流程如下:
- 条件检查:该功能只在活塞推动的是蜂蜜块,并且是水平移动时才会触发。
- 定义粘滞区域:它会在移动的蜂蜜块表面上方定义一个特定的 “粘滞区域”(从蜂蜜块顶部到 Y=1.5 的高度)。
- 同步移动:系统会找出所有位于这个粘滞区域内的实体。
- 施加位移:最后,将这些实体与蜂蜜块一起,沿着相同的方向移动相同的距离,从而实现了实体被 “粘” 在蜂蜜块上一起移动的效果。
1.13 基于Extended=False的活塞收回事件的无头活塞制造办法
我们先召回前面提到的onSyncedBlockEvent。值得注意的是,当服务端执行该方法并return false时,并不会向客户端发包,因此客户端也不会执行任何动画,或将任何东西变成 b36。(也就是客户端并不会执行该方法)
实际上在前文提到的setBlockState的传入形参flag仅仅在通常意义下是寻路更新,这里需要更为详细的解释。
我们观察如下现象:
- 放置一个任意方向的任意种类的活塞。并在其除活塞面向方向的一阶毗邻位置放置红石块。这一步会激活这个活塞
/tick freeze- 移除红石块
- 将原本的活塞替换为任意方向的同一类型的活塞
- 放回红石块,红石块的位置要求等同与第一步提到的位置要求
- 在活塞面向方向放置一个不可推动方块
/tick freeze解除时停
如果操作完全没有错误,则该活塞变为无头活塞
这是因为在onSyncedBlockEvent中
方块事件在执行时只会校验方块类型和位置,不会校验这个方块的朝向,因此在这个活塞的位置,我们得到了一个可以校验通过并执行的,活塞状态处于 Extended=False 的活塞,并且该活塞将执行收回事件。在服务端,活塞会被校验是否应该伸出。此时活塞因为被红石块激活,确实应该伸出,所以进入第一个 if 分支条件。这个分支条件首先调用了setBlockState,而我们查看一下setBlockState在flag=2时做了什么:
我们发现在if ((flags & 2) != 0 && (!this.isClient || (flags & 4) == 0) && (this.isClient || worldChunk.getLevelType() != null && worldChunk.getLevelType().isAfter(ChunkLevelType.BLOCK_TICKING)))这个分支调用中,调用了一个方法updateListeners,我们会发现这个updateListeners有两个实现方法:
ServerWorldClientWorld
在ServerWorld中,这里就是普通的寻路更新。这里寻路更新会假定活塞臂存在,当然这是题外话了。
而在ClientWorld中,这个updateListeners负责了渲染。因此这里客户端抢跑了,直接渲染了伸出的活塞臂而没有渲染活塞头。
接下来是第二个,由于活塞自检引起的伸出事件。这个伸出事件在执行时会直接失败。无论客户端还是服务端,在这里不做进一步解释。
因此,这样就制造出了一个无头活塞。
1.14 案例分析
1.14.1 案例 1

敲击音符盒,会发生什么?
粘性活塞受到 NC 更新后开始自检。活塞动作检查登记收回方块事件,随后由方块事件响应入口(onSyncedBlockEvent)执行,并进入下述判断:
稍做判断,进入move方法。
由于目标位置是活塞头,且是收缩动作,将活塞头位置设为空气,触发活塞头的onRemove,活塞头连带着活塞底座掉落。
1.14.2 案例 2

敲击音符盒,会发生什么?
步骤如前一例所述,但这里进入move后不符合上文的移除活塞头的方法。于是进入下一步
由于面前结构无法拉回,则直接return false,无事发生。
1.14.3 案例 3

敲击音符盒,会发生什么?
回到onSyncedBlockEvent,依旧是这个判断:
但在此处,if 分支成立 (黑曜石不可移动,!isMovable是true),因直接移除活塞面前的方块,普通方块被移除。
1.14.4 案例 4

敲击音符盒,会发生什么?
同案例 2,进入move方法。当活塞尝试计算是否可拉回时,它把自己检查到了,而激活的活塞底座本身又是不可推动的,于是return false,无事发生。
1.14.5 案例 5

拉下拉杆,会发生什么?
依旧考察onSyncedBlockEvent,在此处注意到这段代码:
拉下拉杆时,前方的活塞先在面前位置创建一个 b36 (注意活塞底座并未变为 b36),然后执行上述操作,调用了前一个活塞的活塞臂的finish方法。我在前面提到,该方法对于活塞臂的 b36 会有特判,直接将其变为空气。因此前一个活塞的活塞臂就被移除,变为无头活塞。后一个位置的粘性活塞收回,变为Extended=False的活塞。
1.14.6 案例 6

拉下拉杆会发生什么?
原理和案例 5 类似,但是调用的是这里的代码:
面前是 b36 就直接调用finish方法,将其变为空气。
1.14.7 案例 7

拉下拉杆会发生什么?
拉下拉杆,无头活塞面前方块为 b36,并被更新。红石块瞬间到位,造成 NC 更新,更新到中继器,添加 TT 事件
但注意,这里onSyncedBlockEvent还没被 return,继续执行如下操作:
于是红石块就被移除掉了。发出 NC 更新。添加 TT 事件。
中继器亮起 2gt,于是最左侧粘性活塞丢方块。
2 应用
2.1 复制
2.1.1 亮起侦测器的移除运算
亮起侦测器被移除时会向后方发出 NC 更新并熄灭自己。
2.1.2 基于侦测器的复制
复制的本质实际上是这个方块添加进了移动列表,但是这个时候方块还没转化为 b36。我以某种迅雷不及掩耳盗铃的姿势插入一个更新,这个时候这个更新导致了某些东西被激活 / 掉落,但是因为这个方块本身已经滚进移动方块列表,到位的时候又被以 b36 的形式放下来了。
我们将复制分为两种,基于侦测器和基于附着性方块的。其中,基于侦测器的复制正利用了上述只要侦测器被移除(换成了 b36)的时候就会瞬时产生 NC 更新。
这里给出基于侦测器的铁轨复制:

在移动方块列表中,当粘性活塞推出时,会先移动左侧侦测器,在目标位置创建 b36,再移动粘液块,覆盖了亮起侦测器,更新,铁轨发现下面居然是 b36,掉落。移动铁轨,在目标位置创建 b36。
这样我们就完成了一次复制
2.1.3 基于附着方块破碎的复制
2.1.3.1 基于 BUD 的 TNT 复制


附着方块的掉落同样会产生更新,但是由于附着方块通常会直接掉落而难以二次利用。例如:
2.1.3.2 基于粉导向的极简 TNT 复制

2.1.3.3 地毯复制
下图中是一种极为流行的地毯复制:

当活塞收回时其完成一次复制,我们在这里对其稍做分析:
移动方块列表:
- 移动最下方的粘液块,在目标位置创建 b36
- 移动失活的珊瑚扇,在目标位置创建 b36,发出 PP 更新,自检发现自己应该掉落,发出 PP 更新和 NC 更新,引导上方地毯连锁掉落
- 移动其余粘液块和地毯
这就完成了复制。
2.2 信息不一致的方块事件
根据 5.1 章节、2.1.5 章节等,我们可以发现:活塞在执行方块事件时只对其方块位置和方块类型进行校对。尽管记录了活塞朝向,但它只管辖 b36 的方向。
而活塞执行方块事件时的真实方向是即时采样的。
这意味着,在一个活塞的方块事件实例被在方块事件阶段中执行前,存在一个时段:此时,我们可以随意改变其对应位置的活塞的属性,甚至对此活塞本生进行破坏,更换。

如图所示,从上一游戏刻的 EU 到当前游戏刻的 BE,这段时间内我们可以修改某一方块事件的作用对象。
同理,从当前游戏刻的 EU 到下一游戏刻的 BE,也存在这样一段时间。
但只要执行到这个活塞的方块事件实例时,如果执行位置有活塞,且活塞类型没有改变,那么活塞尝试推出 / 拉回的朝向就会由执行时的朝向决定,尽管这个朝向可能并不是方块事件添加时的朝向。
有趣的是,b36 的收回时的动画仍然以方块事件传入的 data 参数为准,
所以,尽管方块事件执行时活塞朝向与添加时不同,活塞头的动画仍会面向方向方块事件添加时的活塞朝向(但碰撞箱是准确的)。
2.2.1 破基岩
2.2.1.1 跨越游戏刻的破基岩
活塞的逆袭之旅
正如前面所提到的,我们可以在活塞的方块事件阶段前,偷偷摸摸地修改活塞的属性,包括朝向。
这意味着我们可以在活塞推出时,将其朝向改为朝向基岩,然后在活塞收回时,就会将基岩当作活塞头摧毁。
下面的展示动画是一个典型, 在方块事件阶段前更改活塞朝向的破基岩例子。

在整个过程中,发生了如下重要事件
CECECE {gt0}-EU 阶段
- 左侧 TNT 爆炸。
-
爆炸破坏拉杆。
-
爆炸破坏圆石。
-
右侧 TNT 爆炸。
- 爆炸破坏活塞头, 发出 nc 更新。
- 活塞基座被更新,由于拉杆已经被炸掉,活塞给自己添加收回的方块事件。
可惜的是,它已经错过了这个游戏刻的 BE 阶段,所以只能在 gt1 的 BE 阶段默默等待了。。
- 活塞头破坏活塞基座。
-
CECECE {gt0}-AT 阶段
- 玩家放置朝下的活塞新活塞。
CECECE {gt0}-BE 阶段
- 活塞按照现有朝向收回,破坏基岩。
以上便是破基岩的全部过程。
但是,在破基岩机等结构内,由玩家放置活塞是不可能的,我们必须通过其他方式来实现这个效果。
2.2.1.2 在方块事件阶段完成的破基岩

如你所见,下图是部分破基岩机中的破基岩结构的简化版本..


如动图所示,在整个过程中,发生了如下重要事件
前提 -- 上图面标记的数字是各个活塞的深度,他们按照这个顺序在 BE 阶段尝试伸出或收回。
CECECE {gt0}-BE 阶段
- 1 号活塞收回,破坏活塞头, 发出 nc 更新
- 1 号活塞前方的活塞基座被更新,由于处于 BUD 状态,活塞给自己添加收回的方块事件。
由于深度 1,2,3 已被占用,它将会被安排到 4 号,也就是目前情况下的 "最后执行"。
- 活塞头破坏活塞基座。
- 1 号活塞前方的活塞基座被更新,由于处于 BUD 状态,活塞给自己添加收回的方块事件。
- 2 号活塞伸出,创建了 3 个 b36。
- 3 号粘性活塞收回,将正前方的朝下的活塞的 b36 强制瞬间到位。
- 朝下的活塞现在的位置拥有先前 1 号活塞导致的方块事件,按照现有朝向收回,破坏基岩。
2.3 欺骗活塞添加 / 取消添加方块事件
2.3.1 欺骗活塞取消添加方块事件: 推动上限检测
方块事件也会出现活塞觉得自己要执行的事件与实际状况不一致的情况,最典型的便是推动上限检测。

推动上限检测机制最初由玩家_Kayleigh 和 Landmining 发现,其核心在于 "欺骗" 活塞。这里展示的是 Bright_Observer 制作的简化版本,便于分析理解。
该装置虽具有方向性设计,但这并不影响其检测功能,因此可自由选择建造方向。
运行现象:
- 放下紫色羊毛 (羊毛连接了一根很长很长的柱子...) 后敲击音符盒,普通活塞会启动
- 破坏紫色羊毛后敲击,普通活塞则保持不变
原理分析:
- 上推活塞在被更新时自检
- 系统检查推动上限条件
- 若羊毛放下,因上方玻璃柱子已超出推动上限,活塞不会添加方块事件
- 此时激活两个普通活塞
- 粘性活塞被普通活塞更新,自检后伸出
- 羊毛不存在时:
粘性活塞正常运作
上方普通活塞的推出状态取决于朝向
下方普通活塞因深度关系保持原位
关键发现:
粘性活塞的计划动作与实际执行时推动结构可能存在差异,这种特性被巧妙地用于检测机制。
2.3.2 欺骗活塞添加方块事件
遗憾的是,如果你试图复刻5.10.1 破基岩的布局和激活顺序
Footnotes
-
这里的
tryMove需要消歧义, 在 Java 中, 不同类的方法可以重名。这里的tryMove是PistonHandler.java中的方法, 不是PistonBlock.tryMove。 ↩






