末影人与“中立”生物

注:由于末影人更多地被认为是敌对的怪物而非友好的中立生物,所以在1.2节而非1.4节中进行分析。


末影人被认为是敌对性的中立生物,这与EnderMan(旧称EndermanEntity,感觉是把Enderman的字母“M”误打成大写了)类实现的2个接口密切相关。其中的一个接口就是前面说过的Enemy,而另一个接口,NeutralMob(旧称IAngerable),则不仅仅是一个标记接口,还定义了大量与“生气”相关的方法。 这节笔者主要讲NeutralMob这个接口,可能涉及少量实体类对此接口里抽象方法的实现。


在开始之前,先要说明一件事情。
NeutralMob接口靠后的一部分抽象方法(即以下方法)在MobLivingEntity中都有实现,而且很容易理解,因此本节不讨论它们。

@Nullable
LivingEntity getLastHurtByMob();

void setLastHurtByMob(@Nullable LivingEntity lastHurtByMob);

void setLastHurtByPlayer(@Nullable Player lastHurtByPlayer);

void setTarget(@Nullable LivingEntity target);

boolean canAttack(LivingEntity entity);

@Nullable
LivingEntity getTarget();

但是要注意,在你的接口中,千万不能这样写,除非你声明的方法不会被重混淆(例如你声明了一个toString)。举个例子,假设你写了如下的代码:

public interface ExampleInterface {
    float getHealth();

    default void foo() {
        ExampleMod.LOGGER.info(String.valueOf(getHealth()));
    }
}

public class ExampleEntity extends PathfinderMob implements ExampleInterface {
    public ExampleEntity(EntityType<? extends ExampleEntity> type, Level level) {
        super(type, level);
    }

    @Override
    public void tick() {
        super.tick();
        foo();
    }
}

否则编译时正常,开发环境中也正常,而到生产环境中,由于LivingEntity类中的getHealth方法会被重混淆为SRG名,你的ExampleInterface里的getHealth却还是getHealth,于是产生了一个没有被实现的抽象方法。只要你的实体一tickAbstractMethodError就会迎面而来~(本人已亲身体验过一次)

为了摆脱讨厌的AbstractMethodError,你需要这样写:

public interface ExampleInterface {
    private LivingEntity self() {
        return (LivingEntity) this;
    }

    default void foo() {
        ExampleMod.LOGGER.info(String.valueOf(self().getHealth()));
    }
}

public class ExampleEntity extends PathfinderMob implements ExampleInterface {
    public ExampleEntity(EntityType<? extends ExampleEntity> type, Level level) {
        super(type, level);
    }

    @Override
    public void tick() {
        super.tick();
        foo();
    }
}

这样就可以有效地赶走AbstractMethodError。你不妨去翻一下IForgeEntityIForgeBlock等Forge的接口的源代码,里面就有很多这样的写法。


回到正题,我们先从前面的2组getter和setter说起。

// 注意单位为tick
int getRemainingPersistentAngerTime();

void setRemainingPersistentAngerTime(int remainingPersistentAngerTime);

@Nullable
UUID getPersistentAngerTarget();

void setPersistentAngerTarget(@Nullable UUID persistentAngerTarget);

EnderMan里的实现:

@Override
public int getRemainingPersistentAngerTime() {
    return remainingPersistentAngerTime;
}

@Override
public void setRemainingPersistentAngerTime(int remainingPersistentAngerTime) {
   this.remainingPersistentAngerTime = remainingPersistentAngerTime;
}

@Nullable
@Override
public UUID getPersistentAngerTarget() {
    return persistentAngerTarget;
}

@Override
public void setPersistentAngerTarget(@Nullable UUID persistentAngerTarget) {
    this.persistentAngerTarget = persistentAngerTarget;
}

Bee里前两个方法的实现。因为蜜蜂生气时与不生气时材质不同,所以这里用了entityData

@Override
public int getRemainingPersistentAngerTime() {
    return entityData.get(DATA_REMAINING_ANGER_TIME);
}

@Override
public void setRemainingPersistentAngerTime(int remainingPersistentAngerTime) {
    entityData.set(DATA_REMAINING_ANGER_TIME, remainingPersistentAngerTime);
}

上面一组getter和setter控制了剩余的生气时间(一旦remainingPersistentAngerTime为0就不会再生气),下面一组则控制了当前生气的目标(的UUID)(UUID并非只有玩家有,任何实体都有)。

下一个方法及实现。

void startPersistentAngerTimer();
private static final UniformInt PERSISTENT_ANGER_TIME = TimeUtil.rangeOfSeconds(20, 39);

@Override
public void startPersistentAngerTimer() {
    setRemainingPersistentAngerTime(PERSISTENT_ANGER_TIME.sample(random));
}

可见这个方法被调用时,remainingPersistentAngerTime会被设置为一个随机值(在末影人中,这个值是[400, 780]间的随机整数(静态工厂方法TimeUtil.rangeOfSeconds在创建UniformInt实例时,给20和39分别乘上了20))。当然如果你的实体实现了这个方法,你也可以做些其他的事,这个方法的调用时机马上会讲到。

下面就是一些默认方法了,这些方法通常不需要被重写。首先是数据保存与加载。

default void addPersistentAngerSaveData(CompoundTag tag) {
    tag.putInt("AngerTime", getRemainingPersistentAngerTime());
    if (getPersistentAngerTarget() != null) {
        tag.putUUID("AngryAt", getPersistentAngerTarget());
    }
}

default void readPersistentAngerSaveData(Level level, CompoundTag tag) {
    setRemainingPersistentAngerTime(tag.getInt("AngerTime"));
    if (level instanceof ServerLevel) {
        if (!tag.hasUUID("AngryAt")) {
            setPersistentAngerTarget(null);
        } else {
            UUID angryAtUUID = tag.getUUID("AngryAt");
            setPersistentAngerTarget(angryAtUUID);
            Entity angryAt = ((ServerLevel) level).getEntity(angryAtUUID);
            if (angryAt != null) {
                if (angryAt instanceof Mob) {
                    setLastHurtByMob((Mob) angryAt);
                }
                if (entity.getType() == EntityType.PLAYER) {
                    setLastHurtByPlayer((Player) angryAt);
                }
            }
        }
    }
}

对于这里的数据与加载,需要注意如下几点:

  • getPersistentAngerTarget可能返回null,在保存时需要一个null-check
  • 在从NBT中读取数据时,因为可能没有保存persistentAngerTarget,因此要用到hasUUID的检查(这个之前提到过)
  • 通过UUID获取实体一定要在服务端进行

接下来是Anger的更新。

default void updatePersistentAnger(ServerLevel level, boolean alwaysAngryIfTargetsPlayer) {
    LivingEntity target = getTarget();
    UUID angryAt = getPersistentAngerTarget();
    if ((target == null || target.isDeadOrDying()) && angryAt != null && level.getEntity(angryAt) instanceof Mob) {
        stopBeingAngry();
    } else {
        if (target != null && !Objects.equals(angryAt, target.getUUID())) {
            setPersistentAngerTarget(target.getUUID());
      //    这里就调用了startPersistentAngerTimer,下面还会有一次调用
            startPersistentAngerTimer();
        }
        if (getRemainingPersistentAngerTime() > 0 && (target == null || target.getType() != EntityType.PLAYER || !alwaysAngryIfTargetsPlayer)) {
            setRemainingPersistentAngerTime(getRemainingPersistentAngerTime() - 1);
            if (getRemainingPersistentAngerTime() == 0) {
                stopBeingAngry();
            }
        }
    }
}

这个updatePersistentAnger方法应该在实体更新时在服务端手动调用。如果给方法的第二个参数传入了false(在原版中,只有蜜蜂用了false),那么只要remainingPersistentAngerTime大于0,这个变量的值就会每游戏刻减少1,也就是说,你的生物会在追击你时慢慢原谅你。

接下来的几个方法用于获取该NeutralMob是否生气。

default boolean isAngryAt(LivingEntity entity) {
    if (!canAttack(entity)) {
        return false;
    } else {
        return entity.getType() == EntityType.PLAYER && isAngryAtAllPlayers(entity.level()) ? true : entity.getUUID().equals(getPersistentAngerTarget());
    }
}

default boolean isAngryAtAllPlayers(Level level) {
    return level.getGameRules().getBoolean(GameRules.RULE_UNIVERSAL_ANGER) && isAngry() && getPersistentAngerTarget() == null;
}

default boolean isAngry() {
    return getRemainingPersistentAngerTime() > 0;
}

在这些方法中,isAngryAt方法常作为方法引用使用在NearestAttackableTargetGoal中的最后一个参数中,来控制NeutralMob只攻击惹怒自己的玩家。isAngryAtAllPlayers只在isAngryAt方法中被调用。而isAngry方法使用得就比较广泛,常用于判断NeutralMob是否在生气中,并用于下一步的行为或材质控制。

最后是与移除生气状态相关的方法。

default void playerDied(Player player) {
    if (player.level().getGameRules().getBoolean(GameRules.RULE_FORGIVE_DEAD_PLAYERS)) {
        if (player.getUUID().equals(getPersistentAngerTarget())) {
            stopBeingAngry();
        }
    }
}

default void forgetCurrentTargetAndRefreshUniversalAnger() {
    stopBeingAngry();
    startPersistentAngerTimer();
}

default void stopBeingAngry() {
    setLastHurtByMob(null);
    setPersistentAngerTarget(null);
    setTarget(null);
    setRemainingPersistentAngerTime(0);
}

当玩家死亡后,playerDied方法将会在服务端被自动调用,也就是说,原版中player参数总是传入一个ServerPlayer的实例。forgetCurrentTargetAndRefreshUniversalAnger方法,则在NeutralMobResetUniversalAngerTargetGoal里的start方法中被调用,用来重置Anger。stopBeingAngry方法用于直接移除NeutralMob的生气状态,由于NeutralMob类中已经写好了部分方法,所以一般不需要直接调用它(唯一的例外是在蜜蜂成功伤害实体后,这个方法会被直接调用)。

这节的内容就是这么多啦,下一节将正式开始分析末影人~

results matching ""

    No results matching ""

    results matching ""

      No results matching ""