02 方块的放置、改变与破坏
在地上放置一个拉杆,然后拉下它。
第一次操作让空气变成了拉杆;第二次操作没有改变方块种类,只把拉杆的状态从关闭改成了开启。虽然两次变化并不相同,但它们最终都需要完成同一件事:把一个新的方块状态写入世界。
本篇关注写入前后发生了什么。NC 更新、PP 更新及其顺序将在方块更新章节中详细讨论。
1 改变方块状态的共同入口
在 1.20.1 中,大量方块变化最终会进入:
它接收三个关键信息:
pos:改变哪组坐标;state:写入什么方块状态;flags:写入后需要通知哪些系统。
setBlockState 首先检查高度限制,然后取得该坐标所属的 WorldChunk,调用 WorldChunk#setBlockState 真正修改区块数据。
因此,所有方块变化遵循严格的三步模型:
2 放置方块
玩家右键放置普通方块时,主要入口是 BlockItem#place。
2.1 确定位置和状态
游戏不会拿着默认状态直接写入世界。它会:
- 检查该方块在当前功能集中是否启用;
- 检查玩家是否能够在目标坐标放置;
- 建立
ItemPlacementContext; - 调用方块的
getPlacementState计算初始状态; - 检查该状态能否存在,以及是否与实体碰撞。
楼梯会在这里确定朝向、上下半部和含水;活塞会根据玩家视线确定朝向;火把则会检查依附位置。
所以,“放下一个方块” 并不是先写入默认状态、再慢慢调整,而是先计算出适合当前环境的状态,再尝试写入。
2.2 写入区块
BlockItem#place 默认使用:
调用世界的 setBlockState。
区块会根据方块坐标找到子区块及子区块内坐标,用新状态替换调色板容器中的旧状态。替换成功后,它还会:
- 更新相关高度图;
- 在需要时安排光照检查;
- 调用旧状态的
onStateReplaced; - 调用新状态的
onBlockAdded; - 创建、移除或更新方块实体;
- 将区块标记为需要保存。
这里的 “旧状态” 通常是空气,但放置操作也可能替换草、雪层、流体或其他可替换方块。
2.3 放置完成后的行为
状态成功写入后,BlockItem#place 还会:
- 应用物品携带的
BlockStateTag; - 将物品中的方块实体 NBT 写入新方块实体;
- 调用方块的
onPlaced; - 触发放置方块进度条件;
- 播放放置声音;
- 发出
BLOCK_PLACE游戏事件; - 在非创造模式下消耗一个物品。
这些操作不是方块状态本身的一部分。状态写入失败时,后面的放置完成逻辑也不会执行。
3 改变已有方块的状态
拉杆、按钮、活板门等方块在交互时,通常会从旧状态取得一个新状态,再调用 setBlockState 写回原位置。
例如可以把变化抽象成:
方块种类仍然是拉杆,但子区块中的 BlockState 已经换成另一个状态。因此客户端显示、NC 更新和保存标记仍然可能发生。
flags 决定写入后还需要做什么:
NOTIFY_LISTENERS:通知监听者,服务端通常借此同步客户端;NOTIFY_NEIGHBORS:发出 NC 更新,并在需要时更新比较器;- 未设置
FORCE_STATE:继续执行由旧、新状态产生的 PP 更新链; NO_REDRAW、REDRAW_ON_MAIN_THREAD等标记控制客户端重绘行为。
关于这些标记和更新顺序,参见更新的概念与不同类型的更新。
4 方块实体的变化
区块写入状态时会比较旧、新状态是否需要方块实体:
- 旧方块实体不再适用时,将其移除;
- 新状态需要方块实体而当前位置没有时,通过
BlockEntityProvider#createBlockEntity创建; - 方块实体仍然适用时,更新它缓存的方块状态和 ticker。
这说明方块实体不能脱离方块状态独立存在。某个位置是否应该拥有方块实体,首先由该位置的方块状态决定。
5 玩家破坏方块
玩家挖掘方块的入口是 ServerPlayerInteractionManager#tryBreakBlock。
服务端会依次处理:
- 工具是否允许挖掘;
- 权限、冒险模式和操作员方块限制;
- 调用方块的
onBreak; - 调用
World#removeBlock,把当前坐标替换为原流体状态对应的方块状态; - 成功后调用
onBroken; - 非创造模式下损耗工具;
- 满足采集条件时调用
afterBreak生成掉落物和经验。
这里有一个容易忽略的细节:破坏含水方块后,目标坐标不一定变成空气。World#removeBlock 会先取得该坐标的 FluidState,再写入该流体对应的方块状态。因此,破坏含水方块后留下水并不是额外补放了一格水,而是移除方块时选择的替代状态。
World#breakBlock 是另一个通用破坏入口,供指令、更新或其他游戏逻辑使用。它可以直接处理破坏效果、掉落物、状态替换和 BLOCK_DESTROY 游戏事件。不要把它和玩家挖掘入口视为完全相同的调用过程。
6 写入之后为什么会发生一连串变化
一个状态被写入区块,只完成了 “这组坐标现在是什么” 的修改。为了让周围方块与这个结果保持一致,游戏还可能需要:
- 通知客户端;
- 重新检查光照;
- 更新高度图;
- 发出 NC 更新;
- 让相邻方块重新计算自己的状态;
- 更新比较器;
- 创建或移除方块实体;
- 将区块标记为需要保存。
这正是方块更新理论的出发点:一次局部状态写入,可能沿方块之间的关系继续传播。
本篇只负责找到传播的起点。传播范围、方向和顺序将在后续章节中继续展开。
7 小结
- 大量方块变化最终通过
World#setBlockState写入世界。 - 放置前先计算初始状态并检查能否放置。
- 真正的方块状态保存在子区块中。
- 写入时会处理高度图、光照、旧新方块回调和方块实体。
flags控制客户端通知、NC 更新和 PP 更新等后续行为。- 改变同一种方块的属性仍然是一次世界数据写入。
- 玩家破坏与通用
World#breakBlock使用不同入口。 - 移除含水方块时,替代状态可以是流体而非空气。
8 源码分析
8.1 setBlockState:所有变化的共同入口
World#setBlockState 是方块状态写入维度的核心方法:
三步走:先写区块,再按 flags 决定通知哪些系统,最后执行 PP 更新链。maxUpdateDepth 限制了 PP 更新的传播深度(默认 512)。
8.2 WorldChunk.setBlockState:真正写入数据的地方
注意第 5~7 步的防御性检查:onStateReplaced 回调中可能触发递归的 setBlockState,因此第 6 步重新读区块确认当前状态是否仍与新方块一致。若不一致,方法提前返回 null,外层的 World#setBlockState 也会返回 false。
8.3 BlockItem.place:放置方块的完整流程
这里的第 3 步确认了 "放置时先计算合适的状态,再写入"。第 4 步内部的 place 方法默认使用 NOTIFY_ALL | REDRAW_ON_MAIN_THREAD 调用 setBlockState。第 5 步的 placeFromNbt 和 onPlaced 是写入成功后才执行的——写入失败则整个放置流程中止,后续操作全部不执行。
8.4 World.removeBlock:移除方块,用流体替代
这就是破坏含水方块后留下水的机制来源:移除方块时,替代状态是 "该坐标的流体对应的方块状态",而非固定的空气。
8.5 World.breakBlock:通用破坏入口
与 removeBlock 不同,breakBlock 多了一步:在替换方块状态前,先处理掉落物。
8.6 ServerPlayerInteractionManager.tryBreakBlock:玩家破坏入口
流程清晰:权限检查 → onBreak → removeBlock(内部调用 setBlockState)→ onBroken → 工具损耗 → 掉落物。注意 removeBlock(false) 传入 move=false,所以 flag 中没有 MOVED。
8.7 参考类列表
net.minecraft.world.Worldnet.minecraft.world.chunk.WorldChunknet.minecraft.world.chunk.ChunkSectionnet.minecraft.item.BlockItemnet.minecraft.block.Blocknet.minecraft.block.BlockEntityProvidernet.minecraft.server.network.ServerPlayerInteractionManager
