04 方块实体
1 方块实体的概念与内容
方块本身通过预定义的有限 BlockState 保存数据;通过默认的方块渲染行为渲染模型;通过更新系统、计划系统等决定执行逻辑。
但是,这样的模式具有局限性, 无法直接满足在游戏交互中一些方块的功能。
方块实体为此而生。它为普通方块提供了三个重要的额外功能:
- 使用 NBT 存储数据
- 自定义的渲染行为
- 在每一次 tick 中进行更新
1.1 方块实体的内容
方块实体是因方块而异的,但一个抽象的,基本的方块实体含有以下基本信息与功能:
- type 类型
- world 所在的世界
- pos 位置
- removed 是否已被移除
- cachedState 缓存的,对应方块的状态
1.2 方块实体与方块
方块实体与世界中的方块绑定。一个世界里,每个方块位置仅可有一个方块实体实例。
仅有经过特殊声明的方块可以合法地与指定种类的方块实体共存。
方块实体随着方块一同添加与移除。但通过更新抑制等特殊手段可以在方块移除时保留其方块实体。
2 重要的带有方块实体的方块
2.1 漏斗
在 Minecraft 中,漏斗是一种重要的方块实体,主要用于物品的自动化传输。
2.1.1 漏斗方块实体携带的信息
一个可爱的漏斗方块实体携带以下基本信息:
inventory储存空间(5 格)transferCooldown冷却(默认为 - 1)lastTickTime上一 tick 的世界时间facing朝向
2.1.2 漏斗方块实体的功能
- 将物品传输到目标容器
- 从容器中吸取物品
- 尝试吸取物品实体
2.1.3 漏斗方块实体的工作流程
每个游戏刻(tick),漏斗都会执行以下步骤:
-
减少冷却时间(cd, Cooldown)
- 若 cd 仍大于 0,漏斗不会执行任何拉取或输出操作,而是直接结束运算。
- 若 cd 归零,漏斗会开始尝试执行输出和拉取操作。
-
执行物品传输逻辑
- 首先尝试输出(将物品放入目标容器)
- 随后尝试拉取(从上方容器吸取物品)
- 如果吸取或输出成功,则漏斗会重置冷却时间,并同步更新自身数据。
现在,让我们移步到尝试拉取和输出的具体的逻辑。
当漏斗确定自己需要做些什么时,它会先尝试向指向的容器输出。随后,它会尝试从上方吸取物品。最后,如果吸取和输出中有任意一方成功了,它就会设置冷却,并更新同步自己的数据。
我们分别来看输出和拉取的逻辑。
先看输出...
输出输出操作由 insert() 方法完成,它的作用是将物品从漏斗传输到目标容器,并返回一个布尔值来表示是否成功传输。如果传输成功,则漏斗进入冷却状态。
输出的基本逻辑:
-
确认目标容器是否可用
- 如果目标容器已满,则输出失败。
- 如果目标容器有可用槽位,则尝试传输物品。
-
尝试输出物品
- 遍历漏斗中的物品槽,逐个尝试传输物品到目标容器。
- 只要有一次传输成功,就返回 “成功”;否则返回 “失败”。
当调用 transfer() 方法尝试传输物品时,transfer() 方法会返回一个物品组:
- 若返回值为空,则说明物品已完全传输,输出成功。
- 若返回值不为空,则说明物品未能全部传输,输出失败。
到此,输出的部分已经完成,接下来,我们来看看拉取的部分。
拉取物品的逻辑与输出类似,但方向相反:
- 输出 时:漏斗
transfer()物品至目标容器。 - 拉取 时:某个容器
transfer()物品至漏斗。
由于 transfer() 方法的逻辑马上会详细解释,因此这里不再赘述。
transfer() 是漏斗实现物品传输的核心方法。它接收以下参数:
- from:物品来源(可以是漏斗或其他容器)
- to:传输目标(可以是容器或另一个漏斗)
- itemStack:希望传输的物品组
- slot:目标容器中的目标槽位
执行流程如下:
-
检查目标槽位的状态
- 如果该槽为空,直接将
itemStack放入,并清空itemStack。 - 如果该槽已有相同类型的物品,并且仍有剩余空间,则合并物品并清空
itemStack。
- 如果该槽为空,直接将
-
判断是否传输成功
- 传输后,若
itemStack为空,则表示成功传输,否则表示失败。
- 传输后,若
-
特殊处理漏斗间传输的时序
- 如果目标是另一个漏斗,则会给目标漏斗增加
8gt的冷却时间。 - 若目标漏斗
to已先于源漏斗from计算,则目标漏斗to仅会得到7gt的冷却时间。值得注意的是,这只在目标漏斗为空时发生;
此外,就算from后于to计算, 并给予8gt的冷却时间,由于随后totick 时会将冷却立刻 - 1,所以结果上还是只有7gt冷却。
- 如果目标是另一个漏斗,则会给目标漏斗增加
到此,你已经完全了解了漏斗的基本运作机制。相信这将会对之后的篇章理解提供很大的帮助。
2.1.4 移动中的活塞
另外一个及其重要的方块叫 moving_piston,移动的活塞,俗称 b36。
2.1.5 移动的活塞方块实体携带的信息
一个移动活塞方块实体携带一下基本信息:
pushedBlock存储的方块facing朝向extending是否在伸出(与动画播放的方向有关)source是否是收回中的活塞本体progress当前的移动进度savedWorldTime上次执行逻辑时的世界时间
2.1.6 移动的活塞方块实体的功能
- 处理活塞推送实体
- 更新动画进度
- 如果
progress>=1,则将 MOVING_PISTON 替换为pushedBlock中存储的方块 - 在客户端渲染动画
2.1.7 移动的活塞方块实体的工作流程
详情见5.7。
3 方块实体间的时序
方块实体tick过程执行由 LevelChunk 中以下两个列表维护:
pendingBlockEntityTickers: 在tickBlockEntities过程中动态加入的方块实体。blockEntityTickers: 将要tick的方块实体
这两个列表都是 ArrayList,因此它们的遍历顺序完全取决于元素的加入顺序。具体流程如下:
tickBlockEntities()会首先将pendingBlockEntityTickers中的元素添加到blockEntityTickers末尾,然后清空pendingBlockEntityTickers。- 遍历
blockEntityTickers时,如果某个TickingBlockEntity已被移除(isRemoved()返回true),则从列表中移除;否则会调用其tick()方法进行更新。
新增方块实体时,会调用 addBlockEntityTicker() 方法,将其加入列表。若当前正处于 tick 过程中(tickingBlockEntities == true),则加入 pendingBlockEntityTickers;否则直接加入 blockEntityTickers。
卸载时, 同一区块内的方块实体会被保存在 Map<BlockPos, BlockEntity> 中,加载时遍历这个 Map,并依次调用 addBlockEntityTicker() 加入 blockEntityTickers。因此它们的执行顺序取决于 BlockPos 的 hashCode 计算顺序。
不同区块之间的方块实体加载顺序则取决于区块的加载顺序,即哪个区块先加载,它的方块实体就会先加入 blockEntityTickers。
一句话总结:
卸载前, 方块实体的计算顺序取决于放置的顺序. 重新加载后。一个区块内的方块实体计算取决与其 pos 的 hash, 不同区块的方块实体则取决于区块加载的顺序。
相关代码如下 (删减了部分无关内容)





