实体(Entities)
实体(Entities)是游戏世界中可以与环境进行各种交互的对象。常见的例子包括生物(mobs)、抛射物(projectiles)、可骑乘物体(rideable objects),甚至玩家本身。每一个实体都由多个系统组成,初看可能会让人觉得难以理解。本节将拆解与构建实体及实现其预期行为相关的一些关键组件,帮助模组开发者更好地把握实体的工作原理。
术语(Terminology)
一个简单的实体通常由三部分组成:
Entity子类,负责保存大部分实体的逻辑EntityType,需要进行注册,并保存一些通用属性EntityRenderer,负责在游戏中渲染实体
更复杂的实体可能还需要更多的组成部分。例如,许多复杂的 EntityRenderer 会使用底层的 EntityModel 实例。又如,如果实体需要自然生成,还需要某种生成机制。
EntityType
EntityType 与 Entity 之间的关系类似于 Item 与 ItemStack 的关系。与 Item 一样,EntityType 是单例(singleton),会被注册到对应的注册表(Registry)(即实体类型注册表),并保存所有该类型实体共有的一些属性;而 Entity,就像 ItemStack,是该单例类型的“实例”,用于保存特定实体实例的数据。不过,这里有一个关键区别:大部分行为并不是在单例的 EntityType 中定义的,而是在具体实例化的 Entity 类中实现的。
接下来我们来创建一个 EntityType 注册表,并为其注册一个 EntityType,假设我们有一个继承自 Entity 的类 MyEntity(更多信息见下文)。EntityType.Builder 上的所有方法,除了最后的 #build 调用,都是可选的。
public static final DeferredRegister.Entities ENTITY_TYPES =
DeferredRegister.createEntities(ExampleMod.MOD_ID);
public static final Supplier<EntityType<MyEntity>> MY_ENTITY = ENTITY_TYPES.register(
"my_entity",
// 实体类型(entity type),通过构建器(builder)创建。
() -> EntityType.Builder.of(
// 一个 EntityType.EntityFactory<T>,其中 T 是实体类,这里是 MyEntity。
// 你可以把它理解为一个 BiFunction<EntityType<T>, Level, T>。
// 通常这里传递实体的构造方法引用。
MyEntity::new,
// 我们的实体使用的 MobCategory。主要和生成(spawning)有关。
// 详细信息见下文。
MobCategory.MISC
)
// 实体的宽度和高度,以方块为单位。宽度会同时应用于两个水平方向。
// 这也意味着不支持非正方形的占地面积。默认值为 0.6f 和 1.8f。
.sized(1.0f, 1.0f)
// 用于生成不同尺寸生物的乘法因子(标量)。
// 在原版中,只有史莱姆和岩浆怪使用 4.0f。
.spawnDimensionsScale(4.0f)
// 视线高度(eye height),从底部起算,以方块为单位。默认为高度 * 0.85。
// 必须在调用 #sized 之后设置才会生效。
.eyeHeight(0.5f)
// 禁止通过 /summon 命令召唤该实体。
.noSummon()
// 防止实体被保存到磁盘。
.noSave()
// 让实体免疫火焰伤害。
.fireImmune()
// 让实体对某种方块造成的伤害免疫。原版中用于让狐狸免疫甜浆果丛,
// 凋零和凋零骷髅免疫凋零玫瑰,北极熊、雪傀儡和流浪者免疫细雪。
.immuneTo(Blocks.POWDER_SNOW)
// 禁用生成处理器中限制实体生成距离的规则。
// 这意味着无论与玩家距离多远,该实体都可以生成。
// 原版对掠夺者和潜影贝启用了此项。
.canSpawnFarFromPlayer()
// 客户端保持实体加载的范围,以区块为单位。
// 原版的取值不一,但通常在 8 或 10 左右。默认值为 5。
// 注意,如果此值大于客户端的区块视野距离,则实际以区块视野距离为准。
.clientTrackingRange(8)
// 该实体发送更新数据包的频率,每 x tick 发送一次。对于运动模式可预测的实体(如投射物),
// 此值会更高。默认值为 3。
.updateInterval(10)
// 通过资源键(resource key)构建实体类型。第二个参数应与实体 id 相同。
.build(ResourceKey.create(
Registries.ENTITY_TYPE,
ResourceLocation.fromNamespaceAndPath("examplemod", "my_entity")
))
);
// 简写版本以减少样板代码。下面的调用等价于
// ENTITY_TYPES.register("my_entity", () -> EntityType.Builder.of(MyEntity::new, MobCategory.MISC).build(
// ResourceKey.create(Registries.ENTITY_TYPE, ResourceLocation.fromNamespaceAndPath("examplemod", "my_entity"))
// );
public static final Supplier<EntityType<MyEntity>> MY_ENTITY =
ENTITY_TYPES.registerEntityType("my_entity", MyEntity::new, MobCategory.MISC);
// 还能传入 UnaryOperator<EntityType.Builder> 参数,允许链式调用更多构建器方法的简写版本。
public static final Supplier<EntityType<MyEntity>> MY_ENTITY = ENTITY_TYPES.registerEntityType(
"my_entity", MyEntity::new, MobCategory.MISC,
builder -> builder.sized(2.0f, 2.0f).eyeHeight(1.5f).updateInterval(5));
MobCategory
另见 自然生成。
一个实体的 MobCategory(生物类别)决定了该实体的一些属性,这些属性主要与生成与消失相关。原版 Minecraft 默认添加了八种 MobCategory:
| 名称 | 生成上限 | 示例 |
|---|---|---|
MONSTER | 70 | 各类怪物 |
CREATURE | 10 | 各类动物 |
AMBIENT | 15 | 蝙蝠 |
AXOLOTS | 5 | 美西螈 |
UNDERGROUND_WATER_CREATURE | 5 | 发光鱿鱼 |
WATER_CREATURE | 5 | 鱿鱼、海豚 |
WATER_AMBIENT | 20 | 鱼类 |
MISC | N/A | 所有非生物实体,如抛射物;使用此 MobCategory 的实体将无法自然生成 |
此外,还有一些只针对特定 MobCategory 设置的其他属性:
isFriendly:对于MONSTER设为 false,其他类别均为 true。isPersistent:对于CREATURE和MISC设为 true,其他类别为 false。despawnDistance:WATER_AMBIENT设为 64,其他类别为 128。
MobCategory 是一个 可扩展枚举(extensible enum),这意味着你可以向其中添加自定义条目。如果你这样做,还需要为该自定义 MobCategory 的实体添加相应的生成机制。
实体类(The Entity Class)
首先,我们需要创建一个 Entity 的子类。除了构造函数之外,Entity(它是一个抽象类)还定义了四个必须实现的方法。前三个方法将在 数据与网络通信 一文 中详细讲解,以避免本篇内容过长,#hurtServer 方法则在 实体伤害机制 部分进行说明。
public class MyEntity extends Entity {
// 我们继承了这个构造函数,但没有对泛型通配符加上限定。
// 下面注册时需要这个限定,因此我们在这里加上。
public MyEntity(EntityType<? extends MyEntity> type, Level level) {
super(type, level);
}
// 关于这些方法的信息,请参阅“数据与网络”章节。
@Override
protected void readAdditionalSaveData(CompoundTag compoundTag) {}
@Override
protected void addAdditionalSaveData(CompoundTag compoundTag) {}
@Override
protected void defineSynchedData(SynchedEntityData.Builder builder) {}
@Override
public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
return true;
}
}
虽然可以直接继承 Entity,但通常更推荐以其众多子类之一为基础。更多信息请参阅 实体类层级结构。
如果有需要(例如你需要通过代码生成实体),你也可以添加自定义构造函数。这类构造函数通常会将实体类型硬编码为已注册对象的引用,例如:
public MyEntity(EntityType<? extends MyEntity> type, Level level, double x, double y, double z) {
// 委托给工厂构造函数,使用之前注册的 EntityType。
this(type, level);
this.setPos(x, y, z);
}
自定义构造函数绝不能只包含两个参数,否则会与上面的 (EntityType, Level) 构造函数混淆。
现在,我们基本上可以随意扩展和自定义我们的实体了。下面的子章节将展示各种常见实体的使用场景。
实体上的数据存储(Data Storage on Entities)
详见 实体/数据与网络。
实体渲染(Rendering Entities)
详见 实体/实体渲染器。
生成实体(Spawning Entities)
如果现在启动游戏并进入一个世界,目前只有一种生成方式:通过 /summon 指令(前提是没有调用 EntityType.Builder#noSummon)。
显然,我们希望用其他方式添加实体。最简单的方法是使用 LevelWriter#addFreshEntity 方法。该方法只需传入一个 Entity 实例即可将其添加到世界中,如下所示:
// 在某个有 level 对象的方法中,仅在服务端执行
if (!level.isClientSide()) {
MyEntity entity = new MyEntity(level, 100.0, 200.0, 300.0);
level.addFreshEntity(entity);
}
另外,你也可以调用 EntityType#spawn,特别推荐用于生成 生物实体,因为它会进行一些额外设置,比如触发生成 事件。
这部分内容适用于几乎所有非生物实体。玩家(Player)显然不应该由你自己生成,Mob 有它们自己的生成方式(尽管也可以通过 #addFreshEntity 添加),而原版的投射物也有 Projectile 类中的静态辅助方法用于生成。
伤害实体(Damaging Entities)
另见 左键点击物品。
虽然并非所有实体都有生命值(hit points)的概念,但它们仍然可以受到伤害。这不仅仅适用于像生物(mob)和玩家这样的对象:比如说物品实体(item entities,掉落的物品),它们同样可以从火焰或仙人掌等来源受到伤害,这种情况下通常会被立即删除。
对实体造成伤害可以通过调用 Entity#hurt 或 Entity#hurtOrSimulate 方法实现,这两者的区别将在下文解释。这两个方法都接受两个参数:DamageSource 和伤害值(以半颗心为单位的 float)。例如,调用 entity.hurt(entity.damageSources().wither(), 4.25) 会对实体造成略高于两颗心的凋零伤害。
反过来,实体也可以修改这种行为。这不是通过重写 #hurt 方法实现的,因为它是 final 方法。实际上,有两个方法 #hurtServer 和 #hurtClient,分别处理服务端和客户端的伤害逻辑。#hurtClient 通常用于告知客户端攻击已成功,即使这并不总是事实,主要用于播放攻击音效和其他效果。若要改变伤害行为,我们主要关注 #hurtServer,可以像下面这样重写:
@Override
// 布尔返回值决定实体是否真的受到了伤害。
public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
if (damageSource.is(DamageTypeTags.IS_FIRE)) {
// 这里假设 super#hurt() 已经实现。其他常见做法是你自己设置某个字段。
// 原版实现因实体类型不同差异很大。值得注意的是,生物实体通常会调用 #actuallyHurt,
// 而该方法又会调用 #setHealth。
return super.hurt(level, damageSource, amount * 2);
} else {
return false;
}
}
服务端/客户端的分离也是 Entity#hurt 和 Entity#hurtOrSimulate 的区别所在:Entity#hurt 只在服务端运行(并调用 Entity#hurtServer),而 Entity#hurtOrSimulate 会在两端都运行,根据当前端还是服务端分别调用 Entity#hurtServer 或 Entity#hurtClient。
你也可以通过事件(event)来修改对非你自己定义的实体(比如 Minecraft 或其他模组添加的实体)造成的伤害。这些事件包含了许多专门针对 LivingEntity 的代码,因此相关文档在伤害事件部分,位于生物实体(Living Entities)一文中。
实体的 tick(Ticking Entities)
通常情况下,你会希望你的实体(entity)在每个 tick 时执行某些操作(比如移动)。这些逻辑分布在几个方法(method)中:
#tick:这是核心的 tick 方法,在 99% 的情况下你都需要重写(override)它。- 默认情况下,它会调用
#baseTick,但几乎所有子类都会重写这个方法。
- 默认情况下,它会调用
#baseTick:这个方法负责更新所有实体共有的一些属性,包括“着火”状态、因粉雪冻结、游泳状态以及穿越传送门等。LivingEntity还会在这里处理溺水、方块伤害和伤害追踪器的更新。如果你想修改或扩展这些逻辑,可以重写此方法。- 默认情况下,
Entity#tick会调用这个方法。
- 默认情况下,
#rideTick:这个方法会在实体作为其他实体的乘客时被调用,例如玩家骑马,或者通过/ride指令让任何实体骑乘另一个实体。- 默认实现会做一些检查,然后调用
#tick。骷髅和玩家会重写此方法,以特殊处理骑乘逻辑。
- 默认实现会做一些检查,然后调用
此外,实体还包含一个名为 tickCount 的字段(field),它表示实体在当前世界中存在的 tick 数(时间),还有一个名为 firstTick 的布尔字段,顾名思义就是标记是否为第一个 tick。例如,如果你想每 5 tick 生成一个粒子,可以使用如下代码:
@Override
public void tick() {
// 除非有充分理由,否则始终调用父类方法。
super.tick();
// 每 5 tick 执行一次,并确保只在服务器端生成粒子。
if (this.tickCount % 5 == 0 && !level().isClientSide()) {
level().addParticle(...);
}
}
实体拾取(Picking Entities)
另见 中键点击。
拾取(picking)是指选中玩家当前正在注视的对象,并据此选择关联的物品。中键点击的结果,被称为“拾取结果(pick result)”,可以由你的实体自定义(注意,Mob 类会自动为你选择正确的刷怪蛋):
@Override
@Nullable
public ItemStack getPickResult() {
// 假设 MY_CUSTOM_ITEM 是一个 DeferredItem<?>,详见 Items 相关文档。
// 如果实体不应被拾取,建议这里返回 null。
return new ItemStack(MY_CUSTOM_ITEM.get());
}
一般来说,实体应该是可拾取的,但在某些特殊场景下这并不合适。例如原版中的末影龙(ender dragon),它由多个部分组成。父实体禁用了拾取,子部分则重新启用拾取,以便更精细地调整碰撞箱。
如果你有类似的特殊需求,也可以让你的实体完全不可被拾取,方法如下:
@Override
public boolean isPickable() {
// 如有需要,可以在此添加额外的检查逻辑。
return false;
}
如果你希望自己进行拾取(即射线投射,ray casting),可以在你想要作为射线起点的实体上调用 Entity#pick 方法。这个方法会返回一个 HitResult,你可以进一步检查射线实际命中的对象。
实体附着点(Entity Attachments)
不要与 数据附着(Data Attachments) 混淆。
实体附着点用于为实体定义可视化的附着位置。通过这个系统,可以指定诸如乘客、名称标签等元素相对于实体本体显示的位置。实体自身只控制附着点的默认位置,而附着点则可以在此基础上定义一个偏移量。
在构建 EntityType 时,可以通过调用 EntityType.Builder#attach 方法设置任意数量的附着点。该方法接受一个 EntityAttachment(定义要考虑的附着点),以及三个 float 类型的参数用于定义位置(x/y/z)。这个位置应该是相对于该附着点默认值来定义的。
原版 Minecraft 定义了以下四个 EntityAttachment:
| 名称 | 默认值 | 用途 |
|---|---|---|
PASSENGER | 碰撞箱(hitbox)中心 X/顶部 Y/中心 Z | 可骑乘实体,如马,用于定义乘客出现的位置 |
VEHICLE | 碰撞箱中心 X/底部 Y/中心 Z | 所有实体,用于定义作为乘客时 实体出现的位置 |
NAME_TAG | 碰撞箱中心 X/顶部 Y/中心 Z | 定义实体名称标签显示的位置(如适用) |
WARDEN_CHEST | 碰撞箱中心 X/中心 Y/中心 Z | 由 Warden 使用,定义声波攻击的发射起点 |
PASSENGER 和 VEHICLE 是相关的,因为它们在同一上下文中使用。首先,PASSENGER 用于定位骑乘者的位置;然后,VEHICLE 应用于骑乘者本身。
每个附着点都可以被视为一个从 EntityAttachment 到 List<Vec3> 的映射。实际使用的点数量取决于具体的消费系统。例如,船和骆驼会使用两个 PASSENGER 点,而像马或矿车这样的实体只会用到一个 PASSENGER 点。
EntityType.Builder 还提供了一些与 EntityAttachment 相关的辅助方法:
#passengerAttachment():用于定义PASSENGER附着点(attachment)。有两种重载形式。- 一种重载接受一个
Vec3...类型的附着点列表。 - 另一种重载接受一个
float...,会将每个 float 转换为一个 y 值为该 float、x 和 z 均为 0 的Vec3,然后转发给Vec3...形式的重载。
- 一种重载接受一个
#vehicleAttachment():用于定义VEHICLE附着点。参数为一个Vec3。#ridingOffset():用于定义VEHICLE附着点。参数为一个 float,并将其转为 x 和 z 均为 0,y 为传入 float 的相反数的Vec3,然后转发给#vehicleAttachment()。#nameTagOffset():用于定义NAME_TAG附着点。参数为一个 float,作为 y 值,x 和 z 均为 0。
另外,你也可以通过调用 EntityAttachments#builder(),然后在该 builder 上调用 #attach() 方法,手动自定义附着点,例如:
// 在某个 EntityType<?> 创建过程中
EntityType.Builder.of(...)
// 这个 EntityAttachment 会让名称标签(name tags)悬浮在地面上方半格的位置。
// 如果没有设置,则默认使用实体的碰撞箱高度。
.attach(EntityAttachment.NAME_TAG, 0, 0.5f, 0)
.build();
实体类层级结构(Entity Class Hierarchy)
由于实体(entity)类型繁多,Entity 有一套复杂的子类层级结构。当你自定义实体时,了解这些层级非常重要,因为选择合适的父类可以让你复用已有代码,节省大量工作量。
原版(vanilla)实体的类层级结构如下(红色类为 abstract 抽象类,蓝色类为非抽象类):
让我们逐一解释这些类:
Projectile:各种抛射物的基类,包括箭、火球、雪球、烟花等类似实体。你可以在下文了解更多相关内容。LivingEntity:所有“生物”类实体的基类,这里的“生物”指具有生命值、装备、生物效果等属性的实体。包括怪物、动物、村民以及玩家等。详细内容请参考 生物实体(Living Entities)文章。BlockAttachedEntity:所有静止且附着在方块上的实体的基类。包括拴绳结、物品展示框和画等。这些子类主要用于复用通用代码。PartEntity:分部实体(即由多个小实体组成的大型实体)的基类。在原版游戏中,目前只用于末影龙。VehicleEntity:船和矿车的基类。虽然这些实体与LivingEntity有生命值等相似概念,但它们与生物实体在其他属性上差别很大,因此被单独划分。子类主要用于代码复用。
此外,还有一些实体直接继承自 Entity,这通常是因为没有其他合适的父类。大多数名称都比较直观:
AreaEffectCloud(滞留药水云)EndCrystal(末地水晶)EvokerFangs(唤魔者之牙)ExperienceOrb(经验球)EyeOfEnder(末影之眼)FallingBlockEntity(掉落的方块,如沙子、沙砾等)ItemEntity(掉落物品)LightningBolt(闪电)OminousItemSpawner(用于持续生成试炼刷怪笼掉落物的实体)PrimedTnt(已点燃的 TNT) 未包含在此图表和列表中的还有地图制作者实体(mapmaker entities),例如显示类(displays)、交互类(interactions)和标记类(markers)。
抛射物(Projectiles)
抛射物是实体(entities)的一种子类。它们的共同特征是会沿一个方向飞行直到碰撞某物,并且有一个归属者(owner),例如玩家或骷髅是箭的归属者,恶魂(ghast)是火球的归属者。
抛射物的类继承结构如下(红色类为 abstract 抽象类,蓝色类为普通类):
值得注意的是,Projectile 有三个直接抽象子类:
AbstractArrow:该类涵盖了不同类型的箭,以及三叉戟(trident)。它们的一个重要共同点是不会直线飞行,会受到重力影响。AbstractHurtingProjectile:该类涵盖了风能弹(wind charges)、各种火球(fireballs)和凋零骷髅头(wither skulls)。这些都是不受重力影响的伤害型抛射物。ThrowableProjectile:该类包括鸡蛋(eggs)、雪球(snowballs)和末影珍珠(ender pearls)等。它们和箭一样会受到重力影响,但与箭不同的是,它们击中目标时不会造成伤害。此外,它们都是通过使用对应的物品生成的。
你可以通过继承 Projectile 或其合适的子类来创建新的抛射物,并重写相关方法以实现你的自定义功能。常见的需要重写的方法包括: