Skip to main content
Version: 1.21.4

生物实体、怪物与玩家(Living Entities, Mobs & Players)

生物实体是 实体 的一个重要子类,它们都继承自共同的 LivingEntity 超类。这个类别包括怪物(通过 Mob 子类)、玩家(通过 Player 子类)以及盔甲架(通过 ArmorStand 子类)。

与普通实体不同,生物实体拥有许多额外的属性。这些属性包括 属性怪物效果、伤害追踪等。

生命值、伤害与治疗(Health, Damage and Healing)

参见:属性

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

默认情况下,最大生命值由 minecraft:generic.max_health 属性 决定,当前生命值在 生成 时会被设置为相同的数值。当调用 Entity#hurt 方法对实体造成伤害时,当前生命值会按照伤害计算结果减少。许多实体(如僵尸)在受到伤害后会保持减少后的生命值,而有些实体(如玩家)则可以恢复失去的生命值。

要获取或设置最大生命值,需要直接读取或写入该属性,如下所示:

// 获取实体的属性映射。
AttributeMap attributes = entity.getAttributes();

// 获取实体的最大生命值。
float maxHealth = attributes.getValue(Attributes.MAX_HEALTH);
// 上述操作的简写方式。
maxHealth = entity.getMaxHealth();

// 设置最大生命值必须通过获取 AttributeInstance 并调用 #setBaseValue,
// 或者添加属性修饰符。这里我们采用前者。更多细节请参考 Attributes 相关文档。
attributes.getInstance(Attributes.MAX_HEALTH).setBaseValue(50);

受到伤害 时,生物实体会进行额外的计算,比如考虑 minecraft:generic.armor 属性(但如果伤害类型minecraft:bypasses_armor 标签 中,则不会考虑),以及 minecraft:generic.absorption 属性。生物实体还可以重写 #onDamageTaken 方法以执行攻击后的行为;该方法只会在最终伤害值大于零时被调用。

伤害事件(Damage Events)

由于伤害处理流程较为复杂,NeoForge 提供了多个事件供你接入,这些事件会按照下述顺序依次触发。通常,这些事件适用于你想要修改非自己(或不一定是自己)的实体所受到的伤害,例如你希望修改 Minecraft 原生实体或其他模组实体受到的伤害,或者你希望统一修改所有实体(无论是否属于你自己)受到的伤害时使用。 所有这些事件共有的一个概念是 DamageContainer。每次攻击开始时都会实例化一个新的 DamageContainer,攻击结束后即被丢弃。它包含了原始的 DamageSource、原始伤害数值,以及所有单独的修改项列表——如护甲、吸收,附魔生物效果 等。DamageContainer 会被传递给下文列出的所有事件,你可以检查已经做过哪些修改,并根据需要进行自定义调整。

EntityInvulnerabilityCheckEvent

该事件允许模组既可以绕过,也可以为实体增加无敌效果。此事件同样会对非生物实体触发。你可以使用此事件让某个实体对攻击免疫,或移除其已有的免疫效果。

出于技术原因,对该事件的钩子应当是确定性的,并且只依赖于伤害类型。这意味着带有随机概率的无敌,或者仅在伤害数值低于某一阈值时才生效的无敌,应当放在 LivingIncomingDamageEvent(见下文)中实现。

LivingIncomingDamageEvent

此事件只会在服务器端被调用,主要用于两种场景:动态取消攻击,以及添加伤害减免回调。

动态取消攻击,实质上就是添加一种非确定性的无敌效果,例如有一定概率抵消伤害、根据时间或已受伤害量决定是否免疫等。如果是始终一致的无敌效果,应通过 EntityInvulnerabilityCheckEvent(见上文)实现。

伤害减免回调允许你修改特定部分的伤害减免。例如,你可以让护甲的伤害减免效果降低 50%。这样,后续的生物效果等也会基于新的伤害数值进行处理。你可以这样添加一个减免回调:

@SubscribeEvent
public static void decreaseArmor(LivingIncomingDamageEvent event) {
// 我们只对玩家应用该减免,僵尸等其他实体不受影响
if (event.getEntity() instanceof Player) {
// 添加我们自定义的减免回调
event.getDamageContainer().addModifier(
// 目标减免类型。可选值见 DamageContainer.Reduction 枚举。
DamageContainer.Reduction.ARMOR,
// 实际的修改操作。参数为伤害容器和基础减免值,
// 返回新的减免值。输入输出均为 float 类型。
(container, baseReduction) -> baseReduction * 0.5f
);
}
}

回调的应用顺序与添加顺序一致。这意味着在事件处理器中优先级(priority)更高的回调会先执行。

LivingShieldBlockEvent

该事件可用于完全自定义盾牌格挡机制。这包括:引入额外的盾牌格挡、阻止盾牌格挡、修改原版的盾牌格挡检测、改变对盾牌或攻击物品造成的伤害、调整盾牌的视野角度、允许投射物穿透但阻挡近战攻击(或相反)、被动格挡攻击(即无需主动使用盾牌)、只格挡部分伤害百分比等。

请注意,此事件并不适用于“类盾牌”物品以外的免疫或攻击取消等用途。

ArmorHurtEvent

该事件应该很容易理解。当计算护甲因攻击受到损伤时会触发此事件,可用于修改每件护甲受到的耐久度损耗(如有的话)。

LivingDamageEvent.Pre

此事件会在伤害即将造成时立即调用。此时 DamageContainer 已经完全填充,最终伤害数值已可获得,并且事件已经无法被取消,因为此时攻击已被判定为成功。

在这一阶段,各类修饰器(modifier)均已生效,你可以精细地调整伤害数值。需要注意的是,诸如护甲损伤等处理已经完成。

LivingDamageEvent.Post

该事件在伤害结算、吸收值减少、战斗追踪器更新以及统计与游戏事件处理之后调用。此事件不可取消,因为攻击已经发生。通常用于实现攻击后的效果。需要注意的是,即使伤害为零,该事件也会被触发,因此如有需要请检查伤害数值。

如果你需要在自定义实体上处理类似逻辑,建议重写 ILivingEntityExtension#onDamageTaken() 方法。与 LivingDamageEvent.Post 不同,只有当伤害大于零时该方法才会被调用。

生物效果(Mob Effects)

参见 生物效果与药水

装备(Equipment)

参见 实体上的容器

继承层级(Hierarchy)

生物实体(living entities)拥有复杂的类继承层级。如前所述,存在三个直接子类(红色为 abstract 类,蓝色为非抽象类):

其中,ArmorStand 没有任何子类(且也是唯一的非抽象类),因此我们将重点关注 MobPlayer 的类继承层级。

Mob 的继承层级(Hierarchy of Mob

Mob 的类继承层级如下所示(红色为 abstract 类,蓝色为非抽象类): 所有未在图中出现的其他生物实体(living entities),都是 AnimalMonster 的子类。

你可能已经注意到,这个继承体系非常混乱。例如,为什么蜜蜂、鹦鹉等也不是飞行动物(flying mobs)?当你深入查看 AnimalMonster 的子类层级时,这个问题会变得更加严重(如果你感兴趣,可以用 IDE 的 Show Hierarchy 功能自行查阅,这里不做详细讨论)。最好的做法是认识到这个问题,但不必过于纠结。

下面我们来介绍几个最重要的类:

  • PathfinderMob:包含寻路(pathfinding)相关的逻辑。
  • AgeableMob:包含实体成长与幼年(baby)相关的逻辑。僵尸等有幼年变种的怪物并没有继承此类,而是直接作为 Monster 的子类。
  • Animal:大多数动物实体都会继承自这个类。它还有一些抽象子类,例如 AbstractHorseTamableAnimal
  • Monster:大多数游戏中被认为是怪物的实体都会继承自这个抽象类。和 Animal 一样,它也有一些抽象子类,如 AbstractPiglinAbstractSkeletonRaiderZombie
  • WaterAnimal:水生动物(如鱼、鱿鱼和海豚)对应的抽象类。由于寻路方式完全不同,这些实体不会和其他动物混在一起。

Player 的继承体系(Hierarchy of Player

根据玩家所处的环境(side),会使用不同的玩家类(player class)。除了 FakePlayer,你几乎永远不需要自己手动构建玩家实体。

  • AbstractClientPlayer:此类作为两个客户端玩家的基类,均用于表示 逻辑客户端 上的玩家。
  • LocalPlayer:此类用于表示当前正在运行游戏的玩家。
  • RemotePlayer:此类用于表示在多人游戏过程中,LocalPlayer 可能遇到的其他玩家。因此,RemotePlayer 在单人游戏环境下不会存在。
  • ServerPlayer:此类用于表示 逻辑服务端 上的玩家。
  • FakePlayer:这是 ServerPlayer 的一个特殊子类,设计用于作为玩家的模拟对象,适用于需要玩家上下文但实际并非玩家的机制。

生成(Spawning)

除了 常规生成方式 —— 即通过 /summon 指令,以及代码中调用 EntityType#spawnLevel#addFreshEntity —— 之外,Mob 还可以通过其他方式生成。ArmorStand 可以通过常规方式生成,而 Player 不应由你自行实例化,FakePlayer 除外。

生成蛋(Spawn Eggs)

为生物 [注册(register)] 一个生成蛋是很常见的做法(虽然不是必须的)。这可以通过 SpawnEggItem 类实现,NeoForge 对其进行了补丁处理,增加了额外的初始化步骤,例如注册颜色处理器,并将生成蛋添加到内部的 SpawnEggItemEntityType 映射表中。

// 假设我们有一个 DeferredRegister.Items,名为 ITEMS
DeferredItem<SpawnEggItem> MY_ENTITY_SPAWN_EGG = ITEMS.registerItem("my_entity_spawn_egg",
properties -> new SpawnEggItem(
// 要生成的实体类型
MY_ENTITY_TYPE.get(),
// 传递给 lambda 的属性,可进行额外设置
properties
));

作为一种普通物品,该物品应被添加到 创造模式标签,并且要为其添加 客户端物品模型翻译

自然生成(Natural Spawning)

另见 实体/MobCategory世界生成/生物群系修改器/添加生成世界生成/生物群系修改器/添加生成消耗;以及 生成循环,可参考 Minecraft Wiki

对于 MobCategory#isFriendly() 返回 true 的实体(默认所有非怪物实体),自然生成会在每个 tick 执行一次。对于 MobCategory#isFriendly() 返回 false 的实体(默认所有怪物),则每 400 tick(即 20 秒)执行一次。如果 MobCategory#isPersistent() 返回 true(主要是动物),那么在区块生成时也会进行此过程。 对于每个区块(chunk)和生物分类(mob category),系统会检查当前是否达到了生成上限(spawn cap)。更技术性地说,这一步会判断在周围的 loadedChunks 区域内,该 MobCategory 的实体数量是否小于 MobCategory#getMaxInstancesPerChunk() * loadedChunks / 289。其中,loadedChunks 最多为以当前区块为中心的 17x17 区块区域,如果因为渲染距离等原因加载的区块更少,则以实际加载数为准。

接下来,对于每个区块,要求在至少有一个玩家附近(“附近”指生物与玩家的距离小于等于 128)时,该 MobCategory 的实体数量小于 MobCategory#getMaxInstancesPerChunk(),才能生成对应的生物。

如果以上条件都满足,会从相关生物群系(biome)的生成数据中随机选择一个条目,并在能找到合适位置的情况下生成生物。系统最多尝试三次随机位置;如果三次都找不到合适的位置,则不会生成生物。

示例(Example)

听起来很复杂?让我们以平原生物群系(plains biome)中的动物为例,详细梳理一遍。

在平原生物群系中,每一 tick,游戏会尝试从 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,系统会扫描每个玩家当前区块为中心的最多 17x17 区块范围内的其他 CREATURE 类型实体。如果找到的实体数量小于等于 10 * chunkCount / 289(这意味着在未加载区块附近,生成概率会更高),系统会对每个找到的实体与最近玩家的距离进行检查。如果至少有一个实体与玩家的距离大于 128,则可以生成新实体。

如果以上所有检查都通过,会根据权重从上面的列表中选择一个生成条目。假设本次选择了猪(pig)。游戏随后会在区块内随机选择一个位置,判断该位置是否适合生成实体。如果合适,就会按照生成数据中的最小和最大数量生成实体(在这个例子中,正好生成 4 只猪)。如果位置不合适,系统会再尝试两次不同的位置。如果三次都找不到合适的位置,则本次生成会被取消。