方块系统与方块状态#

在 Minecraft 中,“方块"不仅仅是一个 ID。每种方块都是一个 Java 类实例,而世界中每一个"格子"的状态,由一个轻量级的 BlockState 对象表示。

Block vs BlockState:核心区分#

这是理解 MC 方块系统最重要的概念:

概念 含义 数量级 内存
Block(方块类) 方块的类型定义(类定义 + 单例实例),如 Blocks.OAK_PLANKS ~1000 个(全局唯一) 较大,含行为逻辑
BlockState(方块状态) 方块的具体状态实例,如"朝向=东、伸出=true"的活塞 ~20000 种(全局共享不可变实例) 极小,仅含属性值
ID(BlockPos 处存储的值) 区块存储的数字 ID,映射到 BlockState ~20000 个可能值 2~4 字节(调色板压缩)

关键: 世界中不存储 Block 对象,甚至不直接存 BlockState 引用,只存一个压缩过的整数 ID,通过调色板映射到 BlockState。

属性 (Property) 系统#

方块通过 Property<T> 描述其可变状态。常见的属性有:

属性类型 典型用途 例子
DirectionProperty (FACING) 朝向 活塞、楼梯、发射器
BooleanProperty 开关状态 EXTENDEDWATERLOGGEDLIT
IntProperty 阶段/数值 AGE_0_7(作物)、LEVEL_0_15(红石信号)
EnumProperty 枚举 PistonType(粘性/普通)、SlabType(上/下/双)

在 Block 类中注册属性#

每个 Block 子类的构造器末尾会调用 appendProperties(),声明该方块支持哪些属性:

// 以 PistonBlock 为例
public class PistonBlock extends FacingBlock {
    public static final BooleanProperty EXTENDED = Properties.EXTENDED;

    public PistonBlock(boolean sticky, Settings settings) {
        super(settings);
        // 设置默认状态:朝北、未伸出
        this.setDefaultState(this.stateManager.getDefaultState()
            .with(FACING, Direction.NORTH)
            .with(EXTENDED, false));
    }

    // 注册属性到状态管理器
    @Override
    protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
        builder.add(FACING, EXTENDED);  // 2 种属性
        // 组合数 = 6 方向 × 2 布尔 = 12 种状态
    }
}

StateManager 会自动生成属性的笛卡尔积所有可能组合,每种组合对应一个不可变的 BlockState 单例。

不可变的 BlockState#

BlockState不可变对象(类似 Java Record)。要改变属性值,必须调用 with() 返回一个新的 BlockState 实例

BlockState state = Blocks.PISTON.getDefaultState();
// 错误!state 本身不会变
state.with(FACING, Direction.EAST);

// 正确:用返回值替换
state = state.with(FACING, Direction.EAST);
state = state.with(EXTENDED, true);
// 现在 state 代表"朝东、伸出"的活塞

这种设计让 BlockState 可以安全地全局共享、按引用比较、做 HashMap 的 key

方块状态 ID 与调色板 (Palette)#

为了节省内存,区块不直接存 BlockState 对象,而是存 stateId

block → (属性排列) → 所有 BlockState 实例列表 → stateId

每个 BlockState 都有唯一的 stateIdBlock.STATE_IDS 注册表)。世界通过一个多层调色板将少量 ID 映射到真实的 BlockState:

  • 单值调色板:整个区块只有同一种方块,存 0 字节
  • 线性调色板:方块种类少时,存线性索引
  • HashMap 调色板:方块种类多时,存映射
  • 全局调色板:方块种类超多时,直接存 stateId

这解释了为什么单一方块组成的超平坦世界内存极小

设置方块状态:World.setBlockState()#

这是修改世界中方块的唯一入口。它的第三个参数 flags(位掩码)极其重要,决定了更新行为:

world.setBlockState(pos, state, Block.NOTIFY_ALL);

常见 flags 组合:

Flag 常量 含义
NOTIFY_NEIGHBORS 1 更新 6 邻居方块
NOTIFY_LISTENERS 2 向客户端发送更新、触发监听器
NO_REDRAW 4 客户端不重绘
NOTIFY_NEIGHBORS_LISTENERS 3 最常用:1+2
NOTIFY_ALL 11 常用:1+2+8(完整通知)
MOVED 16 标记为活塞移动,跳过某些检查
SKIP_REDRAW_AND_BLOCK_ENTITY_REPLACED_CALLBACK 64 活塞动画常用,客户端不重绘
FORCE_STATE 128 强制写入,跳过部分校验

更新调用链(伪代码)#

setBlockState(pos, state, flags)
  ├─ 写入区块 chunk.setBlockState()
  ├─ 移除/迁移方块实体 (BlockEntity)
  ├─ if (flags & NOTIFY_LISTENERS)
  │   └─ 向玩家发送数据包(客户端更新显示)
  ├─ 调用 onBlockAdded / onStateReplaced 回调
  └─ if (flags & NOTIFY_NEIGHBORS)
      └─ updateNeighbors(pos)  // 触发 6 邻居的 neighborUpdate
          └─ 每个邻居调用 getStateForNeighborUpdate()
              └─ 可能再次 setBlockState...(连锁更新)

方块实体 (BlockEntity)#

有状态的方块(箱子、熔炉、活塞移动中)会额外关联一个 BlockEntity

BlockState(不变,描述"是什么")
   +
BlockEntity(可变,存储"内部数据")
   =
世界中一个完整的方块

典型的 BlockEntity 数据:

  • 箱子:物品库存
  • 熔炉:燃烧时间、食谱进度
  • 活塞(MOVING_PISTON):动画进度、被推动的原方块
  • 刷怪笼:刷新类型、冷却

规则:任何 BlockState.hasBlockEntity() == true 的方块,不能被活塞推动。这是推动前 isMovable() 检查的最后一道关卡。

方块行为方法一览#

所有方块的逻辑都通过 Block 类的回调方法注入。最重要的几个:

方法 触发时机 用途
canPlaceAt() 放置时 + 邻居更新时 能否站在下面的方块上(树苗、花草掉落检测)
getStateForNeighborUpdate() 任何邻居变化时 响应邻接变化(如水变为方块)
neighborUpdate() 显式邻居通知 检测红石信号(活塞、中继器)
randomTick() 每区块随机 tick(~68 秒一次) 作物生长、树苗生长、树叶消失
scheduledTick() 通过 tickScheduler 安排的定时 tick 中继器延时、方块下落、流体流动
onUse() 玩家右键点击 打开 GUI、使用按钮
onSyncedBlockEvent() 方块事件(客户端同步) 活塞动画、箱子开合音效

一个完整例子:活塞伸出#

看看方块系统如何协作完成一次活塞伸出:

玩家激活拉杆
  └─ 红石线更新 neighborUpdate() 到活塞
        └─ PistonBlock.tryMove() 判断应该伸出
              ├─ PistonHandler.calculatePush() 计算推动列表(最多12个方块)
              │   └─ 对每块调用 BlockState.getPistonBehavior() 检查可动性
              └─ addSyncedBlockEvent() 发出方块事件(同刻稍后执行)

【稍后来到 processSyncedBlockEvents 阶段】
  └─ PistonBlock.onSyncedBlockEvent(type=0)
        ├─ PistonBlock.move(extend=true)
        │   ├─ 对被推动方块:setBlockState(MOVING_PISTON, MOVED|SKIP_REDRAW...)
        │   ├─ 为每个 MOVING_PISTON 添加 PistonBlockEntity
        │   └─ 活塞本身:setBlockState(EXTENDED=true, NOTIFY_ALL|MOVED)
        └─ 播放 extend 音效、触发 GameEvent

【接下来 2 tick 的 blockEntity tick】
  └─ PistonBlockEntity.tick() 每 tick 推进 progress += 0.5
        ├─ 第 1 tick:progress = 0.5
        └─ 第 2 tick:progress = 1.0,放置最终方块,移除 BE

这就是一个完整的"方块状态 → 属性 → 邻居更新 → 方块事件 → BlockEntity 动画"流程。