方块系统与方块状态#
在 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 |
开关状态 | EXTENDED、WATERLOGGED、LIT |
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 都有唯一的 stateId(Block.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 动画"流程。