Skip to main content
Version: 1.21.4

实体(Entities)

实体(Entities)是游戏世界中可以与环境进行各种交互的对象。常见的例子包括生物(mobs)、抛射物(projectiles)、可骑乘物体(rideable objects),甚至玩家本身。每一个实体都由多个系统组成,初看可能会让人觉得难以理解。本节将拆解与构建实体及实现其预期行为相关的一些关键组件,帮助模组开发者更好地把握实体的工作原理。

术语(Terminology)

一个简单的实体通常由三部分组成:

更复杂的实体可能还需要更多的组成部分。例如,许多复杂的 EntityRenderer 会使用底层的 EntityModel 实例。又如,如果实体需要自然生成,还需要某种生成机制

EntityType

EntityTypeEntity 之间的关系类似于 ItemItemStack 的关系。与 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

名称生成上限示例
MONSTER70各类怪物
CREATURE10各类动物
AMBIENT15蝙蝠
AXOLOTS5美西螈
UNDERGROUND_WATER_CREATURE5发光鱿鱼
WATER_CREATURE5鱿鱼、海豚
WATER_AMBIENT20鱼类
MISCN/A所有非生物实体,如抛射物;使用此 MobCategory 的实体将无法自然生成

此外,还有一些只针对特定 MobCategory 设置的其他属性:

  • isFriendly:对于 MONSTER 设为 false,其他类别均为 true。
  • isPersistent:对于 CREATUREMISC 设为 true,其他类别为 false。
  • despawnDistanceWATER_AMBIENT 设为 64,其他类别为 128。
info

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;
}
}
info

虽然可以直接继承 Entity,但通常更推荐以其众多子类之一为基础。更多信息请参阅 实体类层级结构

如果有需要(例如你需要通过代码生成实体),你也可以添加自定义构造函数。这类构造函数通常会将实体类型硬编码为已注册对象的引用,例如:

public MyEntity(EntityType<? extends MyEntity> type, Level level, double x, double y, double z) {
// 委托给工厂构造函数,使用之前注册的 EntityType。
this(type, level);
this.setPos(x, y, z);
}
warning

自定义构造函数绝不能只包含两个参数,否则会与上面的 (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#hurtEntity#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#hurtEntity#hurtOrSimulate 的区别所在:Entity#hurt 只在服务端运行(并调用 Entity#hurtServer),而 Entity#hurtOrSimulate 会在两端都运行,根据当前端还是服务端分别调用 Entity#hurtServerEntity#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 使用,定义声波攻击的发射起点
info

PASSENGERVEHICLE 是相关的,因为它们在同一上下文中使用。首先,PASSENGER 用于定位骑乘者的位置;然后,VEHICLE 应用于骑乘者本身。

每个附着点都可以被视为一个从 EntityAttachmentList<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 或其合适的子类来创建新的抛射物,并重写相关方法以实现你的自定义功能。常见的需要重写的方法包括:

  • #shoot:计算并设置该抛射物的正确速度。
  • #onHit:当抛射物击中某物时调用。
    • #onHitEntity:当击中的对象是一个 实体 时调用。
    • #onHitBlock:当击中的对象是一个 方块 时调用。
  • #getOwner#setOwner:分别用于获取和设置拥有该抛射物的实体。
  • #deflect:根据传入的 ProjectileDeflection 枚举值使抛射物发生偏转。
  • #onDeflection:在 #deflect 方法调用后,用于处理偏转后的行为。