如何动态生成GOAP的Action代价

解读

国内Unity项目把GOAP(Goal-Oriented Action Planning)当“AI行为树进阶方案”来用,面试官问“动态代价”并不是考你背公式,而是看你是否能把策划意图、运行时数据、性能预算三者量化到代价函数里,并且能在移动端毫秒级更新。回答必须体现“Unity语境”:C# + 增量更新 + 内存可控 + 热插拔。

知识点

  1. GOAP代价三要素:预置基价(Design Cost)世界状态权重(World State Weight)运行时动态系数(Runtime Multiplier)
  2. Unity内常见量化维度:
    • 时间片预算(帧率守护,移动端≤1 ms)
    • 内存GC.Alloc(Action对象池复用,避免foreach new)
    • 路径长度预估(A节点数平均单节点耗时)
    • 动画资源占用(Animator.CullingMode、GPU Skinning开关)
    • 电量/发热(Profiler.GetTotalReservedMemoryLong + 设备温度接口)
  3. 动态更新策略:
    • 分层刷新:每帧只更新“活跃Agent”的代价缓存,其余用脏标记。
    • 差分更新:世界状态变化时,只重算受影响的Action子集,用位掩码+事件总线派发。
    • 热重载:策划在Excel填“基价”,通过ScriptableObject + Addressable热更,不重启客户端。
  4. 代码落地:用Unity Job System并行计算代价数组,结果写进NativeHashMap,主线程零GC。

答案

“我通常把Action代价拆成三层:
第一层,策划基价——在ScriptableObject里配一个ushort,表示‘设计期望’,热更时直接替换AssetBundle,不重启游戏。
第二层,世界状态权重——运行时用BitArray64标记哪些世界符号变化,只重算受影响的Action;权重用曲线ScriptableCurve,方便策划调手感。
第三层,运行时动态系数——每帧在Unity Job里并行采样:

  • 路径长度:用A*的H函数预估剩余步数,乘上0.05 ms的节点平均耗时;
  • 动画开销:取当前Animator.GetCurrentAnimatorStateInfo(0).length,乘上GPU Skinning开关系数(0.7或1.0);
  • 设备发热:Android端读/proc/thermal/tz-by-name/cpu-0-usr/temp,>55 ℃时乘1.5惩罚。
    最后把三层代价累加,clamp到ushort范围,写进NativeSlice,主线程直接拿来做A*排序,单帧耗时稳定在0.8 ms以内,GC.Zero。如果策划想临时让某个技能‘优先级爆炸’,我在Inspector里勾一个‘Force High Priority’布尔,逻辑层直接给该Action代价减10000,不走热更,秒级验证。”

拓展思考

  1. 如果项目把GOAP跑在WebGL上,Job System不可用,可把代价计算退化成WebGL-thread safe的协程分片:每帧最多算8个Action,用双缓冲队列,保证不卡主线程。
  2. 当场景里同屏Agent>200时,代价缓存会爆内存,可引入LOD式代价:距离摄像机>30 m的Agent共用一张“模板代价表”,近处才用高精度个性化代价。
  3. 为了支持策划线上调试,可在Development Build下把代价日志打成Google Protobuf流,通过Unity Cloud Save实时同步到策划手机,策划改完基价回传,客户端ApplyDiff后秒级生效,全程不打断玩家。