概况
Brain 系统是实体的高级 AI 系统, 目前 (1.16.5) 仅有猪灵, 村民, 犹猪兽使用此套系统. 这套系统分为三大部分: Memory, Sensor, 和状态机. 和之前的 Goal 系统相比 (较早加入的实体比如猪, 僵尸等), Brain 系统可以给予生物更复杂的行为, 从开发角度上讲, 也实现了 AI 中三个模块的解耦, 减少了开发的复杂度.
Sensor 负责从环境中获取数据, 并存储到 Memory 中.
状态机
状态机由 Activity 和其下属的一系列具有优先级先后的 Task 组成. 在选定 Activity 之后, Activity 会自动选择下属的最高优先级的可执行的 Task 执行.
在 Task 执行过程中可以切换 Activity. 切换时一般需要清除上一个 Activity 相关的 Memory.
状态机中有一类特殊的 Activity, 叫做 Core Activity (核心活动). 核心活动在任意时刻都处于激活状态. 一般用于类似于习惯动作一类的 AI, 例如: 走向目标点, 游泳, 村民关门, 看向玩家, 村民躲避劫掠者, 猪灵捡东西, 生气等. 从功能上来说, 可能是为了彻底替代旧版的 Goal 系统的附加设计.
工作流程简析
时隔多年我更新啦!这次是基于 1.20.1 的 Minecraft 代码进行分析。
创建 Brain
现在 Brain
是内嵌到 LivingEntity
里面的,也就是所有的生物都有 “大脑”。实体在生成时会调用自己的 LivingEntity::makeBrain(Dynamic<?>)
函数来给自己初始化一颗大脑。原版新出的生物基本都是用的 Brain
系统。
大脑有 3 个组件:Sensor<? super E>
、MemoryModuleType<?>
、Activity
,这里 <E extends LivingEntity>
。
Sensor
和 Memory
是 Brain
实例化后就固定的,Activity
是在 Brain
实例化后添加的。一般使用 Brain.provider(Collection<MemoryModuleType<?>>, Collection<SensorType<? extends Sensor<? super E>>>)
创建一个 Brain.Provider<E>
的实例,再由这个实例为每个实体生成一个 Brain<E>
。
添加 Activity 和 Behavior 时,需要为每个 Behavior 赋予一个优先级(int
)。优先级数字越小,越先被尝试执行。在调用 addActivity
及其衍生接口添加时的参数如下:(伪代码)
activity: Activity,
tasks: Map {
优先级1:Behavior1,
优先级2:Behavior2,
……
},
condition:Set [
Pair (MemoryModuleType1: MemoryStatus1),
Pair (MemoryModuleType2: MemoryStatus2)
……
] default = [],
toEraseOnStop: Set [
MemoryModule1, MemoryModule2, ……
] default = []
condition 是启动这个 activity 的条件,只有当 Brain 中所有记忆都符合 condition 的条件后才会激活这个 activity。
toEraseOnStop 是这个 activity 终止时,需要擦除的记忆
在调用后,tasks 的索引会被转换成红黑树 TreeMap<Integer, Map<Activity, BehaviorController>>
中,树的变量名是 availableBehaviorsByPriority
。第一个 key (type: Integer) 是优先级。
组件的使用、自定义和注册
MemoryModuleType<?>
古希腊今MC 掌管记忆的神
MemoryModuleType<T>
中的 T
代表记忆的数据类型,不过一般没有为其创建子类的必要。自定义时直接 new 一个就行。
构造函数 MemoryModuleType<T>(Optional<Codec<T>>)
唯一的参数是填写其数据类型对应的序列化器。记忆项目的名字是其注册表中的 ID。
如果想自定义一个 MemoryModuleType
,那么需要将其在注册表中注册。具体对应的 ResourceKey
是 net.minecraft.core.registries.Registries.MEMORY_MODULE_TYPE
。
Sensor<E>
和 SensorType<Sensor<E>>
Sensor<E>
是探测器类,用于实体从外界获取信息并记录到记忆中。自定义一般需要继承基类并实现相应方法。E
是可以使用这个探测器的实体类型。例如class MySensor extends Sensor<TamableAnimal>
。private final int scanRate
扫描间隔 tick 数,默认为 20(=1秒),可以用构造函数设置。protected abstract void doTick(ServerLevel level, E entity)
执行 Sensor 逻辑的方法,每scanRate
刻由tick
函数调用一次level
是执行时生物所在的维度entity
是执行这个探测器逻辑的生物- 一般用
entity.getBrain().setMemory(MemoryModuleType<U>, Optional<U>)
来修改记忆 - 有时效的记忆用
Brain::setMemoryWithExpiry
来设定,最后一个long
参数是记忆有效期,过期删除该记忆
public abstract Set<MemoryModuleType<?>> requires()
返回这个探测器需要用到的所有记忆类型,在实例化Brain
时会自动添加到记忆列表中。
SensorType
是探测器类型的类,用于注册探测器类型。与Sensor
的关系类似于EntityType
和Entity
的关系。注册时需要提供一个Supplier<Sensor<E>>
以实例化一个SensorType<? extends Sensor<? super E>>
。注册ResourceKey
是net.minecraft.core.registries.Registries.SENSOR_TYPE
。
Activity
Activity 代表一类动作,需要注册,仅保存一个名字。可以理解为状态机中的“状态”。
Behavior
Behavior 则类似原来的 Goal
系统,是实际执行各种活动的类,不需要注册。
tick 流程
Brain<E>::tick()
函数
这个函数由使用了 Brain 系统的各个生物的 customServerAiStep()
调用,是 Brain
的基础逻辑入口,用来唤起各个 Behavior
public void tick(ServerLevel level, E entity) {
this.forgetOutdatedMemories();
this.tickSensors(level, entity);
this.startEachNonRunningBehavior(level, entity);
this.tickEachRunningBehavior(level, entity);
}
forgetOutdataedMemories
:清空所有过期的记忆tickSensors
:调用所有Sensor
的tick
方法以刷新记忆startEachNonRunningBehavior
:尝试启动所有属于正在进行的 Activity 的 BehaviortickEachRunningBehavior
:执行正在进行的 Behavior
可以看到这里并没有调整 Action 开关的相关代码。原版的实现中,各个生物的 Action 之间的转换是由各自生物的 Ai 类独立控制的,例如蝾螈的 AxolotlAi::updateActivity(Axolotl)
:
public static void updateActivity(Axolotl axolotl) {
Brain<Axolotl> brain = axolotl.getBrain();
Activity activity = (Activity)brain.getActiveNonCoreActivity().orElse((Object)null);
if (activity != Activity.PLAY_DEAD) {
brain.setActiveActivityToFirstValid(ImmutableList.of(Activity.PLAY_DEAD, Activity.FIGHT, Activity.IDLE));
if (activity == Activity.FIGHT && brain.getActiveNonCoreActivity().orElse((Object)null) != Activity.FIGHT) {
brain.setMemoryWithExpiry(MemoryModuleType.HAS_HUNTING_COOLDOWN, true, 2400L);
}
}
}
就是在 Axolotl::customServerAiStep()
中调用的:
protected void customServerAiStep() {
this.level().getProfiler().push("axolotlBrain");
this.getBrain().tick((ServerLevel)this.level(), this);
this.level().getProfiler().pop();
this.level().getProfiler().push("axolotlActivityUpdate");
AxolotlAi.updateActivity(this);
this.level().getProfiler().pop();
if (!this.isNoAi()) {
Optional<Integer> optional = this.getBrain().getMemory(MemoryModuleType.PLAY_DEAD_TICKS);
this.setPlayingDead(optional.isPresent() && (Integer)optional.get() > 0);
}
}
操作状态(Activity)
操作状态相关的函数有好几个