【动作节点数据共享】
行为树中需要的参数可以来自游戏中的各个模块,如果仅需从多个模块获取少量参数,那么可以直接在代码中调用其他模块的单例继而层层调用获取数据。
如果获取的参数量很大,从架构上看,我们需要通过加一个中间者去管理各个模块的参数获取调用,行为树从中间者获取数据即可。
换一种说法就是要有共享数据的地方,通常会采用黑板模式。
综合来说,存在以下情况:
- 多个不同的动作节点或条件节点需要获取或设置来自不同模块的属性
- 多个不同的动作节点或条件节点会获取或设置相同模块的同一属性
- 不同动作节点之间有通信,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) { }}