前些天又重新看了遍ReGoap框架,果然,都忘光了。。。
还是自己写一个简单的小栗子吧。
Base
Goap的核心在于标签化地去抽象描述世界。通过一系列标签来定义世界的当前【状态】,当然,这个所谓的世界其实是Agent认知中的世界,实际应该是通过感知模块将感受到的数据收集到数据组件,然后通过数据组件更新【记忆】中的世界状态,也包括自身状态,比如饥饿值、疲劳度等等,为了描述简单,下面我们不纠结内部世界外部世界,统一用【世界状态】来表示。
一个状态,就是一些列标签的集合,每个标签表示了这个状态的一部分特性,比如 “{年份:2025,月份:7, 日期 : 2,放假:否} ”这样一个标签集合,就共同描述了一个平平无奇的工作日的状态。可见状态中最核心的部分就是记录各个标签的哈希表。
public enum TestGoapTagType
{
Hungry,
Tired,
HasFood,
InKitchen,
}
public struct TestGoapTag
{
public TestGoapTagType TagType;
public int IntValue;
public bool Equals(TestGoapTag _other)
{
return _other.IntValue == IntValue;
}
}
public class TestGoapState
{
...
private Dictionary<TestGoapTagType, TestGoapTag> mTags;
...
}
【以目标为导向的行为规划】自然需要先生成【目标】然后去【规划】一个可以达成这个目标的【行为】序列。
所谓目标(Goal),是期望将世界状态中的某些指定标签变成什么样子。比如当前世界状态的标签集合是 {饥饿 : true 疲惫 : true} ,有一个目标是“吃饱”,那这个目标就是期望将 {饥饿 : true} 变为 {饥饿 : false},至于“疲惫”这个标签怎么样,不在这个目标的考虑范围之内。可见目标的核心是世界状态的一个子集。另外目标还有一个重要的属性是优先级,Agent总是会尝试将更高优先级的目标设置为当前目标。
public class TestGoapGoal
{
/// <summary>
/// 期望达到的目标状态
/// </summary>
protected TestGoapState mTargetState;
/// <summary>
/// 规划得到的行为序列
/// </summary>
protected Queue<TestGoapAction> mActionQueue;
/// <summary>
/// 优先级,可以在子类中根据需求定制规则,既可以是静态也可以是动态
/// </summary>
/// <returns></returns>
public virtual int GetPriority() => mTargetState.Count;
//其他逻辑
...
}
行为(Action)是达成目标的手段,主要包含两个标签集合:一个是Effects,即行为效果,指当行为执行成功后,会将哪些标签进行怎样的改变;另一个是PreConditions,即前置条件,指世界状态必须满足该集合内标签的要求才能够执行这个行为。这样,一些列满足PreCondition的行为按照一定的顺序依次执行,并在执行结束时各自将自身的Effects作用到世界状态上,从而使世界状态最终达到目标所需的样子。我们所作的规划,就是找到这个行为序列。
public class TestGoapAction
{
/// <summary>
/// 前置条件
/// </summary>
protected TestGoapState mPreconditionState;
/// <summary>
/// 效果
/// </summary>
protected TestGoapState mEffectState;
public string Name { get; set; }
public TestGoapState PreConditions => mPreconditionState;
public TestGoapState Effects => mEffectState;
/// <summary>
/// cost值,用于规划路径
/// 可以在子类中定制,既可以是静态也可以是动态
/// </summary>
/// <returns></returns>
public int GetCost() => mEffectState.Count;
//其他逻辑
...
}
Agent是Goap中的主体,其主要包括:
- 一个Goal列表,表示这个Agent可以进行哪些规划
- 一个Action列表,表示这个Agent可以进行哪些行为
- 一个Memory,也就是这个Agent感知到的世界状态
- 一个CurrentGoal,表示这个Agent当前被设置的目标
public class TestGoapAgent
{
private TestGoapState mMemory;
private List<TestGoapGoal> mAbleGoals;
private List<TestGoapAction> mAbleActions;
public TestGoapGoal CurrentGoal { get; private set; }
//其他逻辑
...
}
Planner
Planner负责为Agent执行规划,核心就是一个Plan方法,根据优先级从高到底依次对目标列表中的目标尝试进行规划,直到找到一个可以规划出行为序列的目标,或者都失败。
大体过程可以分为三步:
- 先用Agent的Action列表对Goal进行粗筛,不考虑Action是否真的满足前置条件,只考虑Action的Effects是不是可以覆盖掉Goal所有的标签要求
- 通过粗筛的Goal视为有可能达成的目标,使用A*逆向进行路径搜索,尝试找到一个可行的行为序列
- A*搜索成功,代表这个Goal真实可达,生成行为序列,并将该目标设置为Agent的当前目标。
当然这中间可以根据需要自己增加诸如迭代次数、规划间隔、紧急目标之类的逻辑。
public void Plan(TestGoapAgent _agent, TestGoapAStar _aStar)
{
List<TestGoapGoal> _possibleGoals = _agent.AbleGoals;
for (int i = _possibleGoals.Count - 1; i >= 0; i--)
{
TestGoapGoal _tmpGoal = _possibleGoals[i];
//第一步,粗筛,目标是否能被Action列表覆盖
...
//第二步,尝试规划Action路径
...
//第三步,用搜索到的路径填充Action列表,并为Agent设置当前目标
...
return;
}
}
路径搜索
路径搜索就是A*搜索的过程,从GoalState出发,按cost依次找对达成当前剩余目标有帮助的Action,消除掉目标中那些可以被Action的Effects满足的标签,如果Action有前置条件,这些前置条件也会变成新的目标标签添加到GoalState中,然后继续搜索下一Action,直至剩余目标被清空(或Action用完、或达到指定的迭代次数)。
具体的内容基本都写在注释里,这里不啰嗦了。
测试
新建了两个Goal的子类:
- 吃饭: 目标是将饥饿度清空,但是优先级比较低
- 睡觉:目标是将疲惫值清空,优先级比较高
新建了三个Action的子类:
- 进食:效果会清空饥饿度,但有两个前置条件,一个是在厨房里,另一个是有食物
- 前往厨房:效果是将标签“在厨房里”置为true,没有前置
- 烹饪:效果是将标签"有食物"置为true,前置是需要在厨房里
这里没有加感知的部分,手动为Agent设置了初始的Memory,饥饿度和疲惫值都为1。
因此,根据优先级,Planner会先为Agent规划【睡觉】,但由于Action不足以覆盖目标,无法通过粗筛,该目标被丢弃,然后规划【吃饭】,并通过A*查找到可行路径为:【前往厨房】→【烹饪】→【进食】
完整测试代码:
Base:
using System.Collections.Generic;
using UnityEngine;
namespace MapRandom.Goap
{
public enum TestGoapTagType
{
Hungry,
Tired,
HasFood,
InKitchen,
}
public struct TestGoapTag
{
public TestGoapTagType TagType;
public int IntValue;
public bool Equals(TestGoapTag _other)
{
return _other.IntValue == IntValue;
}
}
public class TestGoapState
{
#region Pool
private static Stack<TestGoapState> mPool = new();
public static TestGoapState Pop()
{
TestGoapState _ret = 0 == mPool.Count ? new TestGoapState() : mPool.Pop();
_ret.Init();
return _ret;
}
public static void Push(TestGoapState _state)
{
_state.Clear();
mPool.Push(_state);
}
#endregion
private Dictionary<TestGoapTagType, TestGoapTag> mBuff_1 = new();
private Dictionary<TestGoapTagType, TestGoapTag> mBuff_2 = new();
private Dictionary<TestGoapTagType, TestGoapTag> mTags;
public Dictionary<TestGoapTagType, TestGoapTag> Tags => mTags;
public int Count => mTags.Count;
public void Init()
{
mTags = mBuff_1;
}
public void AddGoapTag(TestGoapTag _tagData)
{
Tags.Add(_tagData.TagType, _tagData);
}
public void Clear()
{
mBuff_1.Clear();
mBuff_2.Clear();
}
public void Clone(TestGoapState _other)
{
mTags.Clear();
foreach (var _kv in _other.Tags)
{
mTags.Add(_kv.Key, _kv.Value);
}
}
public void CoverByOther(TestGoapState _other)
{
foreach (var _kv in _other.Tags)
{
var _key = _kv.Key;
var _value = _kv.Value;
if (mTags.ContainsKey(_key))
{
mTags[_key] = _value;
}
else
{
mTags.Add(_key, _value);
}
}
}
public bool HasAnySameOf(TestGoapState _other)
{
foreach (var _kv in _other.Tags)
{
if (mTags.TryGetValue(_kv.Key, out TestGoapTag _value))
{
if (_value.Equals(_kv.Value))
{
return true;
}
}
}
return false;
}
public bool HasAnyConflictOf(TestGoapState _other)
{
foreach (var _kv in _other.Tags)
{
if (mTags.TryGetValue(_kv.Key, out TestGoapTag _value))
{
if (!_value.Equals(_kv.Value))
{
return true;
}
}
}
return false;
}
public bool HasAnyConflictOfBoth(TestGoapState _other, TestGoapState _fixState)
{
foreach (var _kv in _other.Tags)
{
if (mTags.TryGetValue(_kv.Key, out TestGoapTag _selfValue))
{
if (!_selfValue.Equals(_kv.Value))
{
//与条件冲突,检查冲突是否可以被结果修复
if (!_fixState.Tags.TryGetValue(_kv.Key, out TestGoapTag _fixValue))
{
return true;
}
return !_selfValue.Equals(_fixValue);
}
}
}
return false;
}
public void RemoveSameToCollector(TestGoapState _other, TestGoapState _diffCollector)
{
foreach (var _kv in mTags)
{
if (_other.Tags.TryGetValue(_kv.Key, out TestGoapTag _otherValue) && _kv.Value.Equals(_otherValue))
{
continue;
}
_diffCollector.Tags.Add(_kv.Key, _kv.Value);
}
}
public void RemoveSameToSelf(TestGoapState _other)
{
Dictionary<TestGoapTagType, TestGoapTag> _next = mTags == mBuff_1 ? mBuff_2 : mBuff_1;
_next.Clear();
foreach (var _kv in mTags)
{
if (_other.Tags.TryGetValue(_kv.Key, out TestGoapTag _otherValue) && _kv.Value.Equals(_otherValue))
{
continue;
}
_next.Add(_kv.Key, _kv.Value);
}
mTags = _next;
}
}
public class TestGoapGoal
{
/// <summary>
/// 期望达到的目标状态
/// </summary>
protected TestGoapState mTargetState;
/// <summary>
/// 规划得到的行为序列
/// </summary>
protected Queue<TestGoapAction> mActionQueue;
public string Name { get; set; }
public TestGoapState TargetState => mTargetState;
/// <summary>
/// 优先级,可以在子类中根据需求定制规则,既可以是静态也可以是动态
/// </summary>
/// <returns></returns>
public virtual int GetPriority() => mTargetState.Count;
public TestGoapGoal()
{
mTargetState = TestGoapState.Pop();
mActionQueue = new();
OnCreate();
}
protected virtual void OnCreate() {}
public void ResetActionQueue(TestGoapAStarNode _node)
{
mActionQueue.Clear();
while (null != _node)
{
TestGoapAction _action = _node.GetAction();
if (null != _action)
{
mActionQueue.Enqueue(_action);
}
_node = _node.GetParentNode();
}
}
public void Run()
{
Debug.Log($"-----------------------------------------");
Debug.Log($" =====> 执行目标 : 【{Name}】");
while (mActionQueue.Count > 0)
{
TestGoapAction _curAction = mActionQueue.Dequeue();
_curAction.Run();
}
}
}
public class TestGoapAction
{
/// <summary>
/// 前置条件
/// </summary>
protected TestGoapState mPreconditionState;
/// <summary>
/// 效果
/// </summary>
protected TestGoapState mEffectState;
public string Name { get; set; }
public TestGoapState PreConditions => mPreconditionState;
public TestGoapState Effects => mEffectState;
/// <summary>
/// cost值,用于规划路径
/// 可以在子类中定制,既可以是静态也可以是动态
/// </summary>
/// <returns></returns>
public int GetCost() => mEffectState.Count;
public TestGoapAction()
{
mPreconditionState = TestGoapState.Pop();
mEffectState = TestGoapState.Pop();
OnCreate();
}
protected virtual void OnCreate() {}
public void Run()
{
Debug.Log($" ===========> 执行行为 : 【{Name}】");
}
}
public class TestGoapAgent
{
private TestGoapState mMemory;
private List<TestGoapGoal> mAbleGoals;
private List<TestGoapAction> mAbleActions;
public TestGoapGoal CurrentGoal { get; private set; }
public TestGoapState Memory => mMemory;
public List<TestGoapGoal> AbleGoals => mAbleGoals;
public List<TestGoapAction> AbleActions => mAbleActions;
public TestGoapAgent()
{
mMemory = TestGoapState.Pop();
mAbleGoals = new();
mAbleActions = new();
}
public void SetCurrentGoal(TestGoapGoal _goal)
{
Debug.LogError($"Agent设置新的目标 : {_goal.Name} !!!");
CurrentGoal = _goal;
}
public void RunCurrentGoal()
{
if (null == CurrentGoal)
{
return;
}
CurrentGoal.Run();
}
/// <summary>
/// 优先级越大越优先
/// </summary>
/// <param name="_goal"></param>
public void AddAbleGoal(TestGoapGoal _goal)
{
int _priority = _goal.GetPriority();
for (int i = 0; i < mAbleGoals.Count; i++)
{
if (_priority > mAbleGoals[i].GetPriority())
{
continue;
}
mAbleGoals.Insert(i, _goal);
return;
}
mAbleGoals.Add(_goal);
}
/// <summary>
/// Cost越小越优先
/// </summary>
/// <param name="_action"></param>
public void AddAbleAction(TestGoapAction _action)
{
int _cost = _action.GetCost();
for (int i = 0; i < mAbleActions.Count; i++)
{
if (_cost < mAbleActions[i].GetCost())
{
continue;
}
mAbleActions.Insert(i, _action);
return;
}
mAbleActions.Add(_action);
}
}
}
Planner:
using System.Collections.Generic;
using UnityEngine;
namespace MapRandom.Goap
{
public class TestGoapPlanner
{
public void Plan(TestGoapAgent _agent, TestGoapAStar _aStar)
{
List<TestGoapGoal> _possibleGoals = _agent.AbleGoals;
for (int i = _possibleGoals.Count - 1; i >= 0; i--)
{
TestGoapGoal _tmpGoal = _possibleGoals[i];
//第一步,粗筛,目标是否能被Action列表覆盖
if (!BroadCheckAction(_agent, _tmpGoal)) continue;
//第二步,尝试规划Action路径
TestGoapAStarNode _startNode = TestGoapAStarNode.Pop();
_startNode.Init(_agent, _tmpGoal.TargetState, null, null);
TestGoapAStarNode _retNode = _aStar.Search(_startNode);
if (null == _retNode)
{
//没有找到可行路径
Debug.Log($"A* 搜索失败!!! 没有可行的Action路径!!! 目标: {_tmpGoal.Name}");
continue;
}
//第三步,用搜索到的路径填充Action列表,并为Agent设置当前目标
_tmpGoal.ResetActionQueue(_retNode);
_agent.SetCurrentGoal(_tmpGoal);
return;
}
}
private bool BroadCheckAction(TestGoapAgent _agent, TestGoapGoal _goal)
{
TestGoapState _goalState = TestGoapState.Pop();
_goalState.Clone(_goal.TargetState);
List<TestGoapAction> _ableActions = _agent.AbleActions;
foreach (TestGoapAction _action in _ableActions)
{
_goalState.RemoveSameToSelf(_action.Effects);
if (0 == _goalState.Count)
{
Debug.LogError($"BroadCheckAction 检查成功!!! Action可以覆盖目标: {_goal.Name}");
return true;
}
}
Debug.Log($"BroadCheckAction 检查失败!!! Action不能覆盖目标: {_goal.Name}");
return false;
}
}
}
A*:
using System;
using System.Collections.Generic;
using MapRandom.MapRandomCommon;
namespace MapRandom.Goap
{
public class TestGoapAStar
{
private MinPriorityQueue<TestGoapAStarNode, int> mOpenList = new();
private Dictionary<TestGoapState, TestGoapAStarNode> mCloseList = new();
public TestGoapAStarNode Search(TestGoapAStarNode _startNode)
{
mOpenList.Enqueue(_startNode, _startNode.GetCost());
while (mOpenList.TryDequeue(out (TestGoapAStarNode node, int priority) _tuple))
{
var _node = _tuple.node;
if (_node.CheckGoalFinish())
{
//目标被满足,找到终点
return _node;
}
mCloseList.Add(_node.GetSubMemory(), _node);
List<TestGoapAStarNode> _expandList = _node.Expand();
foreach (var _childNode in _expandList)
{
if (mCloseList.ContainsKey(_childNode.GetSubMemory()))
{
continue;
}
mOpenList.Enqueue(_childNode, _childNode.GetCost());
}
}
return null;
}
}
public class TestGoapAStarNode : IComparable<TestGoapAStarNode>
{
#region Pool
private static Stack<TestGoapAStarNode> mPool = new();
public static TestGoapAStarNode Pop()
{
TestGoapAStarNode _ret = 0 == mPool.Count ? new TestGoapAStarNode() : mPool.Pop();
return _ret;
}
public static void Push(TestGoapAStarNode _node)
{
_node.Clear();
mPool.Push(_node);
}
#endregion
private List<TestGoapAStarNode> mExpandList = new();
private TestGoapAgent mAgent;
private TestGoapAction mAction;
private TestGoapAStarNode mParentNode;
private TestGoapState mCompareToTarget;
private TestGoapState mSubMemory;
private TestGoapState mRemainGoalState;
private int G;
private int H;
public TestGoapAction GetAction() => mAction;
public TestGoapAStarNode GetParentNode() => mParentNode;
public TestGoapState GetSubMemory() => mSubMemory;
public int GetCost() => G + H;
public bool CheckGoalFinish() => 0 == mCompareToTarget.Count;
/// <summary>
///
/// </summary>
/// <param name="_agent"></param>
/// <param name="_remainGoalState"></param>
/// <param name="_action"></param>
/// <param name="_parent"></param>
public void Init(TestGoapAgent _agent, TestGoapState _remainGoalState, TestGoapAction _action, TestGoapAStarNode _parent)
{
mAgent = _agent;
mAction = _action;
mParentNode = _parent;
mSubMemory = TestGoapState.Pop();
if (null == _parent)
{
//是根节点
G = 0;
mSubMemory.Clone(_agent.Memory);
}
else
{
G = _parent.GetCost();
mSubMemory.Clone(_parent.GetSubMemory());
}
mRemainGoalState = TestGoapState.Pop();
mRemainGoalState.Clone(_remainGoalState);
if (null != _action)
{
TestGoapState _preConditions = _action.PreConditions;
TestGoapState _effects = _action.Effects;
mSubMemory.CoverByOther(_effects);
//当前节点能够实现的效果,从剩余目标当中消掉
mRemainGoalState.RemoveSameToSelf(_effects);
//当前节点要能执行需要的前置条件,添加到剩余目标当中
mRemainGoalState.CoverByOther(_preConditions);
G += _action.GetCost();
}
//先简单粗暴的将H设置为剩余目标数量
H = mRemainGoalState.Count;
//至此,mRemainGoalState里留下的就是搜索至当前节点时,还剩余的目标
//这些目标中有的可能是在记忆中已经满足的,收集实际的差异,用于判定是否A*结束
//但是在A*的Search中,当Node弹出查找邻接点时,使用的是更宽泛的mRemainGoalState,可以优化组合
mCompareToTarget = TestGoapState.Pop();
mRemainGoalState.RemoveSameToCollector(_agent.Memory, mCompareToTarget);
}
/// <summary>
/// 找相邻节点
/// </summary>
/// <returns></returns>
public List<TestGoapAStarNode> Expand()
{
mExpandList.Clear();
List<TestGoapAction> _ableActions = mAgent.AbleActions;
for (int i = _ableActions.Count - 1; i >= 0; i--)
{
TestGoapAction _action = _ableActions[i];
if (_action == mAction) continue;
TestGoapState _preConditions = _action.PreConditions;
TestGoapState _effects = _action.Effects;
if (_effects.HasAnySameOf(mRemainGoalState) && //必须对剩余目标的达成有帮助
!mRemainGoalState.HasAnyConflictOfBoth(_preConditions, _effects) && //前置条件不能跟目标有冲突,或者即使有冲突也会被行为的效果修复
!mRemainGoalState.HasAnyConflictOf(_effects) //一个行为可能有多个效果,因此,当其中一个效果有助于达成目标,但另一个效果会破坏目标时也不可以
)
{
TestGoapAStarNode _childNode = TestGoapAStarNode.Pop();
_childNode.Init(mAgent, mRemainGoalState, _action, this);
mExpandList.Add(_childNode);
}
}
return mExpandList;
}
public void Clear()
{
mExpandList.Clear();
//...
}
public int CompareTo(TestGoapAStarNode other)
{
return GetCost().CompareTo(other.GetCost());
}
}
}
Test :
using UnityEngine;
namespace MapRandom.Goap
{
/// <summary>
/// 目标 -- 吃饭
/// </summary>
public sealed class TestGoapGoal_Eat : TestGoapGoal
{
protected override void OnCreate()
{
base.OnCreate();
Name = "目标 -- 吃饭";
mTargetState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.Hungry,
IntValue = 0,
});
}
}
/// <summary>
/// 目标 -- 睡觉
/// </summary>
public sealed class TestGoapGoal_Sleep : TestGoapGoal
{
protected override void OnCreate()
{
base.OnCreate();
Name = "目标 -- 睡觉";
mTargetState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.Tired,
IntValue = 0,
});
}
public override int GetPriority()
{
return 100;
}
}
/// <summary>
/// 行动 -- 进食
/// </summary>
public sealed class TestGoapAction_EatFood : TestGoapAction
{
protected override void OnCreate()
{
base.OnCreate();
Name = "行动 -- 进食";
mPreconditionState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.HasFood,
IntValue = 1,
});
mPreconditionState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.InKitchen,
IntValue = 1,
});
mEffectState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.Hungry,
IntValue = 0,
});
}
}
/// <summary>
/// 行动 -- 前往厨房
/// </summary>
public sealed class TestGoapAction_GoToKitchen : TestGoapAction
{
protected override void OnCreate()
{
base.OnCreate();
Name = "行动 -- 前往厨房";
mEffectState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.InKitchen,
IntValue = 1,
});
}
}
/// <summary>
/// 行动 -- 烹饪
/// </summary>
public sealed class TestGoapAction_Cook : TestGoapAction
{
protected override void OnCreate()
{
base.OnCreate();
Name = "行动 -- 烹饪";
mPreconditionState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.InKitchen,
IntValue = 1,
});
mEffectState.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.HasFood,
IntValue = 1,
});
}
}
public class TestGoapMain : MonoBehaviour
{
private TestGoapState mWorldState;
private TestGoapAgent mAgent;
private TestGoapAStar mAStar;
private TestGoapPlanner mPlanner;
private void Update()
{
if (Input.GetKeyUp(KeyCode.F2))
{
Initialize();
mPlanner.Plan(mAgent, mAStar);
mAgent.RunCurrentGoal();
}
}
private void Initialize()
{
InitTools();
InitWorldState();
InitAgent();
}
private void InitTools()
{
mAStar = new TestGoapAStar();
mPlanner = new TestGoapPlanner();
}
private void InitWorldState()
{
mWorldState = TestGoapState.Pop();
// mWorldState.AddGoapTag();
}
private void InitAgent()
{
mAgent = new TestGoapAgent();
mAgent.AddAbleGoal(new TestGoapGoal_Eat());
mAgent.AddAbleGoal(new TestGoapGoal_Sleep());
mAgent.AddAbleAction(new TestGoapAction_Cook());
mAgent.AddAbleAction(new TestGoapAction_EatFood());
mAgent.AddAbleAction(new TestGoapAction_GoToKitchen());
mAgent.Memory.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.Hungry,
IntValue = 1,
});
mAgent.Memory.AddGoapTag(new TestGoapTag()
{
TagType = TestGoapTagType.Tired,
IntValue = 1,
});
}
}
}