1. 项目概述:为什么GoAP是Unity AI开发的“瑞士军刀”?
在Unity里做AI,你是不是也经历过这样的循环:写一堆if-else判断角色状态,代码越堆越乱,最后变成一坨难以维护的“面条代码”?或者用行为树(Behavior Tree)感觉节点拖来拖去,逻辑复杂了之后连线都看不清,调试起来更是头疼?如果你正在为角色行为逻辑的可维护性和可扩展性发愁,那今天聊的GoAP(Goal-Oriented Action Planning,目标导向行为规划)很可能就是你的解药。这不是什么高深莫测的学术概念,而是一种让AI角色自己“动脑子”决定怎么做事的实用架构。简单说,你不再需要手把手告诉AI“先A再B然后C”,而是告诉它“你的目标是拿到宝藏”,并给它一套“可以走路、可以开门、可以打败守卫”的行动库,AI自己就会规划出一条“走到门前->开门->打败守卫->拿到宝藏”的行动序列。
为什么在Unity的众多AI方案里,我尤其推荐你试试GoAP?因为它完美解决了行为树和状态机的两大痛点:僵化和臃肿。行为树需要你预先设计好所有分支,一旦需求变更(比如新增一个“贿赂守卫”的选项),你可能要重构整棵树。而GoAP是动态规划的,AI在运行时根据当前世界状态(World State)和可用行动(Actions),实时计算出一条最优路径来达成目标(Goal)。这意味着你的AI会更加灵活和智能,能应对你未曾预料到的场景组合。从《F.E.A.R.》里的经典敌人到许多独立游戏中的NPC,GoAP都证明了其价值。对于Unity开发者,无论是制作需要复杂决策的RPG同伴、具有策略性的RTS单位,还是行为丰富的模拟市民,掌握GoAP都能让你从繁琐的状态管理中解放出来,专注于设计更有趣的行为本身。
2. GoAP核心原理:让AI学会“思考”而非“执行”
要理解GoAP,我们可以把它想象成一个AI版的“路径规划”问题。不过它规划的不是空间中的路线,而是逻辑状态空间中的路线。其核心是三个概念:世界状态(World State)、目标(Goal)和行动(Action)。
世界状态就是游戏世界在某一时刻所有相关事实的集合,通常用一个键值对字典(Dictionary<string, object>)表示。比如:{ “hasWeapon”: true, “enemyVisible”: false, “health”: 75, “distanceToTarget”: 15.2f }。这些是AI做决策的“已知条件”。
目标则是一个具体的、AI想要达成的世界状态。它也是一个世界状态的子集,描述了“理想情况”。例如,一个“攻击敌人”的目标可能是{ “enemyIsDefeated”: true }。一个“治疗自己”的目标可能是{ “health”: 100 }(假设满血是100)。
行动是AI可以执行的基本单元。每个行动都有:
- 前提条件(Preconditions):执行这个行动前,世界必须满足的状态。比如“开枪”行动的前提可能是
{ “hasWeapon”: true, “enemyVisible”: true, “ammoCount” > 0 }。 - 执行效果(Effects):执行这个行动后,会对世界状态产生的影响。比如“开枪”的效果可能是
{ “enemyHealth”: -20, “ammoCount”: -1 }(让敌人生命减20,弹药减1)。 - 成本(Cost):执行这个行动所需的“代价”,可以简单理解为时间、体力或魔法值。规划器会寻找总成本最低的行动序列。
GoAP规划器(Planner)的工作流程,就是一个反向搜索的过程:
- 从目标出发:规划器拿到一个目标(例如
{ “enemyIsDefeated”: true })。 - 寻找匹配行动:在当前的世界状态中,寻找那些效果能部分满足目标的行动。比如找到一个“致命攻击”行动,其效果是
{ “enemyIsDefeated”: true }。 - 检查前提条件:查看“致命攻击”的前提条件,比如
{ “enemyIsStunned”: true, “isInMeleeRange”: true }。如果当前世界状态不满足这些前提,就把这些前提当作新的、需要达成的“子目标”。 - 递归搜索:继续为这些子目标寻找能达成它们的行动,并检查新行动的前提……如此递归,直到找到的所有行动,其前提条件都能被当前世界状态满足。
- 生成序列:将这个过程找到的行动链反向,就得到了从当前状态到目标状态的行动序列。规划器通常会使用A*等搜索算法,在众多可能的行动序列中找到总成本最低的一条。
注意:这里有一个关键理解点。GoAP是离线规划(Offline Planning),它在每个决策周期(比如每秒)计算一次计划,然后让AI执行这个计划序列,直到计划完成或被打断(比如世界状态发生剧变)。它不适合需要每帧做出瞬时反应(如格斗游戏的闪避)的场景,后者更适合行为树或状态机。
2.1 与行为树、状态机的本质区别
为了更直观,我们用一个表格对比一下:
| 特性 | 有限状态机 (FSM) | 行为树 (Behavior Tree) | 目标导向行为规划 (GoAP) |
|---|---|---|---|
| 核心思想 | 定义状态和转移条件 | 定义任务节点和执行顺序 | 定义行动、前提和效果,动态生成计划 |
| 决策方式 | 基于当前状态和输入事件 | 从根节点开始,按节点逻辑(序列、选择、并行等)遍历 | 基于当前世界状态和目标,使用搜索算法(如A*)规划 |
| 灵活性 | 低。状态和转移需预先定义,新增逻辑可能需重构整个状态机。 | 中。树结构清晰,但复杂后难以维护,变更可能影响多个分支。 | 高。只需增加新的行动或修改现有行动的代价/效果,AI能自动组合出新行为。 |
| 可维护性 | 复杂后极差(“面条代码”)。 | 尚可,但树形结构庞大后连线复杂,调试困难。 | 优秀。行动是独立的、可复用的模块,数据驱动,易于调试(可打印规划路径)。 |
| 适用场景 | 简单、确定性的行为(如开门动画、巡逻状态)。 | 复杂的、层次化的行为逻辑(如BOSS战多阶段)。 | 目标驱动、需要智能组合基础动作的复杂行为(如《模拟人生》中市民的日常、RTS单位的资源采集链)。 |
| 性能开销 | 极低 | 低到中(取决于树深度和节点复杂度) | 中到高(搜索算法的开销,行动和状态越多越耗时)。 |
实操心得:不要试图用GoAP完全取代行为树或状态机。一个更佳的架构是混合使用。例如,用行为树或状态机管理AI的高层状态(如“巡逻”、“战斗”、“逃跑”),而在“战斗”状态下,使用一个GoAP规划器来动态决定具体的战术动作(是“寻找掩体”还是“投掷手雷”)。这样既能利用行为树的响应速度,又能获得GoAP的规划智能。
3. 在Unity中从零搭建GoAP系统
理论懂了,手会了吗?接下来我们一步步在Unity里实现一个完整的GoAP系统。我们将创建一个简单的场景:一个AI守卫,它的目标是“保护宝藏”。它拥有“巡逻”、“发现玩家”、“追击”、“攻击”等行为。
3.1 定义核心数据模型
首先,我们需要定义最基础的类:WorldState、GoapAction和GoapGoal。
// WorldState.cs using System.Collections.Generic; public class WorldState { public Dictionary<string, object> states = new Dictionary<string, object>(); public bool HasState(string key) { return states.ContainsKey(key); } public void SetState(string key, object value) { states[key] = value; } public object GetState(string key) { if (states.ContainsKey(key)) return states[key]; return null; } public bool CompareState(Dictionary<string, object> target) { foreach (var kvp in target) { if (!states.ContainsKey(kvp.Key)) return false; if (!states[kvp.Key].Equals(kvp.Value)) return false; } return true; } public WorldState Clone() { WorldState newState = new WorldState(); foreach (var kvp in states) { newState.states.Add(kvp.Key, kvp.Value); } return newState; } }// IGoapAction.cs (接口) public interface IGoapAction { // 行动的唯一标识符 string Name { get; } // 执行此行动的成本 float Cost { get; } // 行动的目标(由哪个GoapAgent持有) GameObject Target { get; set; } // 前提条件 Dictionary<string, object> Preconditions { get; } // 执行效果 Dictionary<string, object> Effects { get; } // 检查前提条件在当前世界状态下是否满足 bool CheckProceduralPrecondition(GoapAgent agent); // 行动是否已完成 bool IsDone { get; } // 每帧执行(如果行动需要时间) void Perform(GoapAgent agent); // 行动开始前调用 void OnActionStart(GoapAgent agent); // 行动结束后调用(无论成功失败) void OnActionEnd(GoapAgent agent, bool success); }// IGoapGoal.cs (接口) public interface IGoapGoal { // 目标的优先级(数值越大越优先) int Priority { get; } // 目标所期望的世界状态 Dictionary<string, object> DesiredState { get; } // 检查此目标在当前世界状态下是否仍然有效/可追求 bool IsValid(GoapAgent agent); // 计算目标对于当前状态的紧迫性(可用于动态调整优先级) float CalculatePriority(GoapAgent agent); }3.2 实现A*规划器(Planner)
这是GoAP的大脑。我们将实现一个基于A*算法的规划器。
// GoapPlanner.cs using System.Collections.Generic; using System.Linq; using UnityEngine; public class GoapPlanner { public Queue<IGoapAction> Plan(GameObject agent, HashSet<IGoapAction> availableActions, WorldState worldState, IGoapGoal goal) { // 重置所有行动的状态 foreach (var action in availableActions) { action.Reset(); // 假设IAction有一个Reset方法,清理临时状态 } // 检查是否有行动能直接满足目标 var usableActions = new HashSet<IGoapAction>(); foreach (var action in availableActions) { if (action.IsAchievableGiven(worldState)) // 检查行动的前提是否可被满足 { usableActions.Add(action); } } // 构建搜索节点 List<Node> leaves = new List<Node>(); Node start = new Node(null, 0, worldState, null); bool success = BuildGraph(start, leaves, usableActions, goal.DesiredState); if (!success) { Debug.LogWarning($"[GoapPlanner] No plan found for goal: {goal.GetType().Name}"); return null; } // 找到代价最小的叶子节点(即达成目标的节点) Node cheapest = null; foreach (Node leaf in leaves) { if (cheapest == null || leaf.runningCost < cheapest.runningCost) cheapest = leaf; } // 从叶子节点回溯到根节点,构建行动序列 List<IGoapAction> result = new List<IGoapAction>(); Node n = cheapest; while (n != null) { if (n.action != null) { result.Insert(0, n.action); // 在头部插入,因为我们是反向回溯 } n = n.parent; } // 将List转换为Queue Queue<IGoapAction> queue = new Queue<IGoapAction>(); foreach (IGoapAction action in result) { queue.Enqueue(action); } return queue; } private bool BuildGraph(Node parent, List<Node> leaves, HashSet<IGoapAction> usableActions, Dictionary<string, object> goal) { bool foundOne = false; // 遍历所有可用行动,看哪些行动的前提能被当前节点状态满足 foreach (var action in usableActions) { if (StateContainsState(parent.state, action.Preconditions)) { // 应用行动的效果,得到新的世界状态 WorldState currentState = parent.state.Clone(); currentState = ApplyActionEffects(currentState, action.Effects); Node node = new Node(parent, parent.runningCost + action.Cost, currentState, action); // 检查新状态是否满足目标 if (StateContainsState(currentState, goal)) { leaves.Add(node); foundOne = true; } else { // 未满足目标,则继续搜索(从剩余行动中移除当前行动,防止循环) HashSet<IGoapAction> subset = ActionSubset(usableActions, action); bool found = BuildGraph(node, leaves, subset, goal); if (found) foundOne = true; } } } return foundOne; } // 判断状态state是否包含子状态substate private bool StateContainsState(WorldState state, Dictionary<string, object> substate) { foreach (var kvp in substate) { if (!state.HasState(kvp.Key)) return false; if (!state.GetState(kvp.Key).Equals(kvp.Value)) return false; } return true; } // 将行动效果应用到世界状态 private WorldState ApplyActionEffects(WorldState state, Dictionary<string, object> effects) { WorldState newState = state.Clone(); foreach (var effect in effects) { newState.SetState(effect.Key, effect.Value); } return newState; } // 获取可用行动的子集(移除已使用的行动) private HashSet<IGoapAction> ActionSubset(HashSet<IGoapAction> actions, IGoapAction removeMe) { HashSet<IGoapAction> subset = new HashSet<IGoapAction>(); foreach (IGoapAction a in actions) { if (!a.Equals(removeMe)) subset.Add(a); } return subset; } // A*搜索中的节点 private class Node { public Node parent; public float runningCost; public WorldState state; public IGoapAction action; public Node(Node parent, float runningCost, WorldState state, IGoapAction action) { this.parent = parent; this.runningCost = runningCost; this.state = state; this.action = action; } } }3.3 创建GoapAgent与具体行动
GoapAgent是承载AI逻辑的MonoBehaviour,它持有规划器、当前目标、行动队列和世界状态。
// GoapAgent.cs using System.Collections.Generic; using UnityEngine; public class GoapAgent : MonoBehaviour { private GoapPlanner planner; private Queue<IGoapAction> currentActions; private IGoapGoal currentGoal; private WorldState worldState; [SerializeField] private List<IGoapAction> availableActions; [SerializeField] private List<IGoapGoal> availableGoals; void Start() { planner = new GoapPlanner(); worldState = new WorldState(); currentActions = new Queue<IGoapAction>(); // 初始化世界状态,例如:{ “seePlayer”: false, “hasTreasure”: false, “health”: 100 } InitializeWorldState(); } void Update() { // 1. 检查当前目标是否仍然有效,或是否有更高优先级的目标 IGoapGoal bestGoal = FindHighestPriorityGoal(); if (currentGoal != bestGoal || currentActions == null || currentActions.Count == 0) { // 目标改变或计划执行完毕,需要重新规划 currentGoal = bestGoal; PlanNewActions(); } // 2. 执行当前行动序列 if (currentActions != null && currentActions.Count > 0) { IGoapAction action = currentActions.Peek(); // 查看队列第一个行动但不移除 if (action.IsDone) { // 行动完成,移出队列 currentActions.Dequeue(); action.OnActionEnd(this, true); } else { // 执行行动 action.Perform(this); } } } private IGoapGoal FindHighestPriorityGoal() { IGoapGoal bestGoal = null; int highestPriority = int.MinValue; foreach (var goal in availableGoals) { if (goal.IsValid(this) && goal.Priority > highestPriority) { highestPriority = goal.Priority; bestGoal = goal; } } return bestGoal; } private void PlanNewActions() { if (currentGoal == null) return; HashSet<IGoapAction> actionSet = new HashSet<IGoapAction>(availableActions); Queue<IGoapAction> plan = planner.Plan(gameObject, actionSet, worldState, currentGoal); if (plan != null) { currentActions = plan; Debug.Log($"[GoapAgent] New plan formed for goal: {currentGoal.GetType().Name}"); foreach (var a in plan) { Debug.Log($" - {a.Name}"); } } else { Debug.LogWarning($"[GoapAgent] Failed to find a plan for goal: {currentGoal.GetType().Name}"); currentActions = new Queue<IGoapAction>(); } } private void InitializeWorldState() { worldState.SetState("seePlayer", false); worldState.SetState("playerInAttackRange", false); worldState.SetState("health", 100); worldState.SetState("hasTreasure", false); worldState.SetState("isAtPost", true); // 是否在岗哨位置 } // 提供方法供外部(如感知系统)修改世界状态 public void UpdateWorldState(string key, object value) { worldState.SetState(key, value); // 世界状态发生重大变化时,可以强制重新规划 // if (key == "seePlayer" && (bool)value == true) PlanNewActions(); } }现在,让我们实现两个具体的行动:PatrolAction(巡逻)和AttackAction(攻击)。
// PatrolAction.cs using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class PatrolAction : MonoBehaviour, IGoapAction { public string Name => "Patrol"; public float Cost => 1.0f; public GameObject Target { get; set; } public bool IsDone => isDone; private NavMeshAgent navAgent; private Vector3[] waypoints; private int currentWaypoint = 0; private bool isDone = false; public Dictionary<string, object> Preconditions => new Dictionary<string, object> { { "isAtPost", false } // 前提:不在岗哨(巡逻就是为了回到岗哨或去下一个点) }; public Dictionary<string, object> Effects => new Dictionary<string, object> { { "isAtPost", true } // 效果:到达岗哨(巡逻点) }; void Start() { navAgent = GetComponent<NavMeshAgent>(); // 假设在Inspector中设置巡逻点 waypoints = new Vector3[] { /* ... 巡逻点位置 ... */ }; } public bool CheckProceduralPrecondition(GoapAgent agent) { // 设置目标为下一个巡逻点 if (waypoints.Length == 0) return false; Target = new GameObject("PatrolTarget"); Target.transform.position = waypoints[currentWaypoint]; return true; } public void Perform(GoapAgent agent) { if (Target == null) { isDone = true; return; } navAgent.SetDestination(Target.transform.position); float distanceToTarget = Vector3.Distance(transform.position, Target.transform.position); if (distanceToTarget < 1.0f) { // 到达当前巡逻点 currentWaypoint = (currentWaypoint + 1) % waypoints.Length; isDone = true; // 本次巡逻行动完成(到达了一个点) } } public void OnActionStart(GoapAgent agent) { isDone = false; Debug.Log($"[{Name}] Starting patrol to waypoint {currentWaypoint}"); } public void OnActionEnd(GoapAgent agent, bool success) { if (Target != null) Destroy(Target); Debug.Log($"[{Name}] Patrol action ended. Success: {success}"); // 更新世界状态:现在在岗哨(巡逻点)了 agent.UpdateWorldState("isAtPost", true); } public void Reset() { isDone = false; } public bool IsAchievableGiven(WorldState state) { /* 简化实现,通常检查Preconditions是否在state中 */ return true; } }// AttackAction.cs using System.Collections.Generic; using UnityEngine; public class AttackAction : MonoBehaviour, IGoapAction { public string Name => "Attack"; public float Cost => 5.0f; // 攻击成本较高,可能消耗体力或暴露自己 public GameObject Target { get; set; } public bool IsDone => isDone; private bool isDone = false; private float attackTimer = 0f; public float attackInterval = 2.0f; public int damagePerAttack = 10; public Dictionary<string, object> Preconditions => new Dictionary<string, object> { { "seePlayer", true }, { "playerInAttackRange", true } }; public Dictionary<string, object> Effects => new Dictionary<string, object> { { "playerIsDefeated", true } // 攻击的最终效果是击败玩家(简化) }; public bool CheckProceduralPrecondition(GoapAgent agent) { // 在实际项目中,这里会通过感知系统找到玩家对象 GameObject player = GameObject.FindGameObjectWithTag("Player"); if (player != null) { Target = player; return true; } return false; } public void Perform(GoapAgent agent) { if (Target == null) { isDone = true; return; } attackTimer += Time.deltaTime; if (attackTimer >= attackInterval) { attackTimer = 0f; // 执行攻击逻辑,例如播放动画,调用Target的受伤方法 Debug.Log($"[{Name}] Attacking {Target.name} for {damagePerAttack} damage!"); // 假设这里减少了玩家的生命值 // 检查玩家是否被击败 // if (playerHealth <= 0) { agent.UpdateWorldState("playerIsDefeated", true); } } // 简化:攻击一次就认为行动完成(实际可能需要多次攻击直到目标死亡) isDone = true; } public void OnActionStart(GoapAgent agent) { isDone = false; attackTimer = 0f; Debug.Log($"[{Name}] Starting attack on {Target.name}"); } public void OnActionEnd(GoapAgent agent, bool success) { Debug.Log($"[{Name}] Attack action ended. Success: {success}"); } public void Reset() { isDone = false; } public bool IsAchievableGiven(WorldState state) { return true; } }3.4 实现目标(Goal)
最后,我们实现一个“保护宝藏”的目标。
// ProtectTreasureGoal.cs using System.Collections.Generic; using UnityEngine; public class ProtectTreasureGoal : MonoBehaviour, IGoapGoal { public int Priority => 50; // 基础优先级 public Dictionary<string, object> DesiredState => new Dictionary<string, object> { { "treasureIsSafe", true } // 期望状态:宝藏安全 }; public bool IsValid(GoapAgent agent) { // 宝藏是否还存在?是否已经被玩家拿走? // 这里可以访问agent的世界状态或查询场景中的宝藏对象 // 简化:始终有效 return true; } public float CalculatePriority(GoapAgent agent) { // 动态计算优先级,例如,看到玩家时优先级提高 WorldState ws = agent.GetWorldState(); // 假设agent有GetWorldState方法 bool seesPlayer = (bool)ws.GetState("seePlayer"); return Priority + (seesPlayer ? 30 : 0); } }4. 性能优化与高级技巧
一个基础的GoAP系统跑起来后,你很快会发现一个问题:搜索爆炸。当行动和世界状态变量很多时,A*搜索的节点数会呈指数级增长,导致规划耗时剧增,卡顿掉帧。以下是几个关键的优化方向:
1. 世界状态剪枝与抽象不要把所有游戏数据都塞进世界状态。只放入对决策有影响的变量。例如,“玩家衣服颜色”通常不影响战斗AI,就不要放进去。同时,可以使用分层抽象。底层状态是具体的(“distanceToPlayer”: 12.5f),但规划时可以使用抽象状态(“playerIsNear”: true)。这能大幅减少状态空间。
2. 行动池预过滤在调用规划器前,先根据当前世界状态过滤掉绝对不可能执行的行动。例如,一个“开车”的行动,前提是{ “hasCar”: true },如果世界状态里“hasCar”是false,那么这个行动根本不用加入搜索。这可以通过在GoapAgent中维护一个“可用行动”列表,并在世界状态变化时更新它来实现。
3. 使用更高效的搜索算法与启发函数标准的A*需要好的启发式函数(Heuristic)来引导搜索。对于GoAP,启发值可以估算为当前状态与目标状态之间差异的“代价”。一个简单但有效的启发函数是计算当前状态与目标状态中不匹配的变量数量。你也可以为每个状态变量赋予不同的权重。
4. 异步规划与计划缓存不要让规划阻塞主线程。可以将规划任务放到另一个线程或使用UnityWebRequest那样的协程异步处理。规划完成后,再将计划队列传回主线程执行。同时,对于常见的目标-状态组合,可以缓存规划结果。如果AI再次处于相同的世界状态并追求相同目标,可以直接使用缓存计划,省去计算开销。
5. 增量式规划与计划修复不是每次都要从头规划。当世界状态发生微小变化(如玩家移动了几米)但未破坏当前计划的前提时,可以尝试修复(Repair)现有计划,而不是重新规划。例如,当前计划是“走到A点->开门”,如果走到一半发现门已经开了,那么只需移除“开门”这个行动即可。
实操心得:调试与可视化GoAP的调试比行为树更抽象。我强烈建议你实现一个调试视图。在Unity Editor中,可以绘制GUI来显示:
- 当前活跃的目标及其优先级。
- 当前执行中的行动序列。
- 当前的世界状态快照。
- 规划器搜索的节点数和耗时。 这能让你直观地理解AI的“思考过程”,快速定位逻辑错误。例如,如果AI总是卡住,你可以看到是因为没有找到有效计划,还是计划中的某个行动无法完成其前提条件。
5. 常见问题与排查实录
即使理解了原理,实际集成时还是会踩坑。下面是我在项目中遇到的一些典型问题及解决方案:
问题1:AI陷入“规划循环”或频繁重新规划。
- 现象:AI不停地规划新计划,刚执行一两个行动就放弃,然后又开始规划,行为抽搐。
- 原因:
- 世界状态更新太频繁:例如,每帧都根据玩家位置更新
distanceToPlayer,导致世界状态持续变化,触发重新规划。 - 行动效果没有正确更新世界状态:行动执行完了,但对应的世界状态(如
“hasKey”: true)没有设置,导致目标永远无法达成,规划器不断尝试新计划。 - 目标优先级波动剧烈:
CalculatePriority方法逻辑有问题,导致目标优先级在阈值上下频繁跳动。
- 世界状态更新太频繁:例如,每帧都根据玩家位置更新
- 解决:
- 对连续变化的状态(如距离)设置阈值或滞后区间。例如,只有当玩家距离变化超过5米时才更新
distanceToPlayer状态。 - 在行动的
OnActionEnd方法中,务必调用agent.UpdateWorldState来反映行动的效果。 - 为目标优先级添加平滑或冷却机制,避免抖动。
- 对连续变化的状态(如距离)设置阈值或滞后区间。例如,只有当玩家距离变化超过5米时才更新
问题2:规划时间过长,导致游戏卡顿。
- 现象:游戏在AI决策时明显掉帧。
- 原因:可用行动太多(>20个),世界状态变量太多(>15个),搜索空间过大。
- 解决:
- 实施“行动预过滤”:如前所述,这是效果最显著的优化。
- 限制搜索深度/时间:在规划器的
BuildGraph函数中,设置一个最大搜索深度(如50层)或最大耗时(如5ms),超时则返回当前找到的最佳(但不一定完整)计划,或执行一个默认的“备用行为”。 - 简化状态表示:用布尔值或枚举代替浮点数。用
“playerIsNear”代替“distanceToPlayer”。
问题3:多个AI争夺同一个目标或资源,行为不合理。
- 现象:两个AI都规划了“拿起唯一一把剑”的行动,导致逻辑冲突。
- 原因:世界状态是每个AI私有的,它们不知道其他AI的意图。
- 解决:引入世界资源管理器(World Resource Manager)。这是一个全局管理器,跟踪关键资源(如武器、位置、任务)的占用情况。AI在规划“拿剑”行动前,需要向管理器“预订”这把剑。如果预订失败,则该行动的前提条件不满足,AI会规划其他方案(比如“用拳头攻击”)。这需要扩展
CheckProceduralPrecondition方法,加入资源锁检查。
问题4:行动执行失败,但计划没有重新规划。
- 现象:AI计划“开门”,但门被锁住了,行动执行失败,AI就傻站着。
- 原因:规划器生成计划后就不再运行,除非主动触发重新规划。
- 解决:在
GoapAgent的Update中,除了检查目标变化,还要监控当前行动的执行状态。如果当前行动执行失败(IsDone为true但success为false),或者执行超时,应立即清除当前计划队列,并触发重新规划。同时,可以考虑让失败的行动在其效果中增加一个“负面状态”(如{ “doorIsLocked”: true }),这样新的规划就会避开这个行动。
问题5:如何与Unity的动画系统、导航系统优雅集成?
- 核心:将
IGoapAction.Perform方法作为协调者,而不是具体执行者。在Perform中:- 调用
navAgent.SetDestination来移动。 - 设置动画参数(如
animator.SetBool(“IsAttacking”, true))。 - 等待这些外部系统完成。可以通过检查
navAgent.remainingDistance来判断是否到达,通过动画状态机事件或定时器来判断攻击动画是否播放完毕。 - 只有所有这些子任务都完成后,才将行动的
IsDone设为true。这样,GoAP行动就成为了一个高级别的“任务”封装,底层实现可以很灵活。
- 调用