04 方块的放置、改变与破坏
在地上放置一个拉杆,然后拉下它。
第一次操作让空气变成了拉杆;第二次操作没有改变方块种类,只把拉杆的状态从关闭改成了开启。虽然两次变化并不相同,但它们最终都需要完成同一件事:把一个新的方块状态写入维度。
本篇关注写入前后发生了什么。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 使用不同入口。
- 移除含水方块时,替代状态可以是流体而非空气。
ADVANCED
8 源码分析ADVANCED
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.World
net.minecraft.world.chunk.WorldChunk
net.minecraft.world.chunk.ChunkSection
net.minecraft.item.BlockItem
net.minecraft.block.Block
net.minecraft.block.BlockEntityProvider
net.minecraft.server.network.ServerPlayerInteractionManager
if (bl && state.isAir()) return null; // 空子区块写空气 → 跳过
// 1. 写入子区块的调色板容器
int j = pos.getX() & 15;
int k = i & 15;
int l = pos.getZ() & 15;
BlockState blockState = chunkSection.setBlockState(j, k, l, state);
if (blockState == state) return null;
// 2. 更新四种高度图
this.heightmaps.get(Heightmap.Type.MOTION_BLOCKING).trackUpdate(j, i, l, state);
this.heightmaps.get(Heightmap.Type.MOTION_BLOCKING_NO_LEAVES).trackUpdate(j, i, l, state);
this.heightmaps.get(Heightmap.Type.OCEAN_FLOOR).trackUpdate(j, i, l, state);
this.heightmaps.get(Heightmap.Type.WORLD_SURFACE).trackUpdate(j, i, l, state);
// 3. 子区块空/非空状态变化时,更新光照引擎
boolean bl2 = chunkSection.isEmpty();
if (bl != bl2) {
this.world.getChunkManager().getLightingProvider().setSectionStatus(pos, bl2);
}
// 4. 需要光照更新时安排光照检查
if (ChunkLightProvider.needsLightUpdate(this, pos, blockState, state)) {
this.world.getChunkManager().getLightingProvider().checkBlock(pos);
}
// 5. 调用旧状态的 onStateReplaced
if (!this.world.isClient) {
blockState.onStateReplaced(this.world, pos, state, moved);
} else if (!blockState.isOf(block) && blockState.hasBlockEntity()) {
this.removeBlockEntity(pos); // 客户端:旧方块实体不再适用则移除
}
// 6. 新方块不一致时终止(防止被 onStateReplaced 中的递归调用覆盖)
if (!chunkSection.getBlockState(j, k, l).isOf(block)) return null;
// 7. 调用新状态的 onBlockAdded
if (!this.world.isClient) {
state.onBlockAdded(this.world, pos, blockState, moved);
}
// 8. 维护方块实体
if (state.hasBlockEntity()) {
BlockEntity blockEntity = this.getBlockEntity(pos, WorldChunk.CreationType.CHECK);
if (blockEntity == null) {
blockEntity = ((BlockEntityProvider) block).createBlockEntity(pos, state);
if (blockEntity != null) this.addBlockEntity(blockEntity);
} else {
blockEntity.setCachedState(state); // 更新缓存状态
this.updateTicker(blockEntity); // 更新 ticker
}
}
this.needsSaving = true; // 标记区块需要保存
return blockState;
}
context
.
getWorld
()
.
getEnabledFeatures
()))
return
FAIL;
// 2. 检查玩家能否在此处放置
if (!context.canPlace()) return FAIL;
// 3. 计算初始状态
BlockState blockState = this.getPlacementState(itemPlacementContext);
if (blockState == null) return FAIL;
// 4. 写入维度
if (!this.place(itemPlacementContext, blockState)) return FAIL;
// 5. 写入成功后的后续操作
BlockState blockState2 = world.getBlockState(blockPos);
if (blockState2.isOf(blockState.getBlock())) {
blockState2 = this.placeFromNbt(blockPos, world, itemStack, blockState2);
this.postPlacement(blockPos, world, playerEntity, itemStack, blockState2);
blockState2.getBlock().onPlaced(world, blockPos, blockState2, playerEntity, itemStack);
// 触发进度
if (playerEntity instanceof ServerPlayerEntity) {
Criteria.PLACED_BLOCK.trigger((ServerPlayerEntity) playerEntity, blockPos, itemStack);
}
}
// 6. 播放声音和游戏事件
world.playSound(playerEntity, blockPos, getPlaceSound(blockState2), SoundCategory.BLOCKS, ...);
world.emitGameEvent(GameEvent.BLOCK_PLACE, blockPos, ...);
// 7. 非创造模式消耗物品
if (playerEntity == null || !playerEntity.getAbilities().creativeMode) {
itemStack.decrement(1);
}
return ActionResult.success(world.isClient);
}
|
(move
?
Block
.
MOVED
:
0
));
}
.
getBlockState
(pos);
if (blockState.isAir()) return false;
FluidState fluidState = this.getFluidState(pos);
if (!(blockState.getBlock() instanceof AbstractFireBlock)) {
this.syncWorldEvent(WorldEvents.BLOCK_BROKEN, pos, Block.getRawIdFromState(blockState));
}
// 需要掉落物时生成掉落
if (drop) {
BlockEntity blockEntity = blockState.hasBlockEntity()
? this.getBlockEntity(pos) : null;
Block.dropStacks(blockState, this, pos, blockEntity, breakingEntity, ItemStack.EMPTY);
}
// 用流体状态替换方块状态
boolean bl = this.setBlockState(pos, fluidState.getBlockState(),
Block.NOTIFY_ALL, maxUpdateDepth);
if (bl) {
this.emitGameEvent(GameEvent.BLOCK_DESTROY, pos, ...);
}
return bl;
}
!
this
.
player
.
getMainHandStack
()
.
getItem
()
.
canMine
(blockState
,
this
.
world
,
pos
,
this
.
player
))
return false;
// 2. 操作员方块权限检查
Block block = blockState.getBlock();
if (block instanceof OperatorBlock && !this.player.isCreativeLevelTwoOp()) { ... return false; }
// 3. 冒险模式限制
if (this.player.isBlockBreakingRestricted(this.world, pos, this.gameMode)) return false;
// 4. 调用 onBreak
block.onBreak(this.world, pos, blockState, this.player);
// 5. 移除方块(→ removeBlock → setBlockState)
boolean bl = this.world.removeBlock(pos, false);
// 6. 调用 onBroken
if (bl) block.onBroken(this.world, pos, blockState);
// 7. 非创造模式:损耗工具
ItemStack itemStack = this.player.getMainHandStack();
ItemStack itemStack2 = itemStack.copy();
itemStack.postMine(this.world, blockState, pos, this.player);
// 8. 可采集时生成掉落物和经验
boolean bl2 = this.player.canHarvest(blockState);
if (bl && bl2) {
block.afterBreak(this.world, this.player, pos, blockState, blockEntity, itemStack2);
}
return true;
}
04 方块的放置、改变与破坏 — Graduate Texts in Minecraft