红石系统全剖析#

红石是 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;
    }
}

结合第二节的方向约定,可以读出三条规则:

  1. direction == DOWN(接收方在红石线上方)→ 恒为 0:红石线不向头顶的方块供能。
  2. direction == UP(接收方在红石线下方)→ 恒为该线的 POWER。
  3. 水平方向 → 只有当红石线朝接收方连接时(检查 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 × 2 gt,从 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 确定性顺序

以上就是红石系统从方块状态、信号计算到两条传播路径(默认递归 / 实验性双队列)的完整拆解。红石看似"玄学",本质上是弱/强充能协议加一套邻居更新顺序;理解了这两者,绝大多数"更新顺序技巧"都能在源码里找到出处。