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

本文也发表于我的博客: https://my.minecraft.kim/tech/2021/12/19/post-393/

灵感: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 类的构造:

  1. package com.destroystokyo.paper.entity.ai;

  2. import org.jetbrains.annotations.NotNull;

  3. import java.util.EnumSet;

  4. import org.bukkit.entity.Mob;

  5. /**
  6. * Represents an AI goal of an entity
  7. */
  8. public interface Goal<T extends Mob> {

  9.     /**
  10.      * Checks if this goal should be activated
  11.      *
  12.      * [url=home.php?mod=space&uid=491268]@Return[/url] if this goal should be activated
  13.      */
  14.     boolean shouldActivate();

  15.     /**
  16.      * Checks if this goal should stay active, defaults to {[url=home.php?mod=space&uid=41191]@link[/url] Goal#shouldActivate()}
  17.      *
  18.      * @return if this goal should stay active
  19.      */
  20.     default boolean shouldStayActive() {
  21.         return shouldActivate();
  22.     }

  23.     /**
  24.      * Called when this goal gets activated
  25.      */
  26.     default void start() {
  27.     }

  28.     /**
  29.      * Called when this goal gets stopped
  30.      */
  31.     default void stop() {
  32.     }

  33.     /**
  34.      * Called each tick the goal is activated
  35.      */
  36.     default void tick() {
  37.     }

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

  46.     /**
  47.      * Returns a list of all applicable flags for this goal.<br>
  48.      *
  49.      * This method is only called on construction.
  50.      *
  51.      * @return the subtypes.
  52.      */
  53.     @NotNull
  54.     EnumSet<GoalType> getTypes();
  55. }
复制代码

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

  1. package com.destroystokyo.paper.entity.ai;

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

  6.     MOVE,
  7.     LOOK,
  8.     JUMP,
  9.     TARGET,
  10.     /**
  11.      * Used to map vanilla goals, that are a behavior goal but don't have a type set...
  12.      */
  13.     UNKNOWN_BEHAVIOR,

  14. }
复制代码

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

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

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

  1. package com.destroystokyo.paper.entity.ai;

  2. import com.google.common.base.Objects;

  3. import org.jetbrains.annotations.NotNull;

  4. import java.util.StringJoiner;

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

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

  14.     private final Class<T> entityClass;
  15.     private final NamespacedKey namespacedKey;

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

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

  21.     @NotNull
  22.     public static <A extends Mob> GoalKey<A> of(@NotNull Class<A> entityClass, @NotNull NamespacedKey namespacedKey) {
  23.         return new GoalKey<>(entityClass, namespacedKey);
  24.     }
  25. }
复制代码

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

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

  1. package com.destroystokyo.paper.entity.ai;

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

  4. import java.util.Collection;

  5. import org.bukkit.entity.Mob;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  30.     @NotNull
  31.     <T extends Mob> Collection<Goal<T>> getRunningGoalsWithout(@NotNull T mob, @NotNull GoalType type);
  32. }
复制代码

看完代码我们就会明白,这个所谓的 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

  1. package com.destroystokyo.paper.entity.ai;

  2. import com.destroystokyo.paper.entity.RangedEntity;

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

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

  10.     GoalKey<Bee> BEE_ATTACK = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_attack"));
  11.     GoalKey<Bee> BEE_BECOME_ANGRY = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_become_angry"));
  12.     GoalKey<Bee> BEE_ENTER_HIVE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_enter_hive"));
  13.     GoalKey<Bee> BEE_GO_TO_HIVE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_go_to_hive"));
  14.     GoalKey<Bee> BEE_GO_TO_KNOWN_FLOWER = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_go_to_known_flower"));
  15.     GoalKey<Bee> BEE_GROW_CROP = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_grow_crop"));
  16.     GoalKey<Bee> BEE_HURT_BY_OTHER = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_hurt_by_other"));
  17.     GoalKey<Bee> BEE_LOCATE_HIVE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_locate_hive"));
  18.     GoalKey<Bee> BEE_POLLINATE = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_pollinate"));
  19.     GoalKey<Bee> BEE_WANDER = GoalKey.of(Bee.class, NamespacedKey.minecraft("bee_wander"));
  20.     GoalKey<Blaze> BLAZE_FIREBALL = GoalKey.of(Blaze.class, NamespacedKey.minecraft("blaze_fireball"));
  21.     GoalKey<Cat> TEMPT_CHANCE = GoalKey.of(Cat.class, NamespacedKey.minecraft("tempt_chance"));
  22.     GoalKey<Cat> CAT_AVOID_ENTITY = GoalKey.of(Cat.class, NamespacedKey.minecraft("cat_avoid_entity"));
  23.     GoalKey<Cat> CAT_RELAX_ON_OWNER = GoalKey.of(Cat.class, NamespacedKey.minecraft("cat_relax_on_owner"));
  24.     GoalKey<Dolphin> DOLPHIN_SWIM_TO_TREASURE = GoalKey.of(Dolphin.class, NamespacedKey.minecraft("dolphin_swim_to_treasure"));
  25.     GoalKey<Dolphin> DOLPHIN_SWIM_WITH_PLAYER = GoalKey.of(Dolphin.class, NamespacedKey.minecraft("dolphin_swim_with_player"));
  26.     GoalKey<Dolphin> DOLPHIN_PLAY_WITH_ITEMS = GoalKey.of(Dolphin.class, NamespacedKey.minecraft("dolphin_play_with_items"));
  27.     GoalKey<Drowned> DROWNED_ATTACK = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_attack"));
  28.     GoalKey<Drowned> DROWNED_GOTO_BEACH = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_goto_beach"));
  29.     GoalKey<Creature> DROWNED_GOTO_WATER = GoalKey.of(Creature.class, NamespacedKey.minecraft("drowned_goto_water"));
  30.     GoalKey<Drowned> DROWNED_SWIM_UP = GoalKey.of(Drowned.class, NamespacedKey.minecraft("drowned_swim_up"));
  31.     GoalKey<RangedEntity> DROWNED_TRIDENT_ATTACK = GoalKey.of(RangedEntity.class, NamespacedKey.minecraft("drowned_trident_attack"));
  32.     GoalKey<Enderman> ENDERMAN_PICKUP_BLOCK = GoalKey.of(Enderman.class, NamespacedKey.minecraft("enderman_pickup_block"));
  33.     GoalKey<Enderman> ENDERMAN_PLACE_BLOCK = GoalKey.of(Enderman.class, NamespacedKey.minecraft("enderman_place_block"));
  34.     GoalKey<Enderman> PLAYER_WHO_LOOKED_AT_TARGET = GoalKey.of(Enderman.class, NamespacedKey.minecraft("player_who_looked_at_target"));
  35.     GoalKey<Enderman> ENDERMAN_FREEZE_WHEN_LOOKED_AT = GoalKey.of(Enderman.class, NamespacedKey.minecraft("enderman_freeze_when_looked_at"));
  36.     GoalKey<Evoker> EVOKER_ATTACK_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_attack_spell"));
  37.     GoalKey<Evoker> EVOKER_CAST_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_cast_spell"));
  38.     GoalKey<Evoker> EVOKER_SUMMON_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_summon_spell"));
  39.     GoalKey<Evoker> EVOKER_WOLOLO_SPELL = GoalKey.of(Evoker.class, NamespacedKey.minecraft("evoker_wololo_spell"));
  40.     GoalKey<Fish> FISH_SWIM = GoalKey.of(Fish.class, NamespacedKey.minecraft("fish_swim"));
  41.     GoalKey<Fox> FOX_DEFEND_TRUSTED = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_defend_trusted"));
  42.     GoalKey<Fox> FOX_FACEPLANT = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_faceplant"));
  43.     GoalKey<Fox> FOX_BREED = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_breed"));
  44.     GoalKey<Fox> FOX_EAT_BERRIES = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_eat_berries"));
  45.     GoalKey<Fox> FOX_FLOAT = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_float"));
  46.     GoalKey<Fox> FOX_FOLLOW_PARENT = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_follow_parent"));
  47.     GoalKey<Fox> FOX_LOOK_AT_PLAYER = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_look_at_player"));
  48.     GoalKey<Fox> FOX_MELEE_ATTACK = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_melee_attack"));
  49.     GoalKey<Fox> FOX_PANIC = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_panic"));
  50.     GoalKey<Fox> FOX_PERCH_AND_SEARCH = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_perch_and_search"));
  51.     GoalKey<Fox> FOX_POUNCE = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_pounce"));
  52.     GoalKey<Fox> FOX_SEARCH_FOR_ITEMS = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_search_for_items"));
  53.     GoalKey<Fox> FOX_SLEEP = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_sleep"));
  54.     GoalKey<Fox> FOX_STROLL_THROUGH_VILLAGE = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_stroll_through_village"));
  55.     GoalKey<Fox> FOX_SEEK_SHELTER = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_seek_shelter"));
  56.     GoalKey<Fox> FOX_STALK_PREY = GoalKey.of(Fox.class, NamespacedKey.minecraft("fox_stalk_prey"));
  57.     GoalKey<Ghast> GHAST_ATTACK_TARGET = GoalKey.of(Ghast.class, NamespacedKey.minecraft("ghast_attack_target"));
  58.     GoalKey<Ghast> GHAST_IDLE_MOVE = GoalKey.of(Ghast.class, NamespacedKey.minecraft("ghast_idle_move"));
  59.     GoalKey<Ghast> GHAST_MOVE_TOWARDS_TARGET = GoalKey.of(Ghast.class, NamespacedKey.minecraft("ghast_move_towards_target"));
  60.     GoalKey<Guardian> GUARDIAN_ATTACK = GoalKey.of(Guardian.class, NamespacedKey.minecraft("guardian_attack"));
  61.     GoalKey<Illager> RAIDER_OPEN_DOOR = GoalKey.of(Illager.class, NamespacedKey.minecraft("raider_open_door"));
  62.     GoalKey<Illusioner> ILLUSIONER_BLINDNESS_SPELL = GoalKey.of(Illusioner.class, NamespacedKey.minecraft("illusioner_blindness_spell"));
  63.     GoalKey<Illusioner> ILLUSIONER_MIRROR_SPELL = GoalKey.of(Illusioner.class, NamespacedKey.minecraft("illusioner_mirror_spell"));
  64.     GoalKey<Spellcaster> SPELLCASTER_CAST_SPELL = GoalKey.of(Spellcaster.class, NamespacedKey.minecraft("spellcaster_cast_spell"));
  65.     // ......
复制代码

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

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

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

  5. package com.destroystokyo.paper.entity.ai;

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

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

  13.     public PaperVanillaGoal(PathfinderGoal handle) {
  14.         this.handle = handle;
  15.         this.key = MobGoalHelper.getKey(handle.getClass());
  16.         this.types = MobGoalHelper.vanillaToPaper(handle.getGoalTypes());
  17.     }

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

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

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

  27.     public void start() {
  28.         this.handle.start();
  29.     }

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

  33.     public void tick() {
  34.         this.handle.tick();
  35.     }

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

  39.     public EnumSet<GoalType> getTypes() {
  40.         return this.types;
  41.     }
  42. }
复制代码

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

(正文完)

最后

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

(全文完)

来自群组: Complex Studio

w44225769
这个API就有些尴尬。。。想要理解AI运行逻辑,搞这方面开发,基本都要把MC源代码的AI实现了解个七七八八。


ZX夏夜之风
好。
看得我都想学Paper了(过几天就学。。。(

神明点灯y
emmmm羡慕我也想学

GuaGua1020
每日回复帮您顶贴呱呱呱呱呱呱呱呱

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

第一页 上一页 下一页 最后一页