【動作節點數據共享】
行為樹中需要的參數可以來自游戲中的各個模塊,如果僅需從多個模塊獲取少量參數,那么可以直接在代碼中調用其他模塊的單例繼而層層調用獲取數據。
如果獲取的參數量很大,從架構上看,我們需要通過加一個中間者去管理各個模塊的參數獲取調用,行為樹從中間者獲取數據即可。
換一種說法就是要有共享數據的地方,通常會采用黑板模式。
綜合來說,存在以下情況:
- 多個不同的動作節點或條件節點需要獲取或設置來自不同模塊的屬性
- 多個不同的動作節點或條件節點會獲取或設置相同模塊的同一屬性
- 不同動作節點之間有通信,A動作節點生成的臨時數據是B動作節點所需的數據
- 不同動作節點存在大量重復計算,例如距離計算
- 多個動作節點會共用臨時存在的多個數據
針對這些情況,我們可以通過鍵值對的形式實現黑板模式
需要注意的是,這些黑板不屬于節點,考慮到不同行為樹也會共享數據,因此也不一定屬于黑板。需要有一個黑板的管理者來做數據管理。
黑板模式的數據管理本質還是通過鍵值對的方式,為處理不同的情況,我們需要對每種情況提供不同的Key。這和MVC中的數據管理并無本質區別。
和節點參數配置不同的是,這里是要程序做控制的,而且不確定性更大,無法做明確的規定。
在這種情況下,我們需要對每個數據做單獨得ID定義,每個數據有各自的獲取設置方法,通過ID映射。
根據行為樹ID,節點ID,數據ID,方法ID可以實現不同的數據獲取,程序只需實現方法ID即可。
在黑板中,我們需要根據這些參數生成唯一的Key,這里自然而然的就會需要對參數做封裝,用泛型,用對象池。
同樣的,我們需要有這些參數對應的結果,考慮數據類型差異,結果有效性等,自然也需要做封裝。
【動作節點的實現位置】
在整個游戲中,與角色相關的模塊如下:
- 角色動畫,基于狀態機提供動作切換,提供最基礎的接口
- 角色運動,包括基礎移動(走、跑等),地形移動(蹲下、跳躍、攀爬等),尋路。會調用角色動畫提供的接口
- 角色交互:
- 與物體的交互(拾取、推開、握住、抓住、攀繞、踢開等等)。會調用角色動畫或角色運動提供的接口,前者為主
- 與角色的交互(主要是打擊、少量握手、擁抱等)。會調用角色動畫或角色運動提供的接口,前者為主
- 角色技能:技能、Buff、傷害計算、效果表現。會調用角色動畫或角色運動或角色交互提供的接口,前者為主
- 角色屬性:記錄角色的各類狀態
- 角色行為:這里就是角色AI,會調用角色動畫或角色運動或角色交互或角色技能或角色屬性提供的接口
因此,在實現動作節點時,屬于其他模塊的直接調用其他模塊的接口或在其他模塊內實現,屬于角色行為的在動作節點內實現。
例如,就技能而言,對角色技能來說不同角色的技能各有差異,要做不同的實現;但對角色AI而言,只有普攻、1技能、2技能、大招等
【代碼實現】
數據配置
[Serializable]public class DataConfig{public int dataId;public int setMethodId;public int getMethodId;public DataLife dataLife;public bool multi;//同一類型參數,參數值不同,結果不同public bool praseType;//如果解析類型,做自動化生成public bool cache;//是否做數據緩存}public enum DataLife{Persistent,//永久性數據Conditional,//條件性數據,滿足某條件出現,不滿足消失FixedTime,//固定時間內有效的數據FixedFrame,//固定幀數內有效的數據}[CreateAssetMenu(fileName = "BlackBoard_Data_Config", menuName = "BT/BlackBoardDataConfig")]public class BlackBoardDataConfig:ScriptableObject{public List<DataConfig> dataConfigs = new List<DataConfig>();}
數據請求
public class BBDataRequest{public int btId;public int nodeId;public int dataId;public virtual void Release() { }}public class BBDataRequest<Parmas> : BBDataRequest{private static ObjectPool<BBDataRequest<Parmas>> Pool = new ObjectPool<BBDataRequest<Parmas>>(GenBBDataRequest);private static BBDataRequest<Parmas> GenBBDataRequest(){return new BBDataRequest<Parmas>();}public static BBDataRequest<Parmas> GetBBDataRequest(){return Pool.Get();}public static void Release(BBDataRequest<Parmas> data){data.Reset();Pool.Release(data);}public virtual int TryAddParams(Parmas data){return -1;}public virtual int TryAddObject(object data){return -1;}public virtual Parmas GetParmas(int index){return default(Parmas);}public virtual object GetObject(int index){return null;}public override void Release(){Release(this);}public virtual void Reset(){}}public class BBDataRequestSingle<Parmas> : BBDataRequest<Parmas>{public Parmas reqParamsNoBoxing;public object reqParams;public override int TryAddParams(Parmas data){reqParamsNoBoxing = data;return -1;}public override int TryAddObject(object data){reqParams = data;return -1;}public override Parmas GetParmas(int index){return reqParamsNoBoxing;}public override object GetObject(int index){return reqParams;}public override void Reset(){reqParamsNoBoxing = default(Parmas);reqParams = default(object);}}public class BBDataRequestMulti<Parmas>:BBDataRequest<Parmas>{public Dictionary<Parmas,int> paramsNoBoxingIndex = new Dictionary<Parmas,int>();public Dictionary<object,int> paramsIndex = new Dictionary<object,int>();private Dictionary<int, Parmas> index2ParamsNoBoxing = new Dictionary<int, Parmas>();private Dictionary<int,object> index2Params = new Dictionary<int,object>();public override int TryAddParams(Parmas data){if(!paramsNoBoxingIndex.TryGetValue(data,out int res)){res = paramsNoBoxingIndex.Count;paramsNoBoxingIndex[data] = res;}index2ParamsNoBoxing[res] = data;return res;}public override int TryAddObject(object data){if(!paramsIndex.TryGetValue(data,out int res)){res = paramsIndex.Count;paramsIndex[data] = res;}index2Params[res] = data;return res;}public override object GetObject(int index){return index2Params[index];}public override Parmas GetParmas(int index){return index2ParamsNoBoxing[index];}public override void Reset(){paramsIndex.Clear();index2Params.Clear();index2ParamsNoBoxing.Clear();paramsNoBoxingIndex.Clear();}}
數據結果
public class BBDataResult<Result> : IBBDataResult{public int dataId { get; set; }public DataConfig config { get; set; }public BBDataRequest request { get; set; } public float lifeTime;public float curTime;public int curFrame;public bool Valid(){switch(config.dataLife){case DataLife.FixedTime:case DataLife.FixedFrame:return curTime > lifeTime;case DataLife.Conditional:case DataLife.Persistent: return true;}return true; }public virtual bool Getted(int frameCount, int index){return false;}public virtual Result GetCurResult(int index){return default(Result);}public virtual void SetGetResult(Result value, int index){}public virtual void SetCurResult(Result value,int index){}public virtual void Tick(float deltaTime){curFrame = Time.frameCount;if (config.dataLife == DataLife.FixedTime){curTime += deltaTime;}if(config.dataLife == DataLife.FixedFrame){curTime += 1;}}public virtual void Reset(){curTime = 0;curFrame = 0;}private static ObjectPool<BBDataResult<Result>> Pool = new ObjectPool<BBDataResult<Result>>(GenBBDataResult);private static BBDataResult<Result> GenBBDataResult(){return new BBDataResult<Result>();}public static BBDataResult<Result> GetBBDataResult(){return Pool.Get();}public static void Release(BBDataResult<Result> bbDataResult){bbDataResult.Reset();Pool.Release(bbDataResult);}public void Release(){Release(this);}}public class BBDataResultSingle<Result>: BBDataResult<Result>{public Result result;public bool getted;public override void SetGetResult(Result value, int index){result = value;getted = true;}public override void SetCurResult(Result value,int index){result = value;getted = false;}public override bool Getted(int frameCount, int index){return getted && frameCount == curFrame;}public override void Tick(float deltaTime){base.Tick(deltaTime);getted = false;}public override void Reset(){getted = false;result = default(Result);}public override Result GetCurResult(int index){return result;}}public class BBDataResultMulti<Result>: BBDataResult<Result>{public Dictionary<int,Result> resultIndex = new Dictionary<int,Result>();public Dictionary<int, bool> getted = new Dictionary<int, bool>();public override void SetGetResult(Result value,int index){resultIndex[index] = value;getted[index] = true;}public override void SetCurResult(Result value, int index){resultIndex[index] = value;getted[index] = false;}public override bool Getted(int frameCount,int index){return getted[index] && frameCount == curFrame;}public override void Tick(float deltaTime){base.Tick(deltaTime);foreach (var item in getted.Keys){getted[item] = false;}}public override void Reset(){getted.Clear();resultIndex.Clear();}public override Result GetCurResult(int index){return resultIndex[index];}}
黑板類及其管理者
public class BlackBoardManager{private static BlackBoardManager instance;private BlackBoardManager() { }public static BlackBoardManager Instance{get{if (instance == null){instance = new BlackBoardManager();}return instance;}}public Dictionary<int, BlackBoard> id2BB = new Dictionary<int, BlackBoard>();private Dictionary<int,DataConfig> dataConfig = new Dictionary<int, DataConfig>();public void Init(){BlackBoard bb = new BlackBoard();bb.bbId = 1;id2BB[1] = bb;//load配置數據 }public void Tick(float deltaTime){foreach (var bb in id2BB.Values){bb.Tick(deltaTime);}}public BlackBoard CreateBlackBoard(bool common){if (common){return id2BB[1];}else{BlackBoard bb = new BlackBoard();bb.bbId = id2BB.Count + 1;id2BB[bb.bbId] = bb;return bb;}}public BlackBoard GetBlackBoard(int bbId){id2BB.TryGetValue(bbId, out var bb);return bb;}public DataConfig GetDataConfig(int id){return dataConfig[id];}public void RemoveBlackBoard(BlackBoard bb){id2BB.Remove(bb.bbId);}public void Clear(){foreach(var bb in id2BB.Values){bb.Clear();}id2BB.Clear();dataConfig.Clear();}}public class BlackBoard{public int bbId;public Dictionary<int, IBBDataResult> id2Result = new Dictionary<int, IBBDataResult>();public Dictionary<int,BBDataRequest> id2Request;//這里簡單根據Id做劃分,可以做更復雜的分類,以便于收集數據做數據分析或Debugprivate List<int> waitRemoveList = new List<int>();private List<BBDataRequest> reqHistory = new List<BBDataRequest>();//可以收集數據做分析public void Tick(float deltaTime)//Tick檢查去掉無效數據{waitRemoveList.Clear();foreach (var item in id2Result){item.Value.Tick(deltaTime);if(!item.Value.Valid()){waitRemoveList.Add(item.Key);}}foreach (var item in waitRemoveList){RemoveData(item);}}public Result GetData<Params,Result>(int btId,int nodeId,int dataId, Params reqparams,out bool valid){var config = BlackBoardManager.Instance.GetDataConfig(dataId);//根據數據Id獲取數據配置var request = GetBBDataRequest<Params>(btId,nodeId,dataId,reqparams,config.multi && config.cache, out int index);//根據參數獲取請求,分為Single請求和Multi請求var result = GetBBDataResult<Result>(dataId, config, request);//獲取請求對應的結果//一個數據Id只有一個對應的請求和結果valid = result.Valid();if(valid){if(config.praseType){BBDataMethod.DispatchMethoId<Params,Result>(result.config.getMethodId, bbId, dataId, index, true);//自動解析傳入的參數和結果的類型,自動化生成代碼,適用于簡單的值類型}else{BBDataMethod.DispatchMethoId(result.config.getMethodId, bbId, dataId, index);//自定義處理數據類型}return ((BBDataResult<Result>)result).GetCurResult(index);//同一個數據Id,在獲取時會傳入不同的參數,在請求中,給參數生成Index,根據Index獲取其對應的結果} return default(Result);}public Result GetData<Result>(int btId, int nodeId, int dataId, object reqparams, out bool valid){var config = BlackBoardManager.Instance.GetDataConfig(dataId);var request = GetBBDataRequest(btId, nodeId, dataId, reqparams, config.multi && config.cache, out int index);var result = GetBBDataResult<Result>(dataId, config, request);valid = result.Valid();if (valid){if (config.praseType){BBDataMethod.DispatchMethoId<object, Result>(result.config.getMethodId, bbId, dataId, index, true);}else{BBDataMethod.DispatchMethoId(result.config.getMethodId, bbId, dataId, index);}return ((BBDataResult<Result>)result).GetCurResult(index);}return default(Result);}public void SetData<Params, Value>(int btId, int nodeId, int dataId,Value value, Params reqparams = default){var config = BlackBoardManager.Instance.GetDataConfig(dataId);//根據數據Id獲取數據配置var request = GetBBDataRequest<Params>(btId, nodeId, dataId, reqparams, config.multi && config.cache, out int index);//根據參數獲取請求,分為Single請求和Multi請求var result = GetBBDataResult<Value>(dataId, config, request);//獲取請求對應的結果if (!((BBDataResult<Value>)result).GetCurResult(index).Equals(value))//判斷設置的值是否和當前的結果值相當,如果相等就不用再設置了{if (config.praseType){BBDataMethod.DispatchMethoId<Params, Result>(result.config.setMethodId, bbId, dataId, index, false);}else{BBDataMethod.DispatchMethoId(result.config.setMethodId, bbId, dataId, index);}} }public void SetData<Value>(int btId, int nodeId, int dataId, Value value,object reqparams = null){var config = BlackBoardManager.Instance.GetDataConfig(dataId);var request = GetBBDataRequest(btId, nodeId, dataId, reqparams, config.multi && config.cache, out int index);var result = GetBBDataResult(dataId, config, request);if (!((BBDataResult<Value>)result).GetCurResult(index).Equals(value)){if (config.praseType){BBDataMethod.DispatchMethoId<object, Result>(result.config.setMethodId, bbId, dataId, index, false);}else{BBDataMethod.DispatchMethoId(result.config.setMethodId, bbId, dataId, index);}}}public bool RemoveData(int dataId){int count = 0;if(id2Result.TryGetValue(dataId,out var result)){result.Release();id2Result.Remove(dataId);count++;}if(id2Request.TryGetValue(dataId,out var request)){request.Release();id2Request.Remove(dataId);count++;}return count == 2;}public BBDataRequest GetDataRequest(int dataId){id2Request.TryGetValue(dataId, out var result);return result;}public IBBDataResult GetDataResult(int dataId){id2Result.TryGetValue(dataId, out var result);return result;}public void Clear(){id2Request.Clear();id2Result.Clear();waitRemoveList.Clear();//SaveHistoryreqHistory.Clear();}private BBDataRequest GetBBDataRequest<T>(int btId, int nodeId, int dataId,T data,bool multi,out int index){if(!id2Request.TryGetValue(dataId,out var request)){request = multi ? BBDataRequestMulti<T>.GetBBDataRequest() : BBDataRequestSingle<T>.GetBBDataRequest();request.btId = btId;request.nodeId = nodeId;request.dataId = dataId;//reqHistory.Add(request);}var res = request as BBDataRequest<T>;index = res.TryAddParams(data);//將獲取數據傳入的參數封裝在 BBDataRequest中return res;}private BBDataRequest GetBBDataRequest(int btId, int nodeId, int dataId, object data,bool multi,out int index){if (!id2Request.TryGetValue(dataId, out var request)){request = multi ? BBDataRequestMulti<object>.GetBBDataRequest() : BBDataRequestSingle<object>.GetBBDataRequest();request.btId = btId;request.nodeId = nodeId;request.dataId = dataId;//reqHistory.Add(request);}var res = request as BBDataRequest<object>;index = res.TryAddObject(data);return res;}private IBBDataResult GetBBDataResult<T>(int dataId,DataConfig config,BBDataRequest request){if (!id2Result.TryGetValue(dataId, out var result)){BBDataResult<T> res = (config.multi && config.cache) ? BBDataResultMulti<T>.GetBBDataResult() : BBDataResultSingle<T>.GetBBDataResult();if (!config.cache) res.SetCurResult(default(T), 0);result = res;result.dataId = dataId;result.config = config;}result.request = request;return result;}private IBBDataResult GetBBDataResult(int dataId, DataConfig config, BBDataRequest request){if (!id2Result.TryGetValue(dataId, out var result)){BBDataResult<object> res = (config.multi && config.cache) ? BBDataResultMulti<object>.GetBBDataResult() : BBDataResultSingle<object>.GetBBDataResult();if (!config.cache) res.SetCurResult(null, 0);result = res;result.dataId = dataId;result.config = config;}result.request = request;return result;}}
數據的GetSet方法實
public static class BBDataDefinition{//這里通過配置自動生成public const int Def_獲取血量 = 11223344;public const int Def_設置血量 = 11223345;public const int Def_獲取資源數量 = 121212123;public const int Def_設置資源數量 = 121212124;}public partial class BBDataMethod{//這里通過配置自動生成private static Dictionary<(Type, Type), Action<BBDataRequest,IBBDataResult,int,int,bool>> TypeToPraseAction = new Dictionary<(Type, Type), Action<BBDataRequest, IBBDataResult, int, int,bool>>(){[(typeof(void),typeof(int))] = PraseVoidAndInt,[(typeof(int), typeof(int))] = PraseIntAndInt,[(typeof(int), typeof(void))] = PraseIntAndVoid,};private static Dictionary<int, Func<int>> GetIntValue = new Dictionary<int, Func<int>>(){[BBDataDefinition.Def_獲取資源數量] = GetResCount,};private static Dictionary<int, Action<int>> SetIntValue = new Dictionary<int, Action<int>>(){[BBDataDefinition.Def_設置資源數量] = SetResCount,};private static Dictionary<int, Func<int,int>> GetIntValueByInt = new Dictionary<int, Func<int,int>>(){};private static Dictionary<int, Action<int, int>> SetIntValueByInt = new Dictionary<int, Action<int, int>>(){};public static void DispatchMethoId<Params,Result>(int methodId,int bbId,int dataId,int index,bool get){var bb = BlackBoardManager.Instance.GetBlackBoard(bbId);var res = bb.GetDataResult(dataId);if (get && res != null && res.Getted(Time.frameCount, index)){return;}var req = bb.GetDataRequest(dataId);if (req != null && res != null){var typeReq = typeof(Params);var typeRes = typeof(Result);TypeToPraseAction.TryGetValue((typeReq, typeRes), out var action);if (action != null){action(req, res, methodId, index, get);}}}public static void DispatchMethoId(int methodId, int bbId, int dataId, int index){switch (methodId){case BBDataDefinition.Def_獲取血量: GetRoleHp(bbId, dataId, index); break;case BBDataDefinition.Def_設置血量: SetRoleHp(bbId, dataId, index); break;}}private static void PraseVoidAndInt(BBDataRequest req, IBBDataResult res, int methodId, int index,bool get){if(get){int intValue = GetIntValue[methodId].Invoke();var intResult = res as BBDataResult<int>;intResult.SetGetResult(intValue, index);}}private static void PraseIntAndVoid(BBDataRequest req, IBBDataResult res, int methodId, int index, bool get){if (!get){var intResult = res as BBDataResult<int>;int intValue = intResult.GetCurResult(index);SetIntValue[methodId].Invoke(intValue);}}private static void PraseIntAndInt(BBDataRequest req, IBBDataResult res, int methodId, int index, bool get){if(get){var intReq = req as BBDataRequest<int>;int intParams = intReq.GetParmas(index);int intValue = GetIntValueByInt[methodId].Invoke(intParams);var intResult = res as BBDataResult<int>;intResult.SetGetResult(intValue, index);}else{var intReq = req as BBDataRequest<int>;int intParams = intReq.GetParmas(index);var intResult = res as BBDataResult<int>;int intValue = intResult.GetCurResult(index);SetIntValueByInt[methodId].Invoke(intParams, intValue);}}}public partial class BBDataMethod{public static void GetRoleHp(int bbId, int dataId,int index){var bb = BlackBoardManager.Instance.GetBlackBoard(bbId);//獲取數據所在的BBvar res = bb.GetDataResult(dataId);//獲取數據對應的結果if(res != null && res.Getted(Time.frameCount,index))//判斷當前幀該數據是否已經獲取過{return;}var req = bb.GetDataRequest(dataId);//獲取數據對應的請求 if(req != null ){ var intReq = req as BBDataRequest<int>;int roleId = intReq.GetParmas(index);//獲取請求的參數int hp = 100;//通過角色Id獲取角色屬性,屬性系統固定時,這些類似的獲取值的代碼都可以通過自動化配置生成 var intResult = res as BBDataResult<int>;intResult.SetGetResult(hp,index);//設置獲取的結果}}public static void SetRoleHp(int bbId, int dataId,int index){var bb = BlackBoardManager.Instance.GetBlackBoard(bbId);var res = bb.GetDataResult(dataId);var req = bb.GetDataRequest(dataId);if (req != null && res != null){var intReq = req as BBDataRequest<int>;int roleId = intReq.GetParmas(index);var intResult = res as BBDataResult<int>;int hp = intResult.GetCurResult(index); //調用接口設置角色血量}}public static int GetResCount() { return 100; }public static void SetResCount(int value) { }}