聊聊 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
或指定 Entity
。
使用 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 进行开发,的确可以有效增加开发效率。
(全文完)
其实正确做法是往 Spigot 发相关 API 的 PR,但是似乎因为步骤繁琐,很少有人去做。
主要还是没人想发个 Spigot 的 PR 都要提交申请书.jpg