简介
相信看过上一章关于AI的介绍,你应该对行为树的基础知识有一些了解,并且知道游戏中角色AI是基于行为树实现的。在这一章我会简单介绍一些集群AI是如何工作的。
什么是集群AI?
集群AI听起来好像很复杂,实际上集群AI就是一群角色使用同一个AI而已。
创建一个行为节点的实例,想办法让一群角色当前执行这个节点实例,这样就实现了集群AI。
RimWorld源码中有没有一个节点用于集群AI?
有的,RW中有一棵额外的行为树叫做“LordDuty”,其中一个被挂载的节点是“ThinkNode_JoinVoluntarilyJoinableLord”,具有很高的优先级
我们找到这颗行为树,会发现上面有个名字叫做 “ThinkNode_Duty”的节点,集群AI的实现就依赖于这个节点。这个节点涉及一套职责系统来动态改变AI,集群AI只是职责系统的其中一部分功能。
职责节点
职责节点遍历了整个数据库中的职责定义数据,将每个职责对应的行为树作为子节点挂载在职责节点上。也就是说职责节点在运行时会拥有全部的职责定义中的行为树实例。然后会尝试根据角色当前的职责来分配对应职责的行为。
using System;
using Verse;
using Verse.AI;
using Verse.AI.Group;
namespace RimWorld
{
// Token: 0x02000E3F RID: 3647
public class ThinkNode_Duty : ThinkNode
{
// Token: 0x060052AB RID: 21163 RVA: 0x001BF624 File Offset: 0x001BD824
public override ThinkResult TryIssueJobPackage(Pawn pawn, JobIssueParams jobParams)
{
if (pawn.GetLord() == null)
{
Log.Error(pawn + " doing ThinkNode_Duty with no Lord.", false);
return ThinkResult.NoJob;
}
if (pawn.mindState.duty == null)
{
Log.Error(pawn + " doing ThinkNode_Duty with no duty.", false);
return ThinkResult.NoJob;
}
return subNodes[pawn.mindState.duty.def.index].TryIssueJobPackage(pawn, jobParams);
}
// Token: 0x060052AC RID: 21164 RVA: 0x001BF69C File Offset: 0x001BD89C
protected override void ResolveSubnodes()
{
foreach (DutyDef dutyDef in DefDatabase<DutyDef>.AllDefs)
{
//处理每一个职责定义的节点和子节点
dutyDef.thinkNode.ResolveSubnodesAndRecur();
//将所有职责中的节点设置为当前节点的子节点
this.subNodes.Add(dutyDef.thinkNode.DeepCopy());
}
}
}
}
基类方法
/// <summary>
/// 对自身和所有字节点调用ResolveSubnodes
/// </summary>
public void ResolveSubnodesAndRecur()
{
if (this.uniqueSaveKeyInt != -2)
{
return;
}
this.ResolveSubnodes();
for (int i = 0; i < this.subNodes.Count; i++)
{
this.subNodes[i].ResolveSubnodesAndRecur();
}
}
集群AI如何与职责节点联系?
很简单,集群AI拥有一组受其控制的角色,遍历所有的角色,将他们的职责设定为集群AI的职责,之后由于职责节点的高优先级,所有的角色会优先执行同样的行为。
集群AI
说了半天还没有见到集群AI的模块,集群AI在RW种是以“Lord”命名的。
首先要明确一点,集群AI和行为树不一样,集群AI是需要的时候动态生成的,而不是一开始就配置在角色身上的。所以你可能会在事件中看到集群AI的生成,比如袭击事件。
/// <summary>
/// 创建集群AI
/// </summary>
/// <param name="parms"></param>
/// <param name="pawns"></param>
public virtual void MakeLords(IncidentParms parms, List<Pawn> pawns)
{
Map map = (Map)parms.target;
//分组
List<List<Pawn>> list = IncidentParmsUtility.SplitIntoGroups(pawns, parms.pawnGroups);
int @int = Rand.Int;
//每一组生成一个集群AI
for (int i = 0; i < list.Count; i++)
{
List<Pawn> list2 = list[i];
//生成集群AI 设置目标为list2
Lord lord = LordMaker.MakeNewLord(parms.faction, this.MakeLordJob(parms, map, list2, @int), map, list2);
lord.inSignalLeave = parms.inSignalEnd;
QuestUtility.AddQuestTag(lord, parms.questTag);
if (DebugViewSettings.drawStealDebug && parms.faction.HostileTo(Faction.OfPlayer))
{
Log.Message(string.Concat(new object[]
{
"Market value threshold to start stealing (raiders=",
lord.ownedPawns.Count,
"): ",
StealAIUtility.StartStealingMarketValueThreshold(lord),
" (colony wealth=",
map.wealthWatcher.WealthTotal,
")"
}), false);
}
}
}
集群AI的工作模式与AI的工作模式类似,集群AI有一个LordJob,LordJob对应N个LordToil,和Job,Toil的机制很相似。不同的是,LordJob的流程不像Job一样按顺序执行。
在上一章我有介绍过,游戏AI的实现基本使用行为树或状态机。而LordJob的流程是一套状态机的系统。他可以在任意流程之间跳转。行为树有一个缺点就是必须按顺序执行,而与状态机混搭的方式可以规避掉这个缺点。
/// <summary>
/// 创建集群AI工作
/// </summary>
/// <param name="parms"></param>
/// <param name="map"></param>
/// <param name="pawns"></param>
/// <param name="raidSeed"></param>
/// <returns></returns>
protected override LordJob MakeLordJob(IncidentParms parms, Map map, List<Pawn> pawns, int raidSeed)
{
//事件中心或第一个角色的坐标
IntVec3 originCell = parms.spawnCenter.IsValid ? parms.spawnCenter : pawns[0].PositionHeld;
//事件派系与玩家敌对
if (parms.faction.HostileTo(Faction.OfPlayer))
{
//突击殖民者
return new LordJob_AssaultColony(parms.faction, true, true, false, false, true);
}
//非敌对派系 比如动物 尝试在殖民地外寻找随机点突袭
IntVec3 fallbackLocation;
RCellFinder.TryFindRandomSpotJustOutsideColony(originCell, map, out fallbackLocation);
return new LordJob_AssistColony(parms.faction, fallbackLocation);
}
LordToil与Toil的关系?
每个LordJob对应N个LordToil,也就是集群AI的工作流程之一。而LordToil会对应一个Duty职责,从而对应了一颗行为树。一颗行为树上可能有M个节点对应Job,每个节点有K个流程Toil。所以一个LordJob上可能涉及NMK个Toil。
集群AI的状态机
举个复杂的例子,突袭的集群AI,突袭的集群AI包含,突袭、绑架、偷窃、工兵、离开等集群流程。
“stateGraph ”就是LordJob生成的状态机。添加全部流程后,还需要添加某两个流程之间的跳转和触发跳转的条件。 至于集群突袭流程具体就不详细介绍了,突袭Action节点很复杂,仅是寻找合适的目标就有大概300行代码,有兴趣的自己看看。
突袭流程优先级最高的节点并不是突袭节点,而是服用化学增强药物。当集群AI组内受伤人数达到一定数量时,组内角色就会尝试服用药物。
/// <summary>
/// 创建状态图
/// </summary>
/// <returns></returns>
public override StateGraph CreateGraph()
{
var stateGraph = new StateGraph();
LordToil lordToil = null;
//工兵流程
if (sappers)
{
//工兵行为流程
lordToil = new LordToil_AssaultColonySappers();
//使用智能躲避格子
if (useAvoidGridSmart)
{
lordToil.useAvoidGrid = true;
}
//加入状态图
stateGraph.AddToil(lordToil);
//过渡
var transition = new Transition(lordToil, lordToil, true);
//触发器 角色丢失
transition.AddTrigger(new Trigger_PawnLost());
//添加过渡
stateGraph.AddTransition(transition);
}
//突击行为流程
LordToil lordToil2 = new LordToil_AssaultColony();
if (useAvoidGridSmart)
{
lordToil2.useAvoidGrid = true;
}
stateGraph.AddToil(lordToil2);
//离开地图流程
var lordToilExitMap = new LordToil_ExitMap(LocomotionUrgency.Jog, false, true)
{
useAvoidGrid = true
};
stateGraph.AddToil(lordToilExitMap);
if (sappers)
{
//过渡 工兵流程 到 突击流程
var transition2 = new Transition(lordToil, lordToil2);
//触发器 不存在作战工兵
transition2.AddTrigger(new Trigger_NoFightingSappers());
stateGraph.AddTransition(transition2);
}
//突击者派系是类人派系
if (assaulterFaction.def.humanlikeFaction)
{
//可以超时或逃离
if (canTimeoutOrFlee)
{
//过渡 突击流程 到 离开地图流程
var transition3 = new Transition(lordToil2, lordToilExitMap);
//工兵流程存在
if (lordToil != null)
{
//储存工兵流程
transition3.AddSource(lordToil);
}
//添加过渡触发器 超时
transition3.AddTrigger(new Trigger_TicksPassed(sappers
? SapTimeBeforeGiveUp.RandomInRange
: AssaultTimeBeforeGiveUp.RandomInRange));
//触发前回调
transition3.AddPreAction(new TransitionAction_Message(
"MessageRaidersGivenUpLeaving".Translate(this.assaulterFaction.def.pawnsPlural.CapitalizeFirst(),
this.assaulterFaction.Name), null, 1f));
stateGraph.AddTransition(transition3);
//过渡 突击流程 到 离开地图流程
var transition4 = new Transition(lordToil2, lordToilExitMap);
if (lordToil != null)
{
transition4.AddSource(lordToil);
}
//伤害承受比例 取0.25到0.35随机数
var floatRange = new FloatRange(0.25f, 0.35f);
var randomInRange = floatRange.RandomInRange;
//触发器 达到承受比例 或超过900点
transition4.AddTrigger(new Trigger_FractionColonyDamageTaken(randomInRange, 900f));
//突击者造成破坏 心满意足离开
transition4.AddPreAction(new TransitionAction_Message(
"MessageRaidersSatisfiedLeaving".Translate(assaulterFaction.def.pawnsPlural.CapitalizeFirst(),
assaulterFaction.Name)));
stateGraph.AddTransition(transition4);
}
//允许绑架
if (canKidnap)
{
var startingToil = stateGraph.AttachSubgraph(new LordJob_Kidnap().CreateGraph()).StartingToil;
var transition5 = new Transition(lordToil2, startingToil, false, true);
if (lordToil != null)
{
transition5.AddSource(lordToil);
}
transition5.AddPreAction(new TransitionAction_Message(
"MessageRaidersKidnapping".Translate(this.assaulterFaction.def.pawnsPlural.CapitalizeFirst(),
this.assaulterFaction.Name), null, 1f));
transition5.AddTrigger(new Trigger_KidnapVictimPresent());
stateGraph.AddTransition(transition5, false);
}
//允许偷窃
if (canSteal)
{
var startingToil2 = stateGraph.AttachSubgraph(new LordJob_Steal().CreateGraph()).StartingToil;
//过渡 突击 到 偷窃
var transition6 = new Transition(lordToil2, startingToil2, false, true);
if (lordToil != null)
{
transition6.AddSource(lordToil);
}
transition6.AddPreAction(new TransitionAction_Message(
"MessageRaidersStealing".Translate(this.assaulterFaction.def.pawnsPlural.CapitalizeFirst(),
this.assaulterFaction.Name), null, 1f));
//触发方式 有高价值物品在附近 300个tick内集群无成员受伤
transition6.AddTrigger(new Trigger_HighValueThingsAround());
stateGraph.AddTransition(transition6, false);
}
}
//过渡 突击流程 到 离开地图流程
var transition7 = new Transition(lordToil2, lordToilExitMap, false, true);
if (lordToil != null)
{
transition7.AddSource(lordToil);
}
//触发方式 阵营关系变为非敌对
transition7.AddTrigger(new Trigger_BecameNonHostileToPlayer());
transition7.AddPreAction(new TransitionAction_Message(
"MessageRaidersLeaving".Translate(this.assaulterFaction.def.pawnsPlural.CapitalizeFirst(), this.assaulterFaction.Name),
null, 1f));
stateGraph.AddTransition(transition7, false);
return stateGraph;
}