跳到主要内容
版本:1.21.4

实体(Entities)

实体是世界中的对象,可以以各种方式与世界交互。常见示例包括生物、抛射物、可骑乘对象,甚至玩家。每个实体由多个系统组成,乍一看可能不容易理解。本节将分解与构建实体并使其按照模组开发者意图行为相关的一些关键组件。

术语(Terminology)

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

更复杂的实体可能需要更多部分。例如,许多更复杂的EntityRenderer使用底层的EntityModel实例。或者,自然生成的实体将需要某种生成机制(spawn mechanism)

EntityType

EntityTypeEntity之间的关系类似于ItemsItemStacks之间的关系。像Item一样,EntityType是单例,注册到其相应的注册表(实体类型注册表),并保存该类型所有实体通用的一些值,而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",
// 实体类型,使用构建器创建。
() -> 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)
// 眼睛高度,从尺寸底部开始以方块为单位。默认为高度 * 0.85。
// 必须在#sized之后调用才能生效。
.eyeHeight(0.5f)
// 禁用通过/summon召唤实体。
.noSummon()
// 防止实体保存到磁盘。
.noSave()
// 使实体免疫火焰。
.fireImmune()
// 使实体免疫来自特定方块的伤害。原版使用此功能使
// 狐狸免疫甜浆果丛,凋灵和凋灵骷髅免疫凋灵玫瑰,
// 以及北极熊、雪傀儡和流浪者免疫细雪。
.immuneTo(Blocks.POWDER_SNOW)
// 禁用生成处理程序中限制实体生成距离的规则。
// 这意味着无论距离玩家多远,此实体都可以生成。
// 原版为掠夺者和潜影贝启用此功能。
.canSpawnFarFromPlayer()
// 客户端保持实体加载的范围,以区块为单位。
// 原版的值各不相同,但通常在8或10左右。默认为5。
// 请注意,如果此值大于客户端的区块视距,
// 则实际上使用该区块视距。
.clientTrackingRange(8)
// 为此实体发送更新包的频率,每x刻一次。对于具有可预测移动模式的实体,
// 例如抛射物,此值设置得更高。默认为3。
.updateInterval(10)
// 使用资源键构建实体类型。第二个参数应与实体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

另请参见自然生成(Natural Spawning)

实体的MobCategory确定实体的一些属性,这些属性与生成和消失(spawning and despawning)相关。原版默认添加了八个MobCategory

名称生成上限示例
MONSTER70各种怪物
CREATURE10农场动物、狼等
AMBIENT15蝙蝠
AXOLOTLS5美西螈
UNDERGROUND_WATER_CREATURE5发光鱿鱼
WATER_CREATURE5海豚、鱿鱼
WATER_AMBIENT20
MISC-非生物实体,例如船、矿车、盔甲架等

生成上限是玩家周围17x17区块区域内可以存在的该类别实体的最大数量。MISC的生成上限被忽略,因为该类别中的实体不会自然生成。

备注

生成上限不是硬性限制。有可能超过生成上限,例如通过生物刷怪笼、繁殖或命令。

实体类(The Entity Class)

首先,我们创建一个Entity子类。除了构造函数外,Entity(这是一个抽象类)定义了四个我们需要实现的必需方法。前三个将在数据和网络文章(Data and Networking article)中解释,以免进一步膨胀本文,而#hurtServer伤害实体部分(Damaging Entities section)中解释。

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,但通常使用其许多子类之一作为基础更有意义。有关更多信息,请参见实体类层次结构(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实例并将其添加到世界中,如下所示:

// 在某个具有可用世界的方法中,仅在服务器上
if (!level.isClientSide()) {
MyEntity entity = new MyEntity(level, 100.0, 200.0, 300.0);
level.addFreshEntity(entity);
}

或者,您也可以调用EntityType#spawn,这在生成生物实体(living entities)时特别推荐,因为它执行一些额外的设置,例如触发生成事件(event)

这将用于几乎所有非生物实体。玩家显然不应由您自己生成,Mobs有它们自己的生成方式(尽管它们也可以通过#addFreshEntity添加),原版抛射物(projectiles)Projectile类中也有用于生成的静态辅助方法。

伤害实体(Damaging Entities)

另请参见左键单击物品(Left-Clicking an Item)

虽然不是所有实体都有生命值的概念,但它们都可以受到伤害。这不仅用于生物和玩家等事物:如果您想到物品实体(掉落的物品),它们也可以受到火或仙人掌等来源的伤害,在这种情况下,它们通常立即被删除。

可以通过调用Entity#hurtEntity#hurtOrSimulate来伤害实体,两者之间的区别在下面解释。两种方法都接受两个参数:DamageSource和伤害量,作为半心的浮点数。例如,调用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

也可以通过事件修改对不属于您的实体(即由Minecraft或其他模组添加的实体)造成的伤害。这些事件包含许多特定于LivingEntity的代码;因此,它们的文档位于生物实体文章(Living Entities article)中的伤害事件部分(Damage Events section)

实体刻更新(Ticking Entities)

通常,您会希望您的实体每刻执行某些操作(例如移动)。此逻辑分布在几个方法中:

  • #tick:这是中央刻更新方法,是您在99%情况下想要重写的方法。
    • 默认情况下,这转发到#baseTick,但几乎每个子类都会重写此方法。
  • #baseTick:此方法处理更新所有实体通用的一些值,包括"着火"状态、细雪冻结、游泳状态和通过传送门。LivingEntity还在此处处理溺水、方块内伤害和伤害追踪器更新。如果您想更改或添加该逻辑,请重写此方法。
    • 默认情况下,Entity#tick将转发到此方法。
  • #rideTick:此方法为其他实体的乘客调用,例如骑马的玩家,或由于使用/ride命令而骑乘另一个实体的任何实体。
    • 默认情况下,这执行一些检查然后调用#tick。骷髅和玩家重写此方法以特殊处理骑乘实体。

此外,实体有一个名为tickCount的字段,表示实体在世界中存在的时间(以刻为单位),以及一个名为firstTick的布尔字段,这应该是不言自明的。例如,如果您想每5刻生成一个粒子(spawn a particle),可以使用以下代码:

@Override
public void tick() {
// 除非有充分理由,否则始终调用super。
super.tick();
// 每5刻运行一次此代码,并确保我们在服务器上生成粒子。
if (this.tickCount % 5 == 0 && !level().isClientSide()) {
level().addParticle(...);
}
}

拾取实体(Picking Entities)

另请参见中键单击(Middle-Clicking)

拾取是选择玩家当前正在看的东西以及随后拾取关联物品的过程。中键单击的结果,称为"拾取结果",可以由您的实体修改(请注意,Mob类将为您选择正确的刷怪蛋):

@Override
@Nullable
public ItemStack getPickResult() {
// 假设MY_CUSTOM_ITEM是DeferredItem<?>,有关更多信息,请参见物品文章。
// 如果实体不应可拾取,建议在此处返回null。
return new ItemStack(MY_CUSTOM_ITEM.get());
}

虽然实体通常应该是可拾取的,但有一些特殊情况不希望如此。原版的一个用例是末影龙,它由多个部分组成。父实体禁用了拾取,但部分重新启用了它,以便更精细的碰撞箱调整。

如果您有类似的特殊用例,您的实体也可以完全禁用拾取,如下所示:

@Override
public boolean isPickable() {
// 如果需要,可以在此处执行额外检查。
return false;
}

如果您想自己进行拾取(即射线投射),可以在要开始射线投射的实体上调用Entity#pick。这将返回一个HitResult,您可以进一步检查射线投射到底击中了什么。

实体附件(Entity Attachments)

不要与数据附件(Data Attachments)混淆。

实体附件用于定义实体的视觉附着点。使用此系统,可以定义乘客或名称标签等相对于实体本身显示的位置。实体本身仅控制附件的默认位置,然后附件可以定义相对于该默认位置的偏移。

构建EntityType时,可以通过调用EntityType.Builder#attach设置任意数量的附着点。此方法接受一个EntityAttachment,它定义要考虑的附件,以及三个浮点数来定义位置(x/y/z)。位置应相对于附件的默认值位置定义。

原版定义了以下四个EntityAttachments:

名称默认值用途
PASSENGER碰撞箱的中心X/顶部Y/中心Z可骑乘实体,例如马,定义乘客出现的位置
VEHICLE碰撞箱的中心X/底部Y/中心Z所有实体,定义它们骑乘另一个实体时出现的位置
NAME_TAG碰撞箱的中心X/顶部Y/中心Z定义实体的名称标签出现的位置(如果适用)
WARDEN_CHEST碰撞箱的中心X/中心Y/中心Z由监守者使用,定义音爆攻击的起源位置
信息

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

每个附件可以看作是从EntityAttachmentList<Vec3>的映射。实际使用的点数取决于消费系统。例如,船和骆驼将使用两个PASSENGER点,而像马或矿车这样的实体将只使用一个PASSENGER点。

EntityType.Builder还有一些与EntityAttachments相关的辅助方法:

  • #passengerAttachment(): 用于定义PASSENGER附件。有两种变体。
    • 一种变体接受一个Vec3...的附件点。
    • 另一种接受一个float...,通过将每个浮点数转换为使用给定浮点数作为y值的Vec3,并将x和z设置为0,转发到Vec3...变体。
  • #vehicleAttachment(): 用于定义VEHICLE附件。接受一个Vec3
  • #ridingOffset(): 用于定义VEHICLE附件。接受一个浮点数,并使用x和z值设置为0、y值设置为传入浮点数的负值的Vec3转发到#vehicleAttachment()
  • #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: 任何"生物"的基类,从它具有生命值、装备、生物效果和其他一些属性的意义上说。包括怪物、动物、村民和玩家等。在生物实体文章(Living Entities article)中了解更多关于它们的信息。
  • BlockAttachedEntity: 不可移动且附着在方块上的实体的基类。包括拴绳结、物品展示框和画。子类主要用于重用通用代码。
  • PartEntity: 部分实体的基类,即由多个较小实体组成的实体。原版目前仅将此用于末影龙。
  • VehicleEntity: 船和矿车的基类。虽然这些实体与LivingEntitys松散地共享生命值的概念,但它们与它们不共享许多其他属性,因此保持分离。子类主要用于重用通用代码。

还有几个实体是Entity的直接子类,仅仅因为没有其他合适的超类。其中大多数应该是不言自明的:

  • AreaEffectCloud(滞留药水云)
  • EndCrystal
  • EvokerFangs
  • ExperienceOrb
  • EyeOfEnder
  • FallingBlockEntity(下落的沙子、沙砾等)
  • ItemEntity(掉落的物品)
  • LightningBolt
  • OminousItemSpawner(用于持续生成试炼刷怪器的战利品)
  • PrimedTnt

此图表和列表中未包含地图制作者实体(显示、交互和标记)。

弹射物(Projectiles)

弹射物是实体的一个子组。它们的共同点是它们朝一个方向飞行直到击中某物,并且它们有一个分配给它们的所有者(例如,玩家或骷髅将是箭的所有者,或恶魂将是火球的所有者)。

弹射物的类层次结构如下所示(红色类是abstract,蓝色类不是):

值得注意的是Projectile的三个直接抽象子类:

  • AbstractArrow: 这个类涵盖了不同种类的箭,以及三叉戟。一个重要的共同属性是它们不会直线飞行,而是受重力影响。
  • AbstractHurtingProjectile: 这个类涵盖了风弹、各种火球和凋零骷髅头。这些是不受重力影响的伤害性弹射物。
  • ThrowableProjectile: 这个类涵盖了像鸡蛋、雪球和末影珍珠这样的东西。像箭一样,它们受重力影响,但与箭不同,它们在击中目标时不会造成伤害。它们也都是通过使用相应的物品生成的。

可以通过扩展Projectile或合适的子类来创建新的弹射物,然后重写添加功能所需的方法。要重写的常见方法包括:

  • #shoot: 计算并设置弹射物上的正确速度。
  • #onHit: 当某物被击中时调用。
    • #onHitEntity: 当那个某物是一个实体时调用。
    • #onHitBlock: 当那个某物是一个方块时调用。
  • #getOwner#setOwner,分别获取和设置拥有实体。
  • #deflect,根据传递的ProjectileDeflection枚举值偏转弹射物。
  • #onDeflection,从#deflect调用以进行任何偏转后行为。