实体

实体

实体是存在于世界中、能够以多种方式与世界交互的对象,例如生物、弹射物、可骑乘对象,甚至玩家。每个实体都涉及多个系统,初次接触时可能不容易理解。本节拆解实体构建和行为控制中的关键部分,帮助你实现所需功能。

术语

简单实体由三部分组成:

复杂实体可能需要更多组件。例如,复杂的 EntityRenderer 往往还使用底层 EntityModel 实例;自然生成的实体则需要某种生成机制。

EntityType

EntityType 与 Entity 的关系,类似 Item 与 ItemStack。与 Item 一样,EntityType 是注册到对应注册表(实体类型注册表)的单例,保存该类型所有实体共用的一些数值。Entity 则类似 ItemStack,是单例类型对应的实例,保存当前实体独有的数据。关键差别在于,多数行为并不定义在单例 EntityType 中,而是定义在实例化的 Entity 类中。

假设已有继承 Entity 的 MyEntity 类,参见下文,可以创建 EntityType 注册器并注册实体类型。除末尾的 #build 外,EntityType.Builder 的所有方法都是可选的。

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",
    // The entity type, created using a builder.
    () -> EntityType.Builder.of(
        // An EntityType.EntityFactory<T>, where T is the entity class used - MyEntity in this case.
        // You can think of it as a BiFunction<EntityType<T>, Level, T>.
        // This is commonly a reference to the entity constructor.
        MyEntity::new,
        // The MobCategory our entity uses. This is mainly relevant for spawning.
        // See below for more information.
        MobCategory.MISC
    )
    // The width and height, in blocks. The width is used in both horizontal directions.
    // This also means that non-square footprints are not supported. Default is 0.6f and 1.8f.
    .sized(1.0f, 1.0f)
    // A multiplicative factor (scalar) used by mobs that spawn in varying sizes.
    // In vanilla, these are only slimes and magma cubes, both of which use 4.0f.
    .spawnDimensionsScale(4.0f)
    // The eye height, in blocks from the bottom of the size. Defaults to height * 0.85.
    // This must be called after #sized to have an effect.
    .eyeHeight(0.5f)
    // Disables the entity being summonable via /summon.
    .noSummon()
    // Prevents the entity from being saved to disk.
    .noSave()
    // Makes the entity fire immune.
    .fireImmune()
    // Makes the entity immune to damage from a certain block. Vanilla uses this to make
    // foxes immune to sweet berry bushes, withers and wither skeletons immune to wither roses,
    // and polar bears, snow golems and strays immune to powder snow.
    .immuneTo(Blocks.POWDER_SNOW)
    // Disables a rule in the spawn handler that limits the distance at which entities can spawn.
    // This means that no matter the distance to the player, this entity can spawn.
    // Vanilla enables this for pillagers and shulkers.
    .canSpawnFarFromPlayer()
    // The range in which the entity is kept loaded by the client, in chunks.
    // Vanilla values for this vary, but it's often something around 8 or 10. Defaults to 5.
    // Be aware that if this is greater than the client's chunk view distance,
    // then that chunk view distance is effectively used here instead.
    .clientTrackingRange(8)
    // How often update packets are sent for this entity, in once every x ticks. This is set to higher values
    // for entities that have predictable movement patterns, for example projectiles. Defaults to 3.
    .updateInterval(10)
    // Build the entity type using a resource key. The second parameter should be the same as the entity id.
    .build(ResourceKey.create(
        Registries.ENTITY_TYPE,
        Identifier.fromNamespaceAndPath("examplemod", "my_entity")
    ))
);

// Shorthand version to avoid boilerplate. The following call is the same as
// ENTITY_TYPES.register("my_entity", () -> EntityType.Builder.of(MyEntity::new, MobCategory.MISC).build(
//     ResourceKey.create(Registries.ENTITY_TYPE, Identifier.fromNamespaceAndPath("examplemod", "my_entity"))
// );
public static final Supplier<EntityType<MyEntity>> MY_ENTITY =
    ENTITY_TYPES.registerEntityType("my_entity", MyEntity::new, MobCategory.MISC);

// Shorthand version that still allows calling additional builder methods
// by supplying a UnaryOperator<EntityType.Builder> parameter.
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 决定一些与生成和消失相关的属性。原版默认有八种:

名称 生成上限 示例
MONSTER 70 各类怪物
CREATURE 10 各类动物
AMBIENT 15 蝙蝠
AXOLOTS 5 美西螈
UNDERGROUND_WATER_CREATURE 5 发光鱿鱼
WATER_CREATURE 5 鱿鱼、海豚
WATER_AMBIENT 20 鱼
MISC 不适用 所有非生物实体,例如弹射物;使用该分类会使实体完全无法自然生成

还有一些属性只在一两种分类中采用特殊值:

  • isFriendly:MONSTER 为 false,其余为 true。
  • isPersistent:CREATURE 和 MISC 为 true,其余为 false。
  • despawnDistance:WATER_AMBIENT 为 64,其余为 128。

说明 MobCategory 是可扩展枚举,可以添加自定义枚举项。如果添加,也需要为该分类的实体提供生成机制。

实体类

首先创建 Entity 子类。Entity 是抽象类,除构造方法外,还定义了四个必须实现的方法。为控制本文篇幅,前三个会在实体数据与网络同步中介绍,#hurtServer 则在实体受伤一节介绍。

public class MyEntity extends Entity {
    // We inherit this constructor without the bound on the generic wildcard.
    // The bound is needed for registration below, so we add it here.
    public MyEntity(EntityType<? extends MyEntity> type, Level level) {
        super(type, level);
    }

    // See the Data and Networking article for information about these methods.
    @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,但通常选择它的某个现有子类作为基础更合适。更多信息请参阅实体类继承层级。

需要时,例如从代码生成实体时,也可以添加自定义构造方法。通常直接引用注册对象,固定指定实体类型:

public MyEntity(EntityType<? extends MyEntity> type, Level level, double x, double y, double z) {
    // Delegates to the factory constructor, using the EntityType we registered before.
    this(type, level);
    this.setPos(x, y, z);
}

警告 自定义构造方法不应恰好包含两个参数,以免与上面的 (EntityType, Level) 构造方法混淆。

接下来就可以为实体实现所需功能。以下小节介绍一些常见用途。

实体数据存储

参阅实体/数据与网络同步。

实体渲染

参阅实体/实体渲染器。

生成实体

现在启动游戏并进入世界,唯一可用的生成方式是 /summon 命令,前提是没有调用 EntityType.Builder#noSummon。

通常还需要以其他方式生成实体。最简单的是调用 LevelWriter#addFreshEntity,它接受一个 Entity 实例并将其加入世界:

// In some method that has a level available, only on the server
if (!level.isClientSide()) {
    MyEntity entity = new MyEntity(level, 100.0, 200.0, 300.0);
    level.addFreshEntity(entity);
}

也可以调用 EntityType#spawn。生成生物实体时尤其推荐它,因为它会进行一些额外设置,例如触发生成事件。

几乎所有非生物实体都可以采用这种方式。显然不应自行生成玩家;Mob 有自己的生成方式,不过也可以通过 #addFreshEntity 添加;原版弹射物的 Projectile 类还提供了生成用静态辅助方法。

实体受伤

另见:左键使用物品。

并非所有实体都有生命值概念,但它们都可以接收伤害。这不只用于生物和玩家,例如掉落物实体也会受到火或仙人掌的伤害,通常会立即被移除。

可以调用 Entity#hurt 或 Entity#hurtOrSimulate 使实体受伤,两者差别见下文。两个方法均接受 DamageSource 和伤害数值;伤害为以半颗心为单位的浮点数。例如,entity.hurt(entity.damageSources().wither(), 4.25) 会造成略多于两颗心的凋零伤害。

实体也可以修改受伤行为。由于 #hurt 是 final,不能重写它;应使用分别处理两端伤害逻辑的 #hurtServer 和 #hurtClient。#hurtClient 通常用于告知客户端攻击成功,即使实际结果未必如此,主要是为了播放攻击声音等效果。修改伤害行为主要关注 #hurtServer,可以这样重写:

@Override
// The boolean return value determines whether the entity was actually damaged or not.
public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) {
    if (damageSource.is(DamageTypeTags.IS_FIRE)) {
        // This assumes that super#hurtServer() is implemented. Common other ways to do this
        // are to set some field yourself. Vanilla implementations vary greatly across different entities.
        // Notably, living entities usually call #actuallyHurt, which in turn calls #setHealth.
        return super.hurtServer(level, damageSource, amount * 2);
    } else {
        return false;
    }
}

这也是 Entity#hurt 与 Entity#hurtOrSimulate 的区别:前者仅在服务端运行并调用 Entity#hurtServer;后者在两端都运行,并根据当前端调用 Entity#hurtServer 或 Entity#hurtClient。

也可以通过事件修改 Minecraft 或其他 Mod 所定义实体受到的伤害。这些事件包含许多专门针对 LivingEntity 的代码,因此放在生物实体文档的伤害事件一节介绍。

实体刻更新

实体经常需要每个游戏刻执行操作,例如移动。这些逻辑分散在以下方法中:

  • #tick:主要刻更新方法,绝大多数情况下应重写它。
    • 默认转发给 #baseTick,但几乎所有子类都会重写它。
  • #baseTick:更新所有实体共用的一些状态,包括着火、细雪冻结、游泳和穿过传送门。LivingEntity 还在此处理溺水、卡入方块造成的伤害以及伤害追踪器更新。需要改变或扩充这些逻辑时重写它。
    • 默认情况下,Entity#tick 会转发到此方法。
  • #rideTick:实体作为其他实体的乘客时调用,例如玩家骑马,或通过 /ride 命令骑乘的实体。
    • 默认执行一些检查后调用 #tick。骷髅和玩家重写此方法,特殊处理骑乘行为。

此外,实体还有 tickCount 字段,表示其在世界中存在的游戏刻数;以及含义直观的布尔字段 firstTick。例如,每 5 刻生成一个粒子可以这样实现:

@Override
public void tick() {
    // Always call super unless you have a good reason not to.
    super.tick();
    // Run this code once every 5 ticks.
    if (this.tickCount % 5 == 0) {
        this.level().addParticle(...);
    }
}

选取实体

另见:鼠标中键操作。

选取是指确定玩家当前注视的对象,并获取对应物品的过程。实体可以修改鼠标中键选取的结果;注意,Mob 类会自动选择正确的刷怪蛋:

@Override
@Nullable
public ItemStack getPickResult() {
    // Assumes that MY_CUSTOM_ITEM is a DeferredItem<?>, see the Items article for more information.
    // If the entity should not be pickable, it is advised to return null here.
    return new ItemStack(MY_CUSTOM_ITEM.get());
}

实体通常应允许选取,但某些特殊情况并非如此。例如末影龙由多个部分组成,主体实体禁用选取,各部分再单独启用,以更精确地控制碰撞箱。

如果有类似需求,也可以完全禁用实体选取:

@Override
public boolean isPickable() {
    // Additional checks may be performed here if needed.
    return false;
}

若要自行进行选取,也就是射线检测,可以在作为射线起点的实体上调用 Entity#pick。它返回 HitResult,可进一步检查命中的具体对象。

实体附着点

注意与数据附件区分。

实体附着点用于定义视觉上的连接位置,例如乘客或名称标签相对于实体显示在哪里。实体控制附着点的默认位置,附着点再定义相对于默认位置的偏移。

构建 EntityType 时,可通过 EntityType.Builder#attach 设置任意数量的附着点。该方法接受用于指定附着类型的 EntityAttachment,以及表示 x、y、z 位置的三个浮点数。位置应相对于对应附着类型的默认位置定义。

原版定义了四种 EntityAttachment:

名称 默认位置 用途
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 点,马和矿车只使用一个。

EntityType.Builder 还提供一些辅助方法:

  • #passengerAttachment():定义 PASSENGER,有两种重载。
    • 一种接受附着点的 Vec3...。
    • 另一种接受 float...,将每个值转换为 x、z 为 0、y 为该值的 Vec3,再转发给前一种。
  • #vehicleAttachment():定义 VEHICLE,接受 Vec3。
  • #ridingOffset():定义 VEHICLE,接受浮点数,将其取负作为 y,x、z 设为 0,再传给 #vehicleAttachment()。
  • #nameTagOffset():定义 NAME_TAG,接受浮点数作为 y,x、z 设为 0。

也可以调用 EntityAttachments#builder(),再在构建器上调用 #attach(),自行定义附着点:

// In some EntityType<?> creation
EntityType.Builder.of(...)
    // This EntityAttachment will make name tags float half a block above the ground.
    // If this is not set, it will default to the entity's hitbox height.
    .attach(EntityAttachment.NAME_TAG, 0, 0.5f, 0)
    .build();

实体类继承层级

实体种类众多,因此 Entity 的子类继承关系较复杂。创建实体时,了解这些关系有助于选择合适父类,复用现有代码,减少工作量。

原版实体继承关系如下:

graph LR;
    Entity --> Projectile & LivingEntity & BlockAttachedEntity;
    BlockAttachedEntity --> LeashFenceKnotEntity & HangingEntity;
    HangingEntity --> ItemFrame --> GlowItemFrame;
    HangingEntity --> Painting;
    Entity --> PartEntity --> EnderDragonPart;
    Entity --> VehicleEntity;
    VehicleEntity --> AbstractBoat;
    AbstractBoat --> AbstractChestBoat --> ChestBoat & ChestRaft;
    AbstractBoat --> Boat & Raft;
    VehicleEntity --> AbstractMinecart;
    AbstractMinecart --> AbstractMinecartContainer & Minecart & MinecartCommandBlock & MinecartFurnace & MinecartSpawner & MinecartTNT;
    AbstractMinecartContainer --> MinecartChest & MinecartHopper;

    class Entity,Projectile,LivingEntity,BlockAttachedEntity,HangingEntity,PartEntity,VehicleEntity,AbstractBoat,AbstractChestBoat,AbstractMinecart,AbstractMinecartContainer yellow;
    class LeashFenceKnotEntity,ItemFrame,GlowItemFrame,Painting,EnderDragonPart,ChestBoat,ChestRaft,Boat,Raft,MinecartChest,MinecartHopper,Minecart,MinecartCommandBlock,MinecartCommandBlock,MinecartFurnace,MinecartSpawner,MinecartTNT blue;

黄色类是抽象类,蓝色类不是抽象类。

各类的主要用途如下:

  • Projectile:各类弹射物的基类,包括箭、火球、雪球、烟花等,详见下文。
  • LivingEntity:具有生命值、装备、状态效果等属性的“生物”基类,包括怪物、动物、村民和玩家,详见生物实体文档。
  • BlockAttachedEntity:不移动并附着于方块的实体基类,包括拴绳结、物品展示框和画。子类主要用于复用共用代码。
  • PartEntity:NeoForge 添加的实体部件基类,用于由多个小实体组成的实体。EnderDragonPart 通过补丁改为继承它,而非 Entity。
  • VehicleEntity:船和矿车的基类。它们虽然也有类似生命值的概念,但与 LivingEntity 的其他共性较少,因此单独分类。其子类主要用于复用共用代码。

还有一些实体直接继承 Entity,因为没有其他适合的父类。大部分名称即可说明用途:

  • AreaEffectCloud:滞留药水效果云。
  • EndCrystal:末地水晶。
  • EvokerFangs:唤魔者尖牙。
  • ExperienceOrb:经验球。
  • EyeOfEnder:末影之眼。
  • FallingBlockEntity:下落的沙子、沙砾等方块。
  • ItemEntity:掉落物。
  • LightningBolt:闪电。
  • OminousItemSpawner:持续生成试炼刷怪笼战利品的实体。
  • PrimedTnt:已激活的 TNT。

面向地图制作者的展示实体、交互实体和标记实体未包含在这张图和列表中。

弹射物

弹射物是实体的一个子类群体,共同特点是沿某方向飞行,直到命中对象,并具有一个所有者。例如箭的所有者可以是玩家或骷髅,火球的所有者可以是恶魂。

弹射物继承关系如下,红色表示抽象类,蓝色表示非抽象类:

graph LR;
    Projectile --> AbstractArrow --> Arrow & SpectralArrow & ThrownTrident;
    Projectile --> AbstractHurtingProjectile;
    AbstractHurtingProjectile --> AbstractWindCharge --> BreezeWindCharge & WindCharge;
    AbstractHurtingProjectile --> DragonFireball;
    AbstractHurtingProjectile --> Fireball --> LargeFireball & SmallFireball;
    AbstractHurtingProjectile --> WitherSkull;
    Projectile --> FireworkRocketEntity & FishingHook & LlamaSpit & ShulkerBullet;
    Projectile --> ThrowableProjectile --> ThrowableItemProjectile;
    ThrowableItemProjectile --> AbstractThrownPotion --> ThrownLingeringPotion & ThrownSplashPotion;
    ThrowableItemProjectile --> Snowball & ThrownEgg & ThrownEnderpearl & ThrownExperienceBottle;

    class Projectile,AbstractArrow,AbstractHurtingProjectile,AbstractWindCharge,Fireball,ThrowableProjectile,ThrowableItemProjectile,AbstractThrownPotion yellow;
    class Arrow,SpectralArrow,ThrownTrident,BreezeWindCharge,WindCharge,DragonFireball,LargeFireball,SmallFireball,WitherSkull,FireworkRocketEntity,FishingHook,LlamaSpit,ShulkerBullet,Snowball,ThrownEgg,ThrownEnderpearl,ThrownExperienceBottle,ThrownLingeringPotion,ThrownSplashPotion blue;

黄色类是抽象类,蓝色类不是抽象类。

尤其需要注意 Projectile 的三个直接抽象子类:

  • AbstractArrow:涵盖各种箭和三叉戟。它们不会始终直线飞行,而是受到重力影响。
  • AbstractHurtingProjectile:涵盖风弹、各种火球和凋灵之首,能够造成伤害且不受重力影响。
  • ThrowableProjectile:涵盖鸡蛋、雪球和末影珍珠等。它们与箭一样受到重力影响,但与箭不同,命中目标时不造成伤害。它们都通过使用对应物品生成。

创建弹射物时,可以继承 Projectile 或合适的子类,重写实现所需功能的方法。常见重写点包括:

  • #shoot:计算并设置弹射物速度。
  • #onHit:命中对象时调用。
    • #onHitEntity:命中实体时调用。
    • #onHitBlock:命中方块时调用。
  • #getOwner 与 #setOwner:分别获取、设置所有者实体。
  • #deflect:根据传入的 ProjectileDeflection 枚举值偏转弹射物。
  • #onDeflection:由 #deflect 调用,处理偏转后的行为。

来源: NeoForged/Documentation(26.1)。中文翻译依据提交 4a680cdd4d15。