聊聊 PaperAPI 提供的自定义生物 AI 系统

发布于 2021-12-19  78 次阅读


聊聊 PaperAPI 提供的自定义生物 AI 系统

灵感:https://www.mcbbs.net/thread-1285618-1-1.html(原文发布于 https://izzel.io/2021/12/19/living-things

本文旨在介绍由 PaperAPI 封装的自定义生物行为(AI)系统(com.destroystokyo.paper.entity.ai),籍由此系统,我们可以在不接触 NMS 的情况下为单个生物自定义其 AI。本文代码基于 Paper-API 1.16.5。

阅读本文可能需要了解原版的生物 AI 机制,如果您不了解这些机制,则可以阅读海螺的 聊聊生物和 AI 文章(即本文灵感)来对这些机制有一些初步的了解

摒弃 NMS

众所周知,与 Forge 不同,Bukkit API 总是希望包揽一切,提供一套稳定的,高度封装的 API 给服务端插件开发者,而不希望开发者基于内部代码进行开发。但因为各种原因,原生 Bukkit API(甚至 Spigot API)提供的封装总是有限,对于一些进阶的操作,我们总是需要访问和调用内部代码来实现我们所需要的操作。自定义生物 AI 就是其中的一个:以往,开发者们往往需要自行继承原来的生物实体类,然后重载 Goal 初始化方法,甚至利用反射来添加,或是擦除生物 AI——但有了 Paper API 后,这一切都会变得简单,且可控。

了解 PaprAPI 封装的自定义生物 AI 系统

大致来看,PaperAPI 封装的自定义生物 AI 系统主要由 Goal<T extends Mob>MobGoals 两部分组成

先来看 Goal 类的构造:

package com.destroystokyo.paper.entity.ai;

import org.jetbrains.annotations.NotNull;

import java.util.EnumSet;

import org.bukkit.entity.Mob;

/**
 * Represents an AI goal of an entity
 */
public interface Goal<T extends Mob> {

    /**
     * Checks if this goal should be activated
     *
     * @return if this goal should be activated
     */
    boolean shouldActivate();

    /**
     * Checks if this goal should stay active, defaults to {@link Goal#shouldActivate()}
     *
     * @return if this goal should stay active
     */
    default boolean shouldStayActive() {
        return shouldActivate();
    }

    /**
     * Called when this goal gets activated
     */
    default void start() {
    }

    /**
     * Called when this goal gets stopped
     */
    default void stop() {
    }

    /**
     * Called each tick the goal is activated
     */
    default void tick() {
    }

    /**
     * A unique key that identifies this type of goal. Plugins should use their own namespace, not the minecraft
     * namespace. Additionally, this key also specifies to what mobs this goal can be applied to
     *
     * @return the goal key
     */
    @NotNull
    GoalKey<T> getKey();

    /**
     * Returns a list of all applicable flags for this goal.<br>
     *
     * This method is only called on construction.
     *
     * @return the subtypes.
     */
    @NotNull
    EnumSet<GoalType> getTypes();
}

如果接触过 Minecraft 原版 Goal 的开发者,相信已经八九不离十的知道这是什么东西了 —— 其作用,甚至结构都和 Goal 差不多,即用于描述生物的一种行为。在这其中,GoalType

package com.destroystokyo.paper.entity.ai;

/**
 * Represents the subtype of a goal. Used by minecraft to disable certain types of goals if needed.
 */
public enum GoalType {

    MOVE,
    LOOK,
    JUMP,
    TARGET,
    /**
     * Used to map vanilla goals, that are a behavior goal but don't have a type set...
     */
    UNKNOWN_BEHAVIOR,

}

和原版的 Goal.Flag 也大差不差,除了多了一个 UNKNOWN_BEHAVIOR 枚举用于映射 Vanilla 的 Goal。

但细心的人也许会发现,Paper API 的 Goal 和原版的 Goal 还是有一些不同:Paper API 的 Goal 是一个泛型接口,同时额外要求实现一个 GoalKey<T> getKey() 方法。

当我们查看 GoalKey<T extends Mob> 的主要部分,我们立即就能明白其作用:

package com.destroystokyo.paper.entity.ai;

import com.google.common.base.Objects;

import org.jetbrains.annotations.NotNull;

import java.util.StringJoiner;

import org.bukkit.NamespacedKey;
import org.bukkit.entity.Mob;

/**
 *
 * Used to identify a Goal. Consists of a {@link NamespacedKey} and the type of mob the goal can be applied to
 *
 * @param <T> the type of mob the goal can be applied to
 */
public class GoalKey<T extends Mob> {

    private final Class<T> entityClass;
    private final NamespacedKey namespacedKey;

    private GoalKey(@NotNull Class<T> entityClass, @NotNull NamespacedKey namespacedKey) {
        this.entityClass = entityClass;
        this.namespacedKey = namespacedKey;
    }

    // Omit getter, equals, hashcode and toString methods...

    @NotNull
    public static <A extends Mob> GoalKey<A> of(@NotNull Class<A> entityClass, @NotNull NamespacedKey namespacedKey) {
        return new GoalKey<>(entityClass, namespacedKey);
    }
}

它存在的作用就是为了作唯一标识符标识单个 Goal,同时配合 Goal 使用泛型约束这个 Goal 可以被应用到的生物类型。

那么如此以来,我们便摸透了 Goal 的内容,可以开始编写我们自己的自定义 AI了,但是...如何将这些 Goal 应用到我们的生物上呢?这时就需要介绍 ModGoals 了:

package com.destroystokyo.paper.entity.ai;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;

import org.bukkit.entity.Mob;

/**
 * Represents a part of the "brain" of a mob. It tracks all tasks (running or not), allows adding and removing goals
 */
public interface MobGoals {

    <T extends Mob> void addGoal(@NotNull T mob, int priority, @NotNull Goal<T> goal);

    <T extends Mob> void removeGoal(@NotNull T mob, @NotNull Goal<T> goal);

    <T extends Mob> void removeAllGoals(@NotNull T mob);

    <T extends Mob> void removeAllGoals(@NotNull T mob, @NotNull GoalType type);

    <T extends Mob> void removeGoal(@NotNull T mob, @NotNull GoalKey<T> key);

    <T extends Mob> boolean hasGoal(@NotNull T mob, @NotNull GoalKey<T> key);

    @Nullable
    <T extends Mob> Goal<T> getGoal(@NotNull T mob, @NotNull GoalKey<T> key);

    @NotNull
    <T extends Mob> Collection<Goal<T>> getGoals(@NotNull T mob, @NotNull GoalKey<T> key);

    @NotNull
    <T extends Mob> Collection<Goal<T>> getAllGoals(@NotNull T mob);

    @NotNull
    <T extends Mob> Collection<Goal<T>> getAllGoals(@NotNull T mob, @NotNull GoalType type);

    @NotNull
    <T extends Mob> Collection<Goal<T>> getAllGoalsWithout(@NotNull T mob, @NotNull GoalType type);

    @NotNull
    <T extends Mob> Collection<Goal<T>> getRunningGoals(@NotNull T mob);

    @NotNull
    <T extends Mob> Collection<Goal<T>> getRunningGoals(@NotNull T mob, @NotNull GoalType type);

    @NotNull
    <T extends Mob> Collection<Goal<T>> getRunningGoalsWithout(@NotNull T mob, @NotNull GoalType type);
}

看完代码我们就会明白,这个所谓的 MobGoals 其实就是一个 Manager,用来方便的为生物获取、添加和删除 Goal,至于这些方法的作用,相信我不用说大家也都知道了。

最后,要想获取 MobGoals 实例,只需调用 Bukkit.getMobGoals() 方法(同 Bukkit.getServer().getMobGoals() 方法)即可。

当然,额外的,我们还可以配合 Pathfinder 和 PaperAPI 提供的其他 API 封装辅助开发自定义生物 AI,在这里对这些手段进行一些简单的介绍:

Pathfinder

com.destroystokyo.paper.entity.Pathfinder,可以通过 Mob#getPathfinder() 获取到 Pathfinder 实例。和他的名字一样,Pathfinder 就是一个生物的寻路器,PaperAPI 封装的 Pathfinder 为我们提供了像是 寻路、寻路并按此路径移动、设置生物是否可以开门、设置生物是否可以漂浮在水上 之类的便捷方法,令开发者便捷的使生物寻路和自定义移动行为

PaperAPI 提供的其他 API 封装辅助开发自定义生物 AI

除此之外,PaperAPI 还为我们提供了其他的一些便于辅助开发自定义生物 AI 的方法,例如 Mob#lookAt(@NotNull org.bukkit.Location location)Mob#lookAt(@NotNull Entity entity) 就允许我们命令一个生物望向指定 Location 或指定 Enrtity

使用 Minecraft 原生生物 AI —— VanillaGoal

但是,如果我想偷懒,希望使用 Minecraft 原生的生物 AI,而不是从零开始自己实现一个全新的 AI,该怎么做呢?

VanillaGoal<T extends Mob> 类中,我们可以看到其中已经预先声明了很多原版 Goal 对应的 GoalKey

package com.destroystokyo.paper.entity.ai;

import com.destroystokyo.paper.entity.RangedEntity;

import org.bukkit.NamespacedKey;
import org.bukkit.entity.*;

/**
 * Represents a vanilla goal. Plugins should never implement this.<br>
 * Generated by VanillaPathfinderTest in paper-server
 */
public interface VanillaGoal<T extends Mob> extends Goal<T> {

    GoalKey<Bee> BEE_ATTACK = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_attack"));
    GoalKey<Bee> BEE_BECOME_ANGRY = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_become_angry"));
    GoalKey<Bee> BEE_ENTER_HIVE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_enter_hive"));
    GoalKey<Bee> BEE_GO_TO_HIVE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_go_to_hive"));
    GoalKey<Bee> BEE_GO_TO_KNOWN_FLOWER = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_go_to_known_flower"));
    GoalKey<Bee> BEE_GROW_CROP = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_grow_crop"));
    GoalKey<Bee> BEE_HURT_BY_OTHER = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_hurt_by_other"));
    GoalKey<Bee> BEE_LOCATE_HIVE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_locate_hive"));
    GoalKey<Bee> BEE_POLLINATE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_pollinate"));
    GoalKey<Bee> BEE_WANDER = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_wander"));
    GoalKey<Blaze> BLAZE_FIREBALL = GoalKey.of(Blaze.class, NamespacedKey.minecraft("blaze_fireball"));
    GoalKey<Cat> TEMPT_CHANCE = GoalKey.of(Cat.class, NamespacedKey.minecraft("tempt_chance"));
    GoalKey<Cat> CAT_AVOID_ENTITY = GoalKey.of(Cat.class, NamespacedKey.minecraft("cat_avoid_entity"));
    GoalKey<Cat> CAT_RELAX_ON_OWNER = GoalKey.of(Cat.class, NamespacedKey.minecraft("cat_relax_on_owner"));
    GoalKey<Dolphin> DOLPHIN_SWIM_TO_TREASURE = GoalKey.of(Dolphin.class, NamespacedKey.minecraft("dolphin_swim_to_treasure"));
    GoalKey<Dolphin> DOLPHIN_SWIM_WITH_PLAYER = GoalKey.of(Dolphin.class, NamespacedKey.minecraft("dolphin_swim_with_player"));
    GoalKey<Dolphin> DOLPHIN_PLAY_WITH_ITEMS = GoalKey.of(Dolphin.class, NamespacedKey.minecraft("dolphin_play_with_items"));
    GoalKey<Drowned> DROWNED_ATTACK = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_attack"));
    GoalKey<Drowned> DROWNED_GOTO_BEACH = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_goto_beach"));
    GoalKey<Creature> DROWNED_GOTO_WATER = GoalKey.of(Creature.class, NamespacedKey.minecraft("drowned_goto_water"));
    GoalKey<Drowned> DROWNED_SWIM_UP = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_swim_up"));
    GoalKey<RangedEntity> DROWNED_TRIDENT_ATTACK = GoalKey.of(RangedEntity.class, NamespacedKey.minecraft("drowned_trident_attack"));
    GoalKey<Enderman> ENDERMAN_PICKUP_BLOCK = GoalKey.of(Enderman.class, NamespacedKey.minecraft("enderman_pickup_block"));
    GoalKey<Enderman> ENDERMAN_PLACE_BLOCK = GoalKey.of(Enderman.class, NamespacedKey.minecraft("enderman_place_block"));
    GoalKey<Enderman> PLAYER_WHO_LOOKED_AT_TARGET = GoalKey.of(Enderman.class, NamespacedKey.minecraft("player_who_looked_at_target"));
    GoalKey<Enderman> ENDERMAN_FREEZE_WHEN_LOOKED_AT = GoalKey.of(Enderman.class, NamespacedKey.minecraft("enderman_freeze_when_looked_at"));
    GoalKey<Evoker> EVOKER_ATTACK_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_attack_spell"));
    GoalKey<Evoker> EVOKER_CAST_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_cast_spell"));
    GoalKey<Evoker> EVOKER_SUMMON_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_summon_spell"));
    GoalKey<Evoker> EVOKER_WOLOLO_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_wololo_spell"));
    GoalKey<Fish> FISH_SWIM = GoalKey.of(Fish.class, NamespacedKey.minecraft("fish_swim"));
    GoalKey<Fox> FOX_DEFEND_TRUSTED = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_defend_trusted"));
    GoalKey<Fox> FOX_FACEPLANT = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_faceplant"));
    GoalKey<Fox> FOX_BREED = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_breed"));
    GoalKey<Fox> FOX_EAT_BERRIES = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_eat_berries"));
    GoalKey<Fox> FOX_FLOAT = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_float"));
    GoalKey<Fox> FOX_FOLLOW_PARENT = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_follow_parent"));
    GoalKey<Fox> FOX_LOOK_AT_PLAYER = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_look_at_player"));
    GoalKey<Fox> FOX_MELEE_ATTACK = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_melee_attack"));
    GoalKey<Fox> FOX_PANIC = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_panic"));
    GoalKey<Fox> FOX_PERCH_AND_SEARCH = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_perch_and_search"));
    GoalKey<Fox> FOX_POUNCE = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_pounce"));
    GoalKey<Fox> FOX_SEARCH_FOR_ITEMS = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_search_for_items"));
    GoalKey<Fox> FOX_SLEEP = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_sleep"));
    GoalKey<Fox> FOX_STROLL_THROUGH_VILLAGE = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_stroll_through_village"));
    GoalKey<Fox> FOX_SEEK_SHELTER = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_seek_shelter"));
    GoalKey<Fox> FOX_STALK_PREY = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_stalk_prey"));
    GoalKey<Ghast> GHAST_ATTACK_TARGET = GoalKey.of(Ghast.class, NamespacedKey.minecraft("ghast_attack_target"));
    GoalKey<Ghast> GHAST_IDLE_MOVE = GoalKey.of(Ghast.class, NamespacedKey.minecraft("ghast_idle_move"));
    GoalKey<Ghast> GHAST_MOVE_TOWARDS_TARGET = GoalKey.of(Ghast.class, NamespacedKey.minecraft("ghast_move_towards_target"));
    GoalKey<Guardian> GUARDIAN_ATTACK = GoalKey.of(Guardian.class, NamespacedKey.minecraft("guardian_attack"));
    GoalKey<Illager> RAIDER_OPEN_DOOR = GoalKey.of(Illager.class, NamespacedKey.minecraft("raider_open_door"));
    GoalKey<Illusioner> ILLUSIONER_BLINDNESS_SPELL = GoalKey.of(Illusioner.class, NamespacedKey.minecraft("illusioner_blindness_spell"));
    GoalKey<Illusioner> ILLUSIONER_MIRROR_SPELL = GoalKey.of(Illusioner.class, NamespacedKey.minecraft("illusioner_mirror_spell"));
    GoalKey<Spellcaster> SPELLCASTER_CAST_SPELL = GoalKey.of(Spellcaster.class, NamespacedKey.minecraft("spellcaster_cast_spell"));
    // ......

在这里,我们可以很容易的获得到所有 Minecraft 原版 Goal 对应的 GoalKey,然后通过 MobGoals来方便的从一个生物中删除其中一个 Goal,亦或者从一个生物身上获取一个通用的 Goal,再添加到另一个生物身上。

对于 VanillaGoal 的具体实现,不幸的是,因为各种各样的原因,PaperAPI 本身不对外开放 VanillaGoal 的实现,但是通过导入 Paper 服务端,我们可以窥见 VanillaGoal 的真面目:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.destroystokyo.paper.entity.ai;

import java.util.EnumSet;
import net.minecraft.server.v1_16_R3.PathfinderGoal;
import org.bukkit.entity.Mob;

public class PaperVanillaGoal<T extends Mob> implements VanillaGoal<T> {
    private final PathfinderGoal handle;
    private final GoalKey<T> key;
    private final EnumSet<GoalType> types;

    public PaperVanillaGoal(PathfinderGoal handle) {
        this.handle = handle;
        this.key = MobGoalHelper.getKey(handle.getClass());
        this.types = MobGoalHelper.vanillaToPaper(handle.getGoalTypes());
    }

    public PathfinderGoal getHandle() {
        return this.handle;
    }

    public boolean shouldActivate() {
        return this.handle.shouldActivate2();
    }

    public boolean shouldStayActive() {
        return this.handle.shouldStayActive2();
    }

    public void start() {
        this.handle.start();
    }

    public void stop() {
        this.handle.onTaskReset();
    }

    public void tick() {
        this.handle.tick();
    }

    public GoalKey<T> getKey() {
        return this.key;
    }

    public EnumSet<GoalType> getTypes() {
        return this.types;
    }
}

所以实际上,这个所谓的 VanillaGoal 就是一个 Wrapper,用来封装 NMS 的 PathfinderGoal。在使用了 NMS 的环境时,我们也可以直接通过构造一个 PathfinderGoal,然后使用 PaperVanillaGoal 封装,再使用 MobGoal 添加行为到生物身上,以此省去复杂的反射流程。

(正文完)

最后

很多人因为兼容,或者各种原因,不愿意接触 PaperAPI,但是不可否认的是,PaperAPI 确实基于 SpigotAPI 做了太多的拓展和优化,对于一些不那么在意兼容性(比如自用)的情况下,使用 PaperAPI 进行开发,的确可以有效增加开发效率。

(全文完)