红石系统全剖析#
红石是 Minecraft 的逻辑电路底座:一根红石线、一个中继器、一个比较器,背后是一整套「弱充能 / 强充能」的方块交互协议。本文从方块状态、信号计算、邻居更新三条主线,完整拆解红石系统的实现。最后还会讲到 1.21.11 引入、可通过实验性开关启用的全新红石控制器——它用双队列替换了传统的递归传播,让红石更新第一次变得确定性可复现。
一、组件总览#
红石系统由「信号源 + 传输 + 逻辑门 + 负载」四类方块组成:
| 类 | 角色 | 关键状态 / 行为 |
|---|---|---|
RedstoneWireBlock |
红石线 | POWER(0-15) + 四个方向 WIRE_CONNECTION |
RedstoneController |
红石线信号计算基类 | calculateWirePowerAt() 提供衰减算法 |
DefaultRedstoneController |
默认传播器 | 即时递归 + 7 格邻居广播 |
ExperimentalRedstoneController |
实验性传播器 | 双队列 + WireOrientation 确定性传播 |
WireOrientation |
更新朝向(1.21.11) | 48 种朝向,决定邻居更新顺序 |
AbstractRedstoneGateBlock |
红石门基类 | FACING + POWERED + 延迟调度 |
RepeaterBlock |
中继器 | DELAY(1-4)、LOCKED |
ComparatorBlock |
比较器 | MODE(COMPARE/SUBTRACT) + BE 输出电压 |
RedstoneTorchBlock |
红石火把 | LIT + 烧毁机制 |
RedstoneLampBlock |
红石灯 | LIT,熄灭有 4gt 延迟 |
RedstoneBlock |
红石块 | 恒定 15 级弱充能 |
LeverBlock / AbstractPressurePlateBlock |
拉杆 / 压力板 | 手动与实体触发的信号源 |
二、信号的两副面孔:弱充能与强充能#
理解红石的第一道坎,是把「一个方块发出了多少电」和「一个方块接收了多少电」分开。这套协议全部集中在 RedstoneView 这个接口里。
弱充能 vs 强充能#
BlockState 提供两个查询接口,语义完全不同:
protected int getWeakRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction);
protected int getStrongRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction);- 弱充能:直接供能,够点亮红石线、中继器、活塞等「敏感元件」。
- 强充能:除了直接供能,还会让实体方块自身变成导体,把电「透」到它相邻的红石线上。
判断一个方块是否被充能时,游戏并不是逐个查邻居的弱充能,而是先看它自己是不是导体:
default int getEmittedRedstonePower(BlockPos pos, Direction direction) {
BlockState blockState = this.getBlockState(pos);
int i = blockState.getWeakRedstonePower(this, pos, direction);
return blockState.isSolidBlock(this, pos)
? Math.max(i, this.getReceivedStrongRedstonePower(pos))
: i;
}即:实体方块会把「它周围传来的强充能」转发出去,这就是"用一块泥土把信号从火把传导到红石线"的原理。
方向参数的约定#
这里有一个极易读反的约定:getWeakRedstonePower(pos, direction) 里的 direction 是从接收方指向发射方。
验证方式是用红石火把。火把只对自己下方的方块提供强充能:
protected int getStrongRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return direction == Direction.DOWN ? state.getWeakRedstonePower(world, pos, direction) : 0;
}火把 T 上方有方块 B 时,B 的邻居检查调用的是 getStrongRedstonePower(T, DOWN)——DOWN 正是从 B 指向 T 的方向。所以 direction 描述的是「接收方看向发射方」的视线方向。
三、红石线:RedstoneWireBlock#
方块状态#
红石线有两个维度的状态:电平 POWER 和四个方向的连接形态 WIRE_CONNECTION。
public static final EnumProperty<WireConnection> WIRE_CONNECTION_NORTH = Properties.NORTH_WIRE_CONNECTION;
public static final IntProperty POWER = Properties.POWER;
// ...
private final RedstoneController redstoneController = new DefaultRedstoneController(this);
private boolean wiresGivePower = true;WireConnection 有三种取值:NONE(不连)、SIDE(平铺连接)、UP(上坡——爬墙连到高一格的红石线)。
连接判定#
连接形态由 getRenderConnectionType() 计算,核心是「目标方块是不是红石元件」:
protected static boolean connectsTo(BlockState state, @Nullable Direction dir) {
if (state.isOf(Blocks.REDSTONE_WIRE)) {
return true; // 红石线一律相连
} else if (state.isOf(Blocks.REPEATER)) {
Direction direction = state.get(RepeaterBlock.FACING);
return direction == dir || direction.getOpposite() == dir; // 中继器只认前后
} else {
return state.isOf(Blocks.OBSERVER)
? dir == state.get(ObserverBlock.FACING) // 观察者只认背面
: state.emitsRedstonePower() && dir != null; // 其它信号源
}
}- 中继器只有前后可以接红石线,这就是「中继器可以桥接、但侧面不导电」的根源。
- 观察者只把背面当作输入端,正面输出不连红石线,因此观察者天然隔离前后。
- 上方是实心方块时,红石线可以爬上高一格形成
UP连接,这就是楼梯式红石线能连着走的原因。
信号计算:calculateWirePowerAt()#
红石线的电平不是自己算出来的结果,而是取周围所有来源的最大值再减 1:
protected int calculateWirePowerAt(World world, BlockPos pos) {
int i = 0;
for (Direction direction : Direction.Type.HORIZONTAL) {
BlockPos blockPos = pos.offset(direction);
BlockState blockState = world.getBlockState(blockPos);
i = Math.max(i, this.getWirePowerAt(blockPos, blockState));
BlockPos blockPos2 = pos.up();
if (blockState.isSolidBlock(world, blockPos) && !world.getBlockState(blockPos2).isSolidBlock(world, blockPos2)) {
// 邻居是实心方块、且上方不是实心 → 看它上方那格的红石线(上坡)
BlockPos blockPos3 = blockPos.up();
i = Math.max(i, this.getWirePowerAt(blockPos3, world.getBlockState(blockPos3)));
} else if (!blockState.isSolidBlock(world, blockPos)) {
// 邻居不是实心 → 看它下方那格的红石线(下坡)
BlockPos blockPos4 = blockPos.down();
i = Math.max(i, this.getWirePowerAt(blockPos4, world.getBlockState(blockPos4)));
}
}
return Math.max(0, i - 1); // 每走一格衰减 1 级
}源码把「每格衰减 1 级」写得非常直白:最后一行 Math.max(0, i - 1)。这也解释了红石线的两个经典特性:
- 最大传输距离 15 格(从 15 递减到 0);
- 信号可以被"抬高":只要中间隔着实体方块,就能看上方/下方的红石线,所以爬楼梯的红石线仍然只算"一格"。
自身供电的开关:wiresGivePower#
计算强充能时会临时把红石线「闭嘴」,防止自己给自己充电形成死循环:
public int getStrongPower(World world, BlockPos pos) {
this.wiresGivePower = false; // 临时关闭自身供电
int i = world.getReceivedRedstonePower(pos); // 此时红石线不参与
this.wiresGivePower = true; // 恢复
return i;
}弱充能的边界条件#
红石线对外供能时,对方向极其挑剔:
protected int getWeakRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
if (this.wiresGivePower && direction != Direction.DOWN) {
int i = state.get(POWER);
if (i == 0) {
return 0;
} else {
return direction != Direction.UP
&& !this.getPlacementState(world, state, pos)
.get(DIRECTION_TO_WIRE_CONNECTION_PROPERTY.get(direction.getOpposite())).isConnected()
? 0
: i;
}
} else {
return 0;
}
}结合第二节的方向约定,可以读出三条规则:
direction == DOWN(接收方在红石线上方)→ 恒为 0:红石线不向头顶的方块供能。direction == UP(接收方在红石线下方)→ 恒为该线的POWER。- 水平方向 → 只有当红石线朝接收方连接时(检查
direction.getOpposite()对应的连接属性)才供能。
四、默认传播器:DefaultRedstoneController#
默认逻辑很短:算出新电平,若变化就写回方块状态,然后广播给「自身 + 六邻居」这 7 个位置。
public void update(World world, BlockPos pos, BlockState state, @Nullable WireOrientation orientation, boolean blockAdded) {
int i = this.calculateTotalPowerAt(world, pos);
if (state.get(RedstoneWireBlock.POWER) != i) {
if (world.getBlockState(pos) == state) {
world.setBlockState(pos, state.with(RedstoneWireBlock.POWER, i), Block.NOTIFY_LISTENERS);
}
Set<BlockPos> set = Sets.newHashSet();
set.add(pos);
for (Direction direction : Direction.values()) {
set.add(pos.offset(direction));
}
for (BlockPos blockPos : set) {
world.updateNeighbors(blockPos, this.wire);
}
}
}
private int calculateTotalPowerAt(World world, BlockPos pos) {
int i = this.getStrongPowerAt(world, pos);
return i == 15 ? i : Math.max(i, this.calculateWirePowerAt(world, pos));
}两个细节值得注意:
i == 15短路:外部强充能已经满级时,直接用 15,省去一次calculateWirePowerAt的邻居遍历——这是热路径上的性能优化。- 广播范围是 7 格:
{pos} ∪ 六邻居。它不会向「邻居的邻居」广播,这既是性能考虑,也是 BUD(方块更新检测器)能存在的原因(见第十二节)。
五、实验性红石:ExperimentalRedstoneController#
1.21.11 为红石线引入了第二套传播器,由实验性开关门控。分派点在 RedstoneWireBlock.update():
private void update(World world, BlockPos pos, BlockState state, @Nullable WireOrientation orientation, boolean blockAdded) {
if (areRedstoneExperimentsEnabled(world)) {
new ExperimentalRedstoneController(this).update(world, pos, state, orientation, blockAdded);
} else {
this.redstoneController.update(world, pos, state, orientation, blockAdded);
}
}
private static boolean areRedstoneExperimentsEnabled(World world) {
return world.getEnabledFeatures().contains(FeatureFlags.REDSTONE_EXPERIMENTS);
}解决的问题:确定性#
传统红石传播是深度优先递归:一个红石线更新触发邻居更新,邻居又触发下一层,传播顺序取决于递归调用栈与 Set 的迭代顺序,导致同一套电路在不同运行顺序下可能出现不同结果(社区里"更新顺序敏感"的红石机器就靠这个)。实验性实现改成显式队列的广度优先,顺序由 WireOrientation 唯一决定。
WireOrientation:为传播定义"朝向"#
WireOrientation 用 up / front / right / sideBias 四个量描述一次更新的「坐标系」,共 48 种取值:
private WireOrientation(Direction up, Direction front, WireOrientation.SideBias sideBias) {
// ...
Vec3i vec3i = front.getVector().crossProduct(up.getVector());
Direction direction = Direction.fromVector(vec3i, null);
this.right = this.sideBias == WireOrientation.SideBias.RIGHT ? direction : direction.getOpposite();
this.directionsByPriority = List.of(
this.front.getOpposite(), this.front, this.right, this.right.getOpposite(),
this.up.getOpposite(), this.up
);
this.horizontalDirections = /* 按 up 轴过滤 */;
this.verticalDirections = /* 按 up 轴过滤 */;
}directionsByPriority 是固定的六方向优先级序列(前反向 → 前 → 右 → 右反向 → 上反向 → 上)。传播器按这个序列遍历邻居,顺序就不再随机。
朝向从哪来?由 OrientationHelper 生成,只有开启实验才会返回非空值:
public static @Nullable WireOrientation getEmissionOrientation(World world, @Nullable Direction up, @Nullable Direction front) {
if (world.getEnabledFeatures().contains(FeatureFlags.REDSTONE_EXPERIMENTS)) {
WireOrientation wireOrientation = WireOrientation.random(world.random).withSideBias(WireOrientation.SideBias.LEFT);
if (front != null) wireOrientation = wireOrientation.withUp(front);
if (up != null) wireOrientation = wireOrientation.withFront(up);
return wireOrientation;
}
return null;
}注意这里用了
world.random——朝向的起点是随机的,但一旦确定,后续传播顺序就完全由它决定。这是"结果确定、但起点随机"的设计。
双队列传播#
实验性传播器用两个队列区分「电平上升」与「电平下降」,并记录了每根线的朝向与电平:
private final Deque<BlockPos> powerIncreaseQueue = new ArrayDeque<>();
private final Deque<BlockPos> powerDecreaseQueue = new ArrayDeque<>();
private final Object2IntMap<BlockPos> wireOrientationsAndPowers = new Object2IntLinkedOpenHashMap<>();主循环分三段:先把起点入队,再清空升压队列,最后清空降压队列:
private void propagatePowerUpdates(World world, BlockPos pos, WireOrientation orientation) {
// ... 起点入队 ...
while (!this.powerIncreaseQueue.isEmpty()) {
BlockPos blockPos = this.powerIncreaseQueue.removeFirst();
int i = this.wireOrientationsAndPowers.getInt(blockPos);
WireOrientation wireOrientation = unpackOrientation(i);
int j = unpackPower(i);
int k = this.getStrongPowerAt(world, blockPos);
int l = this.calculateWirePowerAt(world, blockPos);
int m = Math.max(k, l);
int n;
if (m < j) {
// 外部来源消失了,但这根线要"关掉",延迟到降压队列处理
if (k > 0 && !this.powerDecreaseQueue.contains(blockPos)) {
this.powerDecreaseQueue.add(blockPos);
}
n = 0;
} else {
n = m;
}
if (n != j) this.updatePowerAt(blockPos, n, wireOrientation);
this.spreadPowerUpdateToNeighbors(world, blockPos, n, wireOrientation, j > m);
}
while (!this.powerDecreaseQueue.isEmpty()) {
// 重新确认外部强充能是否恢复
BlockPos blockPos2 = this.powerDecreaseQueue.removeFirst();
// ...
if (t > q) {
this.updatePowerAt(blockPos2, t, wireOrientation2);
} else if (t < q) {
throw new IllegalStateException("Turning off wire while trying to turn it on. Should not happen.");
}
this.spreadPowerUpdateToNeighbors(world, blockPos2, t, wireOrientation2, false);
}
}- 升压:如果邻居线比当前电平低超过 1(
i < power - 1),说明它被"漏掉"了,丢进降压队列待复核;如果邻居线电平更高,丢进升压队列继续扩散。 - 降压:所有"可能要关掉"的线集中在这里重新确认外部强充能。那句
IllegalStateException是一条不变量断言——一旦出现"关掉时又想打开",说明传播逻辑自相矛盾,直接崩溃而不是静默出错。
朝向 + 电平打包成一个 int#
为了省内存,朝向与电平被塞进同一个整数:
private static int packOrientationAndPower(WireOrientation orientation, int power) {
return orientation.ordinal() << 4 | power; // 高 4 位以后放 ordinal,低 4 位放 0-15 电平
}
private static WireOrientation unpackOrientation(int packed) { return WireOrientation.fromOrdinal(packed >> 4); }
private static int unpackPower(int packed) { return packed & 15; }电平只有 4 bit(0-15),正好用 & 15 取出、<< 4 让位给 ordinal。
邻居广播也按朝向优先级#
默认传播器只广播 7 格;实验性传播器则会按 directionsByPriority 逐个方向通知,并对实体方块继续"透传":
private void update(World world) {
this.wireOrientationsAndPowers.forEach((pos, orientationAndPower) -> {
WireOrientation wireOrientation = unpackOrientation(orientationAndPower);
BlockState blockState = world.getBlockState(pos);
for (Direction direction : wireOrientation.getDirectionsByPriority()) {
if (canProvidePowerTo(blockState, direction)) {
BlockPos blockPos = pos.offset(direction);
BlockState blockState2 = world.getBlockState(blockPos);
WireOrientation wireOrientation2 = wireOrientation.withFrontIfNotUp(direction);
world.updateNeighbor(blockState2, blockPos, this.wire, wireOrientation2, false);
if (blockState2.isSolidBlock(world, blockPos)) {
for (Direction direction2 : wireOrientation2.getDirectionsByPriority()) {
if (direction2 != direction.getOpposite()) {
world.updateNeighbor(blockPos.offset(direction2), this.wire,
wireOrientation2.withFrontIfNotUp(direction2));
}
}
}
}
}
});
}六、红石门基类:AbstractRedstoneGateBlock#
中继器和比较器共享一个基类。它把「输入检测 → 延迟调度 → 输出」抽象成模板方法,子类只需要提供延迟和输入判定。
延迟调度:不是立即翻转#
门的核心是不能立刻改状态,必须调度一个方块 tick:
protected void updatePowered(World world, BlockPos pos, BlockState state) {
if (!this.isLocked(world, pos, state)) {
boolean bl = state.get(POWERED);
boolean bl2 = this.hasPower(world, pos, state);
if (bl != bl2 && !world.getBlockTickScheduler().isTicking(pos, this)) {
TickPriority tickPriority = TickPriority.HIGH;
if (this.isTargetNotAligned(world, pos, state)) {
tickPriority = TickPriority.EXTREMELY_HIGH;
} else if (bl) {
tickPriority = TickPriority.VERY_HIGH;
}
world.scheduleBlockTick(pos, this, this.getUpdateDelayInternal(state), tickPriority);
}
}
}TickPriority 的三档变化很讲究,它直接对应社区里的优先级技巧:
| 情形 | 优先级 | 说明 |
|---|---|---|
| 正常 | HIGH |
普通更新 |
| 目标门朝向不一致 | EXTREMELY_HIGH |
前方也是个门且朝向没对齐,抢先执行 |
| 正在熄灭 | VERY_HIGH |
熄灭比点亮更优先 |
输入与侧面输入#
protected int getPower(World world, BlockPos pos, BlockState state) {
Direction direction = state.get(FACING);
BlockPos blockPos = pos.offset(direction);
int i = world.getEmittedRedstonePower(blockPos, direction);
if (i >= 15) {
return i;
}
BlockState blockState = world.getBlockState(blockPos);
return Math.max(i, blockState.isOf(Blocks.REDSTONE_WIRE) ? blockState.get(RedstoneWireBlock.POWER) : 0);
}
protected int getMaxInputLevelSides(RedstoneView world, BlockPos pos, BlockState state) {
Direction direction = state.get(FACING);
Direction direction2 = direction.rotateYClockwise();
Direction direction3 = direction.rotateYCounterclockwise();
boolean bl = this.getSideInputFromGatesOnly();
return Math.max(
world.getEmittedRedstonePower(pos.offset(direction2), direction2, bl),
world.getEmittedRedstonePower(pos.offset(direction3), direction3, bl)
);
}- 主输入只看朝向那一格。
- 侧面输入看左右两侧;
getSideInputFromGatesOnly()决定侧面是不是「只认门」——中继器返回true,因此只有中继器/比较器能锁中继器。
七、中继器:RepeaterBlock#
中继器就是"带延迟 + 单向 + 可锁定"的红石门。
protected int getUpdateDelayInternal(BlockState state) {
return state.get(DELAY) * 2; // 档位 1-4,对应 2/4/6/8 gt
}
public boolean isLocked(WorldView world, BlockPos pos, BlockState state) {
return this.getMaxInputLevelSides(world, pos, state) > 0; // 侧面有信号 → 锁定
}
protected boolean getSideInputFromGatesOnly() {
return true; // 侧面只被中继器/比较器锁定
}要点:
- 延迟 =
DELAY × 2gt,从 1 档到 4 档分别是 2、4、6、8 gt。红石信号在红石线里是"瞬时"的(同一 tick 内递归传播),中继器才是真正的定时元件。 - 锁定判定只看"有没有信号"(
> 0),不看电平高低:侧面任何强度的中继器/比较器信号都会锁住它。 - 锁定状态在
getStateForNeighborUpdate()里持续更新,且当邻居方向与朝向同轴(前后)时才交给父类处理。
八、比较器:ComparatorBlock#
比较器比中继器复杂,因为它带一个持久化输出电压的 BlockEntity。
输出计算#
private int calculateOutputSignal(World world, BlockPos pos, BlockState state) {
int i = this.getPower(world, pos, state);
if (i == 0) {
return 0;
} else {
int j = this.getMaxInputLevelSides(world, pos, state);
if (j > i) {
return 0; // 侧面比后面强 → 完全关闭
} else {
return state.get(MODE) == ComparatorMode.SUBTRACT ? i - j : i;
}
}
}
protected boolean hasPower(World world, BlockPos pos, BlockState state) {
int i = this.getPower(world, pos, state);
if (i == 0) return false;
int j = this.getMaxInputLevelSides(world, pos, state);
return i > j ? true : i == j && state.get(MODE) == ComparatorMode.COMPARE;
}这就是比较器两种模式的精确规则:
| 模式 | 侧面 > 后面 | 侧面 = 后面 | 侧面 < 后面 |
|---|---|---|---|
| COMPARE | 关闭(0) | 输出后面电平 | 输出后面电平 |
| SUBTRACT | 关闭(0) | 输出 0 | 输出 后面 - 侧面 |
注意 COMPARE 模式在 i == j 时输出 i(而不是 0),这正是"比较器作比较器"与"作减法器"的分水岭。
输入可以来自容器和物品展示框#
ComparatorBlock 重写 getPower(),支持读取容器信号与展示框旋转角:
protected int getPower(World world, BlockPos pos, BlockState state) {
int i = super.getPower(world, pos, state);
Direction direction = state.get(FACING);
BlockPos blockPos = pos.offset(direction);
BlockState blockState = world.getBlockState(blockPos);
if (blockState.hasComparatorOutput()) {
i = blockState.getComparatorOutput(world, blockPos, direction.getOpposite()); // 箱子/漏斗等
} else if (i < 15 && blockState.isSolidBlock(world, blockPos)) {
// 隔着实体方块看下一格:物品展示框或容器
blockPos = blockPos.offset(direction);
blockState = world.getBlockState(blockPos);
ItemFrameEntity itemFrameEntity = this.getAttachedItemFrame(world, direction, blockPos);
int j = Math.max(itemFrameEntity == null ? Integer.MIN_VALUE : itemFrameEntity.getComparatorPower(),
blockState.hasComparatorOutput() ? blockState.getComparatorOutput(world, blockPos, direction.getOpposite()) : Integer.MIN_VALUE);
if (j != Integer.MIN_VALUE) i = j;
}
return i;
}输出写回 BE#
比较器不能只靠 POWERED 布尔状态,因为输出是 0-15 的连续值,必须存到 BlockEntity:
private void update(World world, BlockPos pos, BlockState state) {
int i = this.calculateOutputSignal(world, pos, state);
BlockEntity blockEntity = world.getBlockEntity(pos);
int j = 0;
if (blockEntity instanceof ComparatorBlockEntity comparatorBlockEntity) {
j = comparatorBlockEntity.getOutputSignal();
comparatorBlockEntity.setOutputSignal(i);
}
if (j != i || state.get(MODE) == ComparatorMode.COMPARE) {
boolean bl = this.hasPower(world, pos, state);
boolean bl2 = state.get(POWERED);
if (bl2 && !bl) world.setBlockState(pos, state.with(POWERED, false), Block.NOTIFY_LISTENERS);
else if (!bl2 && bl) world.setBlockState(pos, state.with(POWERED, true), Block.NOTIFY_LISTENERS);
this.updateTarget(world, pos, state);
}
}输出电平则由 getOutputLevel() 从 BE 读取,供 getWeakRedstonePower() 使用:
protected int getOutputLevel(BlockView world, BlockPos pos, BlockState state) {
BlockEntity blockEntity = world.getBlockEntity(pos);
return blockEntity instanceof ComparatorBlockEntity ? ((ComparatorBlockEntity)blockEntity).getOutputSignal() : 0;
}九、红石火把:RedstoneTorchBlock#
红石火把是"取反"元件:底座有电时熄灭,同时还会烧毁(过热宕机)。
取反与方向#
protected int getWeakRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return state.get(LIT) && Direction.UP != direction ? 15 : 0; // 向上不供能
}
protected int getStrongRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return direction == Direction.DOWN ? state.getWeakRedstonePower(world, pos, direction) : 0; // 只向下方强充能
}
protected boolean shouldUnpower(World world, BlockPos pos, BlockState state) {
return world.isEmittingRedstonePower(pos.down(), Direction.DOWN); // 底座被充能 → 该熄灭
}- 弱充能:除正上方外全 15(这就是火把能给侧面红石线、但不能点亮头顶方块的原因)。
- 强充能:只向下。所以在方块下方放火把,能照亮它上方的方块;但火把不能"向上顶"。
延迟 2gt#
protected void neighborUpdate(BlockState state, World world, BlockPos pos, Block sourceBlock, @Nullable WireOrientation wireOrientation, boolean notify) {
if (state.get(LIT) == this.shouldUnpower(world, pos, state) && !world.getBlockTickScheduler().isTicking(pos, this)) {
world.scheduleBlockTick(pos, this, 2); // 状态与期望不符 → 2gt 后处理
}
}条件 LIT == shouldUnpower 的含义是「当前状态与期望状态矛盾」:点着但该灭、或灭着但该亮。这就是红石火把延迟 1~2gt 的原因,也是"火把时钟"能起振的基础。
烧毁机制#
火把在 60gt 内熄灭 8 次就会烧毁,进入 160gt 的冷却:
private static final int SCHEDULED_TICK_DELAY = 2; // 见上方邻居更新
// 常量:60L / 8 / 160L
protected void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {
boolean bl = this.shouldUnpower(world, pos, state);
List<RedstoneTorchBlock.BurnoutEntry> list = BURNOUT_MAP.get(world);
while (list != null && !list.isEmpty() && world.getTime() - list.get(0).time > 60L) {
list.remove(0); // 清掉 60gt 之前的记录
}
if (state.get(LIT)) {
if (bl) {
world.setBlockState(pos, state.with(LIT, false), Block.NOTIFY_ALL);
if (isBurnedOut(world, pos, true)) {
world.syncWorldEvent(WorldEvents.REDSTONE_TORCH_BURNS_OUT, pos, 0);
world.scheduleBlockTick(pos, world.getBlockState(pos).getBlock(), 160); // 冷却 160gt
}
}
} else if (!bl && !isBurnedOut(world, pos, false)) {
world.setBlockState(pos, state.with(LIT, true), Block.NOTIFY_ALL);
}
}
private static boolean isBurnedOut(World world, BlockPos pos, boolean addNew) {
List<RedstoneTorchBlock.BurnoutEntry> list = BURNOUT_MAP.computeIfAbsent(world, worldx -> Lists.newArrayList());
if (addNew) list.add(new RedstoneTorchBlock.BurnoutEntry(pos.toImmutable(), world.getTime()));
int i = 0;
for (RedstoneTorchBlock.BurnoutEntry burnoutEntry : list) {
if (burnoutEntry.pos.equals(pos) && ++i >= 8) return true; // 60gt 内第 8 次 → 烧毁
}
return false;
}烧毁记录用 WeakHashMap<BlockView, List<BurnoutEntry>> 按世界存储,条目随时间自然淘汰。这解释了"高频火把时钟用一会儿就不抖了"的现象。
十、信号源组件#
红石块:恒定 15#
protected boolean emitsRedstonePower(BlockState state) { return true; }
protected int getWeakRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return 15;
}没有状态,永远 15 级弱充能——最简单的信号源。
拉杆:弱充能全方位、强充能只朝附着面#
protected int getWeakRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return state.get(POWERED) ? 15 : 0;
}
protected int getStrongRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return state.get(POWERED) && getDirection(state) == direction ? 15 : 0;
}拉杆的强充能只沿它附着的方向,所以它能"强充能"背后的那个方块,把信号透到墙里。
压力板:强充能只向上#
protected int getWeakRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return this.getRedstoneOutput(state);
}
protected int getStrongRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) {
return direction == Direction.UP ? this.getRedstoneOutput(state) : 0;
}压力板在实体踩踏时通过 onEntityCollision → updatePlateState 改变输出,输出值随实体数量变化(木压力板线性、石压力板恒 15)。抬起后靠 scheduleBlockTick(..., getTickRate())(默认 20gt)复位。
十一、红石灯:熄灭有延迟#
红石灯是"点亮立即、熄灭延迟 4gt"的负载:
protected void neighborUpdate(BlockState state, World world, BlockPos pos, Block sourceBlock, @Nullable WireOrientation wireOrientation, boolean notify) {
if (!world.isClient()) {
boolean bl = state.get(LIT);
if (bl != world.isReceivingRedstonePower(pos)) {
if (bl) {
world.scheduleBlockTick(pos, this, 4); // 熄灭:延迟 4gt
} else {
world.setBlockState(pos, state.cycle(LIT), Block.NOTIFY_LISTENERS); // 点亮:立即
}
}
}
}
protected void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random random) {
if (state.get(LIT) && !world.isReceivingRedstonePower(pos)) {
world.setBlockState(pos, state.cycle(LIT), Block.NOTIFY_LISTENERS);
}
}这个 4gt 延迟是纯粹的手感设计:避免快速脉冲下灯疯狂闪烁。它也让红石灯成为天然的"脉冲展宽器"。
十二、BUD 与更新顺序#
邻居更新的固定顺序#
服务端广播邻居更新时使用一个固定数组:
public interface NeighborUpdater {
Direction[] UPDATE_ORDER = new Direction[]{
Direction.WEST, Direction.EAST, Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH
};
}无论默认还是实验性传播器,最终都通过这个顺序逐个 updateNeighbor。顺序固定 + 每个位置只通知一次,是红石行为可复现的前提。
为什么会有 BUD#
默认传播器只广播 {pos} ∪ 六邻居 这 7 个位置(第四节)。而红石线的"连接形态"取决于邻居的邻居(比如楼梯连接要看实体方块上方那格)。当世界被外部因素改动(如活塞推动、方块替换),这些"间接位置"如果不被 updateNeighborsAlways/updateNeighborsExcept 覆盖到,红石线就可能保留一个该变但没变的状态——这就是 BUD 的物理基础。
实验性传播器则通过 WireOrientation 的六方向优先级 + 对实体方块的"透传",把间接位置也纳入更新范围,从设计上压缩了 BUD 的生存空间。
十三、常见技巧的源码解释#
| 技巧 | 原理 |
|---|---|
| 红石线最大 15 格 | calculateWirePowerAt() 末尾 Math.max(0, i - 1),每格衰减 1 级 |
| 楼梯红石线只算一格 | 邻居是实体方块时改看其上方/下方的红石线,绕过实体方块不算距离 |
| 中继器锁存器 | isLocked() = 侧面有信号;getSideInputFromGatesOnly()=true,只有门能锁 |
| 比较器作减法器 | SUBTRACT 模式输出 后面 - 侧面;COMPARE 模式下 侧面 ≥ 后面 即关闭 |
| 比较器读箱子 | getPower() 走 hasComparatorOutput() / getComparatorOutput(),与红石线电平取最大 |
| 火把时钟会停 | 60gt 内熄灭 8 次触发烧毁,冷却 160gt |
| 火把延迟 1~2gt | neighborUpdate 里状态与期望矛盾时 scheduleBlockTick(pos, this, 2) |
| 灯不闪/脉冲展宽 | 点亮立即、熄灭 scheduleBlockTick(..., 4) |
| 红石线不点亮头顶方块 | getWeakRedstonePower 对 direction == DOWN 恒返回 0 |
| 活塞/观察者的 BUD | 传播器只广播 7 格,间接位置不被覆盖,详见 活塞系统全剖析 第二节 |
| 更新顺序敏感机器(1.21.11 实验前) | 默认传播器为递归顺序,Set 迭代顺序影响结果;实验性传播器改为 WireOrientation 确定性顺序 |
以上就是红石系统从方块状态、信号计算到两条传播路径(默认递归 / 实验性双队列)的完整拆解。红石看似"玄学",本质上是弱/强充能协议加一套邻居更新顺序;理解了这两者,绝大多数"更新顺序技巧"都能在源码里找到出处。