生物实体、生物与玩家

生物实体、生物与玩家

生物实体是实体中的一大类,都继承自 LivingEntity。其中包括通过 Mob 子类实现的生物、通过 Player 子类实现的玩家,以及通过 ArmorStand 子类实现的盔甲架。

生物实体具有普通实体没有的额外特征,例如属性、状态效果、伤害追踪等。

生命值、伤害与治疗

另见:属性。

生物实体与其他实体最显著的区别之一是完整的生命值系统,通常具有最大生命值、当前生命值,有些还有护甲或自然恢复机制。

默认最大生命值由 minecraft:max_health 属性决定,生成时会将当前生命值设为相同数值。调用 Entity#hurtServer 造成伤害后,当前生命值按伤害计算结果减少。僵尸等许多实体默认会一直保留降低后的生命值,而玩家等实体可以恢复失去的生命值。

要获取或设置最大生命值,可直接读写属性:

// Get the attribute map of our entity.
AttributeMap attributes = entity.getAttributes();

// Get the max health of our entity.
float maxHealth = attributes.getValue(Attributes.MAX_HEALTH);
// Shortcut for the above.
maxHealth = entity.getMaxHealth();

// Setting the max health must either be done by getting the AttributeInstance and calling #setBaseValue, or by
// adding an attribute modifier. We will do the former here. Please refer to the Attributes article for more details.
attributes.getInstance(Attributes.MAX_HEALTH).setBaseValue(50);

受到伤害时,生物实体还会进行额外计算,例如考虑 minecraft:armor 属性,但属于 minecraft:bypasses_armor 标签的伤害类型除外,以及 minecraft:absorption 属性。还可以重写 #onDamageTaken 处理攻击后的行为;只有最终伤害大于零时才会调用它。

伤害事件

伤害处理流程较复杂,因此提供了多个可介入的事件,按下文列出的顺序触发。这些事件主要用于修改并非自己定义的实体所受伤害,例如 Minecraft 或其他 Mod 的实体,或修改所有实体受到的伤害。

所有事件共用 DamageContainer。攻击开始时创建新的容器,结束后丢弃。它保存原始 DamageSource、原始伤害,以及护甲、伤害吸收、附魔、状态效果等各项独立修改记录。容器会传给以下全部事件,可以检查已发生的修改,再按需处理。

EntityInvulnerabilityCheckEvent

这个事件可绕过或添加实体伤害免疫,也会对非生物实体触发。可以用它让实体免疫某种攻击,或移除已有免疫。

受技术条件限制,这个事件的处理应具有确定性,并且只依赖伤害类型。随机概率免疫、仅对一定伤害量生效的免疫等,应在下文的 LivingIncomingDamageEvent 中处理。

LivingIncomingDamageEvent

此事件仅在服务端触发,主要用于动态取消攻击,以及添加减伤修正回调。

动态取消攻击相当于添加非确定性的免疫,例如按概率取消伤害,或根据时间、伤害量决定是否免疫。固定不变的免疫应使用上面的 EntityInvulnerabilityCheckEvent。

减伤修正回调可修改某一部分减伤计算,例如让护甲减伤效果降低 50%。这一变化也会正确传递到后续状态效果等计算,使其基于变化后的伤害值运作。添加方式如下:

@SubscribeEvent // on the game event bus
public static void decreaseArmor(LivingIncomingDamageEvent event) {
    // We only apply this decrease to players and leave zombies etc. unchanged
    if (event.getEntity() instanceof Player) {
        // Add our reduction modifier callback.
        event.addReductionModifier(
            // The reduction to target. See the DamageContainer.Reduction enum for possible values.
            DamageContainer.Reduction.ARMOR,
            // The modification to perform. Gets the damage container and the base reduction as inputs,
            // and outputs the new reduction. Both input and output reductions are floats.
            (container, baseReduction) -> baseReduction * 0.5f
        );
    }
}

回调按添加顺序应用,因此优先级更高的事件处理器添加的回调会先执行。

LivingShieldBlockEvent

此事件可全面自定义盾牌格挡,例如添加额外格挡、禁止格挡、修改原版判断、调整盾牌或攻击物品受到的耐久损耗、改变格挡视角范围、只挡弹射物不挡近战或反之、无需举盾的被动格挡,以及只挡住一定比例的伤害等。

这个事件不适合处理与盾牌类物品无关的免疫或攻击取消。

ArmorHurtEvent

该事件在计算攻击对护甲的耐久损耗时触发,可以修改哪件护甲损失多少耐久,也可以使其不损失耐久。

LivingDamageEvent.Pre

此事件在实际施加伤害前立即触发。此时 DamageContainer 已填充完毕,可获取最终伤害值;攻击已被视为成功,因此事件不能再取消。

此时可以利用全部修饰信息精细调整伤害量,但护甲耐久损耗等操作已经完成。

LivingDamageEvent.Post

此事件在伤害已施加、伤害吸收已扣减、战斗追踪器已更新,且统计与游戏事件处理完毕后触发。由于攻击已经发生,不能取消。它通常用于攻击后的效果。注意,即使伤害为零也会触发,必要时应检查该值。

如果处理的是自己的实体,可以考虑改为重写 ILivingEntityExtension#onDamageTaken()。与 LivingDamageEvent.Post 不同,它只在伤害大于零时调用。

状态效果

参阅状态效果与药水。

装备

参阅实体上的容器。

继承层级

生物实体的继承层级较复杂。如前所述,它有三个直接子类:

graph LR;
    LivingEntity --> ArmorStand & Mob & Avatar;
    
    class LivingEntity,Mob,Avatar yellow;
    class ArmorStand blue;

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

其中 ArmorStand 没有子类,也是唯一的非抽象类,因此下面主要介绍 Mob 和 Avatar 的继承层级。

Mob 的继承层级

Mob 的继承层级如下:

graph LR;
    Mob --> AmbientCreature --> Bat;
    Mob --> EnderDragon & Ghast & Phantom & PathfinderMob;
    PathfinderMob --> AbstractGolem --> CopperGolem & IronGolem & Shulker & SnowGolem;
    PathfinderMob --> AgeableMob;
    AgeableMob --> AbstractVillager --> Villager & WanderingTrader;
    AgeableMob --> AgeableWaterCreature;
    AgeableWaterCreature --> Dolphin;
    AgeableWaterCreature --> Squid --> GlowSquid;
    AgeableMob --> Animal;
    PathfinderMob --> Allay & Monster & WaterAnimal;
    WaterAnimal --> AbstractFish;
    AbstractFish --> AbstractSchoolingFish --> Cod & Salmon & TropicalFish;
    AbstractFish --> Pufferfish & Tadpole;
    Mob --> Slime --> MagmaCube;

    class Mob,AmbientCreature,PathfinderMob,AbstractGolem,AgeableMob,AbstractVillager,AgeableWaterCreature,Animal,Monster,WaterAnimal,AbstractFish,AbstractSchoolingFish yellow;
    class Bat,CopperGolem,EnderDragon,Ghast,Phantom,IronGolem,Shulker,SnowGolem,Villager,WanderingTrader,Dolphin,Squid,GlowSquid,Allay,Cod,Salmon,TropicalFish,Pufferfish,Tadpole,Slime,MagmaCube blue;

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

图中未列出的其他生物实体都是 Animal 或 Monster 的子类。

这个层级看起来相当混乱。例如,蜜蜂、鹦鹉为什么不是飞行生物?继续查看 Animal 和 Monster 的子类时会更明显。本文不逐一展开,感兴趣时可以用 IDE 的“显示继承层级”功能查看。了解现状即可,无需过于纠结。

重点类包括:

  • PathfinderMob:包含寻路逻辑。
  • AgeableMob:包含年龄增长和幼年实体逻辑。僵尸等有幼年变体的怪物不继承它,而是继承 Monster。
  • Animal:大多数动物的父类,还包含 AbstractHorse、TamableAnimal 等抽象子类。
  • Monster:大多数怪物的抽象父类,也有 AbstractPiglin、AbstractSkeleton、Raider、Zombie 等抽象子类。
  • WaterAnimal:鱼、鱿鱼、海豚等水生动物的抽象父类。寻路方式差别较大,因此与其他动物分开。

Avatar 的继承层级

Avatar 不仅定义玩家,还定义了类似玩家的假人。根据所在端使用不同的类。除 FakePlayer 和 Mannequin 外,通常都不应自行构造 Avatar。

graph LR;
    Avatar --> Mannequin --> ClientMannequin;
    Avatar --> Player;
    Player --> AbstractClientPlayer --> LocalPlayer & RemotePlayer;
    Player --> ServerPlayer --> FakePlayer;

    class Avatar,Player,AbstractClientPlayer yellow;
    class Mannequin,ClientMannequin,LocalPlayer,RemotePlayer,ServerPlayer,FakePlayer blue;

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

  • AbstractClientPlayer:两种客户端玩家的基类,表示逻辑客户端上的玩家。
  • LocalPlayer:表示当前运行游戏的玩家。
  • RemotePlayer:表示多人游戏中 LocalPlayer 遇到的其他玩家,单人游戏不存在这类玩家。
  • ServerPlayer:表示逻辑服务端上的玩家。
  • FakePlayer:特殊的 ServerPlayer 子类,用于为需要玩家上下文的非玩家机制模拟玩家。
  • Mannequin:可摆姿势的玩家形态实体,通常没有 AI。
  • ClientMannequin:表示逻辑客户端上的假人。

生成

除 /summon 命令和代码中的 EntityType#spawn、Level#addFreshEntity 等常规方式外,Mob 还可通过其他方式生成。ArmorStand 使用常规方式即可;除 FakePlayer 外,不应自行实例化 Player。

刷怪蛋

通常会为生物注册一个刷怪蛋,但不是必需的。通过 SpawnEggItem 和 DataComponents#ENTITY_DATA 数据组件实现:

// Assume we have a DeferredRegister.Items called ITEMS
DeferredItem<SpawnEggItem> MY_ENTITY_SPAWN_EGG = ITEMS.registerItem("my_entity_spawn_egg",
    properties -> new SpawnEggItem(
        // The properties passed into the lambda.
        // Using `spawnEgg` to set the DataComponent.
        // This is done in the lambda to prevent the entity type from resolving before registration.
        properties.spawnEgg(MY_ENTITY_TYPE.get())
    ));

它和其他物品一样,需要加入创造模式物品栏,并添加客户端物品定义、模型和翻译。

生成位置规则

默认情况下实体可在任意位置生成,包括方块内部和半空中。要限制这种情况,需要在 RegisterSpawnPlacementsEvent 中注册生成位置规则:

@SubscribeEvent // on the mod event bus
public static void registerSpawnPlacements(RegisterSpawnPlacementsEvent event) {
    event.register(
        // The EntityType<?> to register the spawn placement for.
        MY_ENTITY.get(),
        // SpawnPlacementType is a functional interface that checks whether a given position is valid
        // for spawning. You can add your own SpawnPlacementType if needed,
        // see the implementation of SpawnPlacementTypes for reference.
        // Vanilla values include NO_RESTRICTIONS, IN_WATER, IN_LAVA and ON_GROUND.
        SpawnPlacementTypes.ON_GROUND,
        // The heightmap type to use. This provides the initial height to check during world generation.
        // Possible values are WORLD_SURFACE, OCEAN_FLOOR, MOTION_BLOCKING and MOTION_BLOCKING_NO_LEAVES.
        Heightmap.Types.WORLD_SURFACE,
        // A predicate to check additional spawn rules, e.g. light. Can either be specified here
        // as a lambda, or (more commonly) as a static method in the entity class.
        (entityType, level, spawnReason, pos, random) -> {
            // Make our mob only spawn on grass blocks.
            return level.getBlockState(pos.below()).is(Blocks.GRASS_BLOCK)
                || Mob.checkMobSpawnRules(entityType, level, spawnReason, pos, random);
        },
        // For our own mobs, any operation may be used.
        RegisterSpawnPlacementsEvent.Operation.OR
    );
}

也可以修改原版或其他 Mod 的规则。Operation.AND 用于增加限制,Operation.OR 用于提供其他允许生成的条件,Operation.REPLACE 完全替换规则,替换时应使用低优先级事件监听器:

@SubscribeEvent // on the mod event bus
public static void registerSpawnPlacements(RegisterSpawnPlacementsEvent event) {
    event.register(
        // The EntityType<?> to register the spawn placement for.
        MY_ENTITY.get(),
        // When not using Operation.REPLACE, leave the spawn placement type
        // and the heightmap type at null.
        null,
        null,
        // Like above, check the additional/alternate spawn rules.
        (entityType, level, spawnReason, pos, random) -> {
            return level.getBlockState(pos.below()).is(Blocks.GRASS_BLOCK)
                || Mob.checkMobSpawnRules(entityType, level, spawnReason, pos, random);
        },
        // Use the appropriate Operation here.
        RegisterSpawnPlacementsEvent.Operation.AND
    );
}

事件还提供两个便捷重载,其中 spawnPredicate 是前面使用的 lambda:

@SubscribeEvent // on the mod event bus
public static void registerSpawnPlacements(RegisterSpawnPlacementsEvent event) {
    // Identical to event.register(MY_ENTITY.get(), null, null, spawnPredicate, Operation.OR)
    event.register(MY_ENTITY.get(), spawnPredicate);
    // Identical to event.register(MY_ENTITY.get(), null, null, spawnPredicate, operation)
    event.register(MY_ENTITY.get(), spawnPredicate, operation);
}

自然生成

另见:实体/MobCategory、世界生成/生物群系修改器/添加生成项、添加生成成本,以及 Minecraft Wiki 的生成周期。

原文描述:MobCategory#isFriendly() 为真的实体(默认所有非怪物)每刻尝试自然生成,为假的实体(所有怪物)每 400 刻,即 20 秒尝试一次。如果 MobCategory#isPersistent() 为真,主要是动物,区块生成时也会执行这一过程。

对于每个区块和生物分类,首先检查是否达到生成上限。具体而言,会检查周边已加载区块中,该分类实体数量是否小于 MobCategory#getMaxInstancesPerChunk() * loadedChunks / 289。loadedChunks 最多为以当前区块为中心的 17×17 区块范围;受视距等条件影响,实际已加载区块可能更少。

接下来,还需要至少有一位玩家,其周围该分类实体数量少于 MobCategory#getMaxInstancesPerChunk(),该区块才能生成这一分类的实体。这里“周围”指实体与玩家距离小于等于 128。

满足条件后,从相关生物群系的生成数据中随机选择一个条目,并尝试寻找合适位置。最多随机尝试三次,全部失败就不生成。

示例

下面以平原生物群系中的动物为例说明。

原文描述:平原生物群系每刻尝试生成 CREATURE 分类实体,该分类包含以下条目:

[
    {"type": "minecraft:sheep",   "minCount": 4, "maxCount": 4, "weight": 12},
    {"type": "minecraft:pig",     "minCount": 4, "maxCount": 4, "weight": 10},
    {"type": "minecraft:chicken", "minCount": 4, "maxCount": 4, "weight": 10},
    {"type": "minecraft:cow",     "minCount": 4, "maxCount": 4, "weight": 8 },
    {"type": "minecraft:horse",   "minCount": 2, "maxCount": 6, "weight": 5 },
    {"type": "minecraft:donkey",  "minCount": 1, "maxCount": 3, "weight": 1 }
]

CREATURE 上限为 10,因此会检查以各玩家当前区块为中心、最多 17×17 范围中的其他 CREATURE 实体。如果找到的数量小于等于 10 * chunkCount / 289,基本相当于未加载区块附近更容易满足生成条件,再检查每个实体与最近玩家的距离。如果至少一个距离大于 128,就可以生成。

全部检查通过后,根据权重从上述列表选择一个生成条目。假设选中了猪,游戏会检查区块中的随机位置是否适合。合适时按条目的最小和最大数量生成,此例恰好为 4 头猪。不合适则再随机尝试两个位置;仍未找到就取消生成。


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