GOAP 目标导向行动规划:行为树之外的游戏 AI 决策范式

一、行为树的盲区:目标与手段的耦合

行为树解决了状态机分支爆炸的问题,但它有一个结构性短板:行为优先级是策划手工写死的。当"逃跑"和"反击"两个分支都在行为树里时,AI 选择哪条,取决于策划把哪个排在更高优先级,而非取决于"当前世界状态下哪条路径能达成目标"。

这导致一个典型现象:行为树 AI 在面对未预见的局面时,会表现得机械而缺乏"目的性"。比如一个 NPC 本应去拿武器再战斗,但行为树里"战斗"优先级高于"取物",于是它空着手硬刚,逻辑上没毛病,观感上却很蠢。GOAP(Goal-Oriented Action Planning,目标导向行动规划)正是为解耦"目标"与"手段"而生。

二、GOAP 的规划机制与世界状态转移

GOAP 把 AI 决策拆成三层:目标(Goal)、世界状态(World State)、动作(Action)。规划器从当前世界状态出发,在动作图上做前向搜索,找到一条能让世界状态满足目标的动作序列。手段是搜索出来的,而不是写死的。

flowchart TD
    A[当前世界状态] --> B[选定可达成的目标]
    B --> C[A* 在动作图上前向搜索]
    C --> D[回溯得到动作序列]
    D --> E[执行器逐步实施动作]
    E --> F[动作改变世界状态]
    F --> A

行为树负责"怎么执行单个动作",GOAP 负责"先执行哪些动作"。二者常组合使用:GOAP 产出计划,行为树执行计划里的每个原子动作。这样 AI 既能涌现目的性,又不失可控性。

三、生产级 GOAP 动作与规划器实现

下面是一个最小但完整的 GOAP 核心:动作带前置条件与效果,规划器用带代价的 BFS/A* 搜索最优动作序列。

public class WorldState {
    public bool hasWeapon;
    public bool enemyInRange;
    public bool lowHealth;
}

public abstract class GoapAction {
    public Dictionary<string, bool> Preconditions = new();
    public Dictionary<string, bool> Effects = new();
    public float Cost = 1f;
    public abstract bool Execute(WorldState ws);
}

// 取武器动作:前置"无武器",效果"有武器",代价低
public class PickWeapon : GoapAction {
    public PickWeapon() {
        Preconditions["hasWeapon"] = false;
        Effects["hasWeapon"] = true;
        Cost = 1f;
    }
    public override bool Execute(WorldState ws) {
        if (ws.hasWeapon) return false; // 前置不满足直接失败,由规划器跳过
        ws.hasWeapon = true;
        return true;
    }
}

// 规划器:从当前状态搜索到满足目标的最优动作链(代价最小优先)
public class GoapPlanner {
    public List<GoapAction> Plan(WorldState start, System.Func<WorldState,bool> goal, List<GoapAction> actions) {
        // 简化:按代价贪心扩展,生产环境应替换为带剪枝的 A*
        var open = new Queue<(WorldState, List<GoapAction>)>();
        open.Enqueue((start, new List<GoapAction>()));
        while (open.Count > 0) {
            var (state, path) = open.Dequeue();
            if (goal(state)) return path; // 命中目标即返回
            foreach (var a in actions) {
                if (!PreconditionMet(a, state)) continue; // 前置不满足跳过
                var ns = Clone(state); a.Execute(ns);
                var np = new List<GoapAction>(path) { a };
                open.Enqueue((ns, np));
            }
        }
        return null; // 无可行计划,交由行为树兜底
    }
    private bool PreconditionMet(GoapAction a, WorldState s) => a.Preconditions.All(kv => GetField(s,kv.Key)==kv.Value);
    private bool GetField(WorldState s, string k) => k=="hasWeapon"? s.hasWeapon : false;
    private WorldState Clone(WorldState s) => new() { hasWeapon = s.hasWeapon, enemyInRange = s.enemyInRange, lowHealth = s.lowHealth };
}

这段实现的关键工程点:当规划器找不到可行计划时返回 null,调用方必须回落到行为树或默认反应,绝不能让 AI 卡死;动作执行前校验前置条件,避免状态不一致导致计划中途断裂。生产环境应把队列扩展替换为带代价的 A* 并加搜索深度上限,防止组合爆炸。

补充一点工程现实:GOAP 的计划结果对动作代价标注极其敏感。同一个取武器再攻击目标,若把取武器代价标得过高,规划器可能选择绕远路直接肉搏,导致观感诡异。因此动作代价不能凭感觉填写,而应来自实测的执行耗时与风险权重,最好支持热更新以便策划在测试中调参。

四、规划爆炸、确定性缺失与实时性边界

GOAP 的最大代价是搜索开销。动作数与世界状态维度稍大,动作图的搜索空间就会指数膨胀,单次规划可能耗时数毫秒甚至更久,直接挤占帧预算。竞技类游戏还要求 AI 决策可复现,但 GOAP 的搜索顺序依赖动作列表排列,轻微的列表调整就可能改变计划,破坏确定性。

另一个隐性问题是"计划脆弱":计划是基于规划时刻的世界状态生成的,执行途中世界剧变(玩家突然消失)时,计划可能已失效,需要中断重规划,带来额外的实时开销。因此 GOAP 一般用于非实时性要求极高的 NPC(如策略层决策),把毫秒级的微操留给行为树或脚本。

所以落地建议:GOAP 负责宏观战术规划,行为树负责微观动作执行;规划加深度上限与缓存,避免每次 Tick 重规划,无可行计划时务必回落到行为树兜底。

五、总结

GOAP 通过前向搜索把"目标"与"手段"解耦,让 AI 能涌现目的性,适合策略层宏观决策。其代价是搜索空间易组合爆炸、决策确定性难保证、计划对世界突变脆弱。工程落地应限定 GOAP 在宏观战术层,由行为树承载微观执行;规划须加深度上限与缓存并以行为树兜底,禁止把 GOAP 用于毫秒级实时微操。动作设计与目标定义需严格收敛状态维度,从源头抑制搜索膨胀。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐