实体(Entities)
实体(Entities)是世界中的对象,可以以多种方式与世界交互。常见的例子包括生物(Mobs)、抛射物(Projectiles)、可骑乘对象以及玩家。每个实体由多个系统组成,这些系统乍一看可能难以理解。本部分将分解与构建实体并使 其按模组开发者意图行为相关的一些关键组件。
术语(Terminology)
一个简单的实体由三部分组成:
Entity子类,它包含了我们实体的大部分逻辑。EntityType,它是已注册(Registered)的并保存一些通用属性。EntityRenderer,负责在游戏中显示实体。
更复杂的实体可能需要更多部分。例如,许多更复杂的 EntityRenderers 使用底层的 EntityModel 实例。或者,自然生成的实体需要某种生成机制(Spawn Mechanism)。
EntityType
EntityTypes 和 Entitys 之间的关系类似于Items和ItemStacks之间的关系。像 Items 一样,EntityTypes 是单例,注册到相应的注册表(实体类型注册表),并保存对该类型所有实体通用的值;而 Entitys,像 ItemStacks 一样,是该单例类型的"实例",保存特定于该实体实例的数据。然而,这里的关键区别在于,大部分行为并非定义在单例 EntityType 中,而是在实例化的 Entity 类本身。
让我们创建我们的 EntityType 注册表并为 它注册一个 EntityType,假设我们有一个类 MyEntity 扩展了 Entity(更多信息见下文)。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",
// 实体类型(EntityType),使用构建器创建。
() -> EntityType.Builder.of(
// 一个 EntityType.EntityFactory<T>,其中 T 是使用的实体类 - 本例中为 MyEntity。
// 你可以把它看作是一个 BiFunction<EntityType<T>, Level, T>。
// 这通常是实体构造函数的引用。
MyEntity::new,
// 我们的实体使用的生物类别(MobCategory)。这主要与生成相关。
// 更多信息见下文。
MobCategory.MISC
)
// 宽度和高度,单位:方块。宽度用于水平和垂直两个方向。
// 这意味着不支持非正方形的足迹。默认为 0.6f 和 1.8f。
.sized(1.0f, 1.0f)
// 一个乘性因子(标量),用于在不同尺寸下生成的生物。
// 在原版中,只有史莱姆和岩浆怪使用此功能,两者都使用 4.0f。
.spawnDimensionsScale(4.0f)
// 眼睛高度,单位:方块,从尺寸底部算起。默认为 height * 0.85。
// 必须在 #sized 之后调用才有效果。
.eyeHeight(0.5f)
// 禁止通过 /summon 命令召唤实体。
.noSummon()
// 防止实体被保存到磁盘。
.noSave()
// 使实体防火。
.fireImmune()
// 使实体免疫来自特定方块的伤害。原版使用此功能使狐狸免疫甜浆果灌木丛,
// 凋灵和凋灵骷髅免疫凋零玫瑰,北极熊、雪傀儡和流浪者免疫细雪。
.immuneTo(Blocks.POWDER_SNOW)
// 禁用生成处理程序中的一条规则,该规则限制了实体可以生成的距离。
// 这意味着无论距离玩家多远,该实体都可以生成。
// 原版为掠夺者和潜影贝启用了此功能。
.canSpawnFarFromPlayer()
// 客户端保持实体加载的范围,单位:区块。
// 原版对此 值有所不同,但通常大约为8或10。默认为5。
// 请注意,如果此值大于客户端的区块视图距离,
// 那么将使用该区块视图距离。
.clientTrackingRange(8)
// 为此实体发送更新数据包的频率,单位:每 x 游戏刻一次。对于具有可预测移动模式的实体,
// 例如抛射物,此值设置得较高。默认为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 决定了实体的一些属性,这些属性与生成和消失(Spawning and Despawning)相关。原版默认添加了总共八个 MobCategorys:
| 名称(Name) | 生成上限(Spawn Cap) | 示例(Examples) |
|---|---|---|
MONSTER | 70 | 各种怪物 |
CREATURE | 10 | 各种动物 |
AMBIENT | 15 | 蝙蝠 |
AXOLOTS | 5 | 美西螈 |
UNDERGROUND_WATER_CREATURE | 5 | 发光鱿鱼 |
WATER_CREATURE | 5 | 鱿鱼、海豚 |
WATER_AMBIENT | 20 | 鱼 |
MISC | 不适用(N/A) | 所有非生物实体,例如抛射物;使用此 MobCategory 将使实体完全无法自然生成 |
还有一些其他属性仅针对一个或两个 MobCategorys 设置:
isFriendly:对于MONSTER设置为 false,对于所有其他设置为 true。isPersistent:对于CREATURE和MISC设置为 true,对于所有其他设置为 false。despawnDistance:对于WATER_AMBIENT设置为64,对于所有其他设置为128。
MobCategory 是一个可扩展枚举(Extensible Enum),意味着你可以向其添加自定义条目。如果这样做,你还需要为此自定义 MobCategory 添加一些生成机制。
实体类(The Entity Class)
首先,我们创建一个 Entity 子类。除了构造函数外,Entity(这是一个抽象类)定义了四个我们需要实现的方法。前三个将在数据与网络(Data and Networking)文章中解释,以免进一步膨胀本文,而 #hurtServer 在伤害实体(Damaging Entities)部分中解释。
public class MyEntity extends Entity {
// 我们继承了没有泛型通配符边界的构造函数。
// 下面的注册需要边界,所以在这里加上。
public MyEntity(EntityType<? extends MyEntity> type, Level level) {
super(type, level);
}
// 关于这些方法的信息,请参见数据与网络(Data and Networking)文章。
@Override
protected void readAdditionalSaveData(ValueInput input) {}
@Override
protected void addAdditionalSaveData(ValueOutput output) {}
@Override
protected void defineSynchedData(SynchedEntityData.Builder builder) {}
@Override
public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
return true;
}
}
虽然可以直接扩展 Entity,但通常使用其众多子类之一作为基类更有意义。更多信息请参阅实体类层次结构(Entity Class Hierarchy)。
如果需要(例如,因为你要从代码生成实体),你也可以添加自定义构造函数。这些构造函数通常将对注册对象的引用硬编码为实体类型,如下所示:
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)
参见实体/数据与网络(Entities/Data and Networking)。
渲染实体(Rendering Entities)
参见实体/实体渲染器(Entities/Entity Renderers)。
生成实体(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,这尤其推荐在生成活体实体(Living Entities)时使用,因为它会进行一些额外的设置,例如触发生成事件(Events)。
这几乎可以用于所有非生物实体。玩家显然不应该由你生成,Mobs 有它们自己的生成方式(尽管它们也可以通过 #addFreshEntity 添加),原版抛射物(Projectiles)在 Projectile 类中也有用于生成的静态辅助方法。
伤害实体(Damaging Entities)
另见左键单击物品(Left-Clicking an Item)。
虽然不是所有实体都有生命值的概念,但它们 都可以受到伤害。这不仅适用于诸如生物和玩家的东西:如果你想到物品实体(掉落的物品),它们也会受到来自火焰或仙人掌等来源的伤害,在这种情况下,它们通常会被立即删除。
可以通过调用 Entity#hurt 或 Entity#hurtOrSimulate 来伤害实体,这两个方法的区别如下所述。两个方法都接受两个参数:DamageSource和伤害量,以半颗心为单位的浮点数。例如,调用 entity.hurt(entity.damageSources().wither(), 4.25) 会造成两心多一点的凋零伤害。
反过来,实体也可以修改这种行为。这不是通过重写 #hurt 来完成的,因为它是一个最终方法。相反,有两个方法 #hurtServer 和 #hurtClient,分别处理相应侧的伤害逻辑。#hurtClient 通常用于告诉客户端攻击成功,即使情况并非总是如此,主要是为了播放攻击声音和其他效果,而不受影响。要更改伤害行为,我们主要关心 #hurtServer,我们可以像这样重写它:
@Override
// 布尔返回值决定了实体是否真的受到了伤害。
public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
if (damageSource.is(DamageTypeTags.IS_FIRE)) {
// 这假设 super#hurtServer() 已实现。其他常见做法是自行设置某个字段。
// 值得注意的是,活体实体通常调用 #actuallyHurt,而后者又调用 #setHealth。
return super.hurtServer(level, damageSource, amount * 2);
} else {
return false;
}
}
这种服务器/客户端分离也是 Entity#hurt 和 Entity#hurtOrSimulate 之间的区别:Entity#hurt 只在服务器端运行(并调用 Entity#hurtServer),而 Entity#hurtOrSimulate 在两侧都运行,根据所在侧调用 Entity#hurtServer 或 Entity#hurtClient。
也可以通过事件(Events)修改对不属于你的实体(即由Minecraft或其他模组添加的实体)造成的伤害。这些事件包含大量特定于 LivingEntitys 的代码;因此,它们的文档位于活体实体(Living Entities)文章中的伤害事件(Damage Events)部分。
实体刻更新(Ticking Entities)
很多时候,你会希望你的实体每游戏刻都做些什么(例如移动)。这个逻辑被拆分到几个方法中:
#tick:这是核心的刻更新方法,在99%的情况下你会想重写它。- 默认情况下,这会转发到
#baseTick,但几乎所有子类都重写了这个方法。
- 默认情况下,这会转发到
#baseTick:此方法处理所有实体通用的值的更新,包括"着火"状态、细雪冻结、游泳状态以及穿过传送门。LivingEntity还在此处理溺水、在方块内伤害以及伤害追踪器的更新。如果你想更改或添加该逻辑,请重写此方法。- 默认情况下,
Entity#tick将转发到此方法。
- 默认情况下,
#rideTick:此方法用于其他实体的乘客,例如玩家骑马,或任何由于使用/ride命令而骑乘其他实体的实体。- 默认情况下,这会进行一些检查,然后调用
#tick。骷髅和玩家重写此方法以特殊处理骑乘实体。
- 默认情况下,这会进行一些检查,然后调用
此外,实体还有一个名为 tickCount 的字段,表示实体在世界中存在的时间(单位:游戏刻),以及一个名为 firstTick 的布尔字段,其含义应该不言而喻。例如,如果你想每5游戏刻生成一个粒子(Particle),你可以使用以下代码:
@Override
public void tick() {
// 除非有充分理由,否则始终调用父类方法。
super.tick();
// 每5游戏刻运行一次此代码,并确保我们在服务器端生成粒子。
if (this.tickCount % 5 == 0 && !this.level().isClientSide()) {
this.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());
}
虽然实体通常应该可以被拾取,但在某些特殊情况下这并不理想。原版的一个用例是末影龙,它由多个部分组成。父实体禁用了拾取,但部分又重新启用了拾取,以便进行更精细的碰撞箱调整。
如果你有类似的特殊用例,你的实体也可以通过以下方式完全禁用拾取:
@Override
public boolean isPickable() {
// 如果需要,可以在此处执行额外的检查。
return false;
}
如果你想自己进行拾取(即光线投射),你可以在要开始光线投射的实体上调用 Entity#pick。这将返回一个HitResult,你可以进一步检查光线投射究竟击中了什么。
实体附着点(Entity Attachments)
不要与数据附加项(Data Attachments)混淆。
实体附着点(Entity Attachments)用于定义实体的视觉附着点。使用这个系统,可以定义诸如乘客或名称标签等事物相对于实体本身显示的位置。实体本身仅控制附着点的默认位置,然后附着点可以定义相对于该默认位置的偏移量。
构建 EntityType 时,可以通过调用 EntityType.Builder#attach 设置任意数量的附着点。此方法接受一个 EntityAttachment(定义要考虑的附着点)和三个浮点数来定义位置(x/y/z)。位置应相对于附着点的默认值定义。
原版定义了以下四个 EntityAttachments:
| 名称(Name) | 默认值(Default) | 用途(Usages) |
|---|---|---|
PASSENGER | 碰撞箱的中心X/顶部Y/中心Z | 可骑乘实体,例如马,以定义乘客出现的位置 |
VEHICLE | 碰撞箱的中心X/底部Y/中心Z | 所有实体,以定义它们在骑乘另一个实体时出现的位置 |
NAME_TAG | 碰撞箱的中心X/顶部Y/中心Z | 定义实体名称标签(如果适用)出现的位置 |
WARDEN_CHEST | 碰撞箱的中心X/中心Y/中心Z | 由监守者使用,定义音波攻击的起源位置 |
PASSENGER 和 VEHICLE 相关,因为它们用于相同的上下文。首先,应用 PASSENGER 来定位骑乘者。然后,在骑乘者身上应用 VEHICLE。
每个附着点可以被看作是从 EntityAttachment 到 List<Vec3> 的映射。实际使用的点数取决于消费系统。例如,船和骆驼会使用两个 PASSENGER 点,而像马或矿车这样的实体只使用一个 PASSENGER 点。
EntityType.Builder 也有一些与 EntityAttachments 相关的辅助方法:
#passengerAttachment():用于定义PASSENGER附着点。有两个变体。- 一个变体接受
Vec3...附着点。 - 另一个接受
float...,它通过将每个浮点数转换为使用给定浮点数作为y值,并将x和z设置为0的Vec3,转发到Vec3...变体。
- 一个变体接受
#vehicleAttachment():用于定义VEHICLE附着点。接受一个Vec3。#ridingOffset():用于定义VEHICLE附着点。接受一个浮点数,并将其转发给#vehicleAttachment(),附带一个x和z值为0、y值为传入浮点数负值的Vec3。#nameTagOffset():用于定义NAME_TAG附着点。接受一个浮点数,用作y值,x和z值使用0。
或者,可以通过调用 EntityAttachments#builder() 然后在该构建器上调用 #attach() 来自己定义附着点,如下所示:
// 在某个 EntityType<?> 的创建过程中
EntityType.Builder.of(...)
// 这个 EntityAttachment 将使名称标签漂浮在地面上方半格。
// 如果未设置,则默认为实体的碰撞箱高度。
.attach(EntityAttachment.NAME_TAG, 0, 0.5f, 0)
.build();
实体类层次结构(Entity Class Hierarchy)
由于实体类型众多,存在一个复杂的 Entity 子类层次结构。当选择制作自己的实体时要扩展哪个类时,了解这些很重要,因为你可以通过重用它们的代码节省大量工作。
原版实体层次结构如下所示(红色类为 abstract,蓝色类不是):
我们来分解一下:
Projectile:各种抛射物的基类,包括箭、火球、雪球、烟花和类似实体。更多信息见下文。LivingEntity:任何"活着"的事物的基类,意思是它具有诸如生命值、装备、状态效果(Mob Effects)和其他一些属性。包括诸如怪物、动物、村民 和玩家之类的事物。更多信息请参阅活体实体(Living Entities)文章。BlockAttachedEntity:不可移动且附着在方块上的实体的基类。包括拴绳结、物品展示框和画。子类主要用于重用通用代码。PartEntity:一个由NeoForge添加的部分实体(Part Entities)基类,即由多个较小实体组成的实体。EnderDragonPart已被补丁以扩展PartEntity而不是Entity。VehicleEntity:船和矿车的基类。虽然这些实体与LivingEntitys 在生命值的概念上有些相似,但它们与LivingEntitys 没有太多其他共同属性,因此保持分离。子类主要用于重用通用代码。
还有一些实体是 Entity 的直接子类,仅仅因为没有其他合适的父类。其中大多数应该是不言自明的:
AreaEffectCloud(滞留药水云)EndCrystal(末地水晶)EvokerFangs(唤魔者尖牙)ExperienceOrb(经验球)EyeOfEnder(末影之眼)FallingBlockEntity(下落的沙子、沙砾等)ItemEntity(掉落的物品)LightningBolt(闪电)OminousItemSpawner(用于持续生成试炼刷怪器的战利品)PrimedTnt(点燃的TNT)
图中和列表中未包括地图制作者实体(Display, Interaction, Marker)。
抛射物(Projectiles)
抛射物(Projectiles)是实体的一个子类。它们的共同点是朝一个方向飞行直 到击中某物,并且它们被分配了一个所有者(例如,玩家或骷髅可以是箭的所有者,恶魂可以是火球的所有者)。
抛射物的类层次结构如下所示(红色类为 abstract,蓝色类不是):
值得注意的是 Projectile 的三个直接抽象子类:
AbstractArrow:这个类涵盖了不同种类的箭,以及三叉戟。一个重要的共同属性是它们不会直线飞行,而是受重力影响。AbstractHurtingProjectile:这个类涵盖了风弹、各种火球和凋零骷髅头。这些是造成伤害且不受重力影响的抛射物。ThrowableProjectile:这个类涵盖了诸如鸡蛋、雪球和末影珍珠之类的东西。像箭一样,它们受重力影响,但与箭不同的是,击中目标时不会造成伤害。它们也都是在使用相应[物品(Item)]时生成的。
可以通过扩展 Projectile 或合适的子类来创建新的抛射物,然后重写添加功能所需的方法。常见需要重写的方法包括:
#shoot:计算并设置抛射物上的正确速度。#onHit:击中某物时调用。#onHitEntity:当击中的某物是[实体(Entity)]时调用。#onHitBlock:当击中的某物是[方块(Block)]时调用。
#getOwner和#setOwner,分别用于获取和设置所有实体。#deflect,根据传入的ProjectileDeflection枚举值使抛射物偏转。#onDeflection,从#deflect调用,用于任何偏转后的行为。