当前位置: 首页 > ops >正文

技能系统详解(1)——技能

【前言】

技能系统是建立在其他基础系统之上的,这些基础系统包括属性系统、运动和动画系统、打击系统、特效系统、网络同步系统、资源管理系统等。

如果这些系统的实现提供了丰富的接口支持或者便捷扩展支持,那么技能系统的实现会简单很多。

然而,通常情况下是上层的技能系统会对下层的系统提出新的需求,已有的系统不能直接支持,需要对这些已有系统做改造。

技能系统本身又包括数值、技能、状态、buff、伤害、特效、运动等多个方面。先看一看技能。

【生命周期】 

系统内的核心对象必然存在生命周期的相关问题,这里技能系统的核心对象是技能,生命周期如下:

  • 第一步是Init:技能初始化
  • 第二步是Check:即是否满足技能释放条件
  • 第三步是Enter:即开始释放技能
  • 第四步是Excute/Break:即技能释放和打断
  • 第五步是Exit:即技能释放结束
  • 第六步是Destroy 

从业务逻辑考虑生命周期时,是没有Init和Destroy的。Init和Destroy是从程序实现上附加在业务逻辑上的数据和资源相关的步骤

不同的生命周期对应不同的阶段状态SkillState,进入不同状态时需要通过消息的方式发布出去,以便其他地方监听

技能数据SkillData

数据可以分为静态的配置数据SkillConfigData、动态的运行数据

其中运行数据又可以分为本地运行数据SKillLocalData、远程运行数据SkillRemoteData,也即有些运行数据需要网络同步

更进一步的,静态配置数据可以根据业务或功能做出来不同的划分。但这里只对数据做最上层的三类区分。

通常,这三类数据Data通过技能的唯一标识SkillID(也即主键)做关联,也,由不同的DataSystem做各自的管理,System和Data应该有一套生成规范

技能的生命周期逻辑和数据构成技能实体(SkillEntity)的主要内容

(上文中的生命周期和数据也可以是任何其他概念的)

技能组合

当谈论到组合时,需要知道系统中核心对象的最小粒度。

技能是技能系统中设计时的对象,不是实现时的对象。

如果认为技能是更基础的对象,不同技能就可以组合出能力Ability(叫天赋、特性什么的都OK)组合而成。而能力对玩家来说就是技能。

也难辞,一个能力,是由多段技能组成。

技能表现

释放技能时所看到的一切都可以看作是技能的表现:包括角色动作、角色移动、角色变身、技能特效、召唤物、环境变化、伤害飘字等

在技能释放过程中会有什么表现是多种多样的,取决于具体的需求,事先无法预料

因此,不能在Exceute中做固定的逻辑,其只是开启表现逻辑的入口,需要将具体的逻辑拆分开来

对于这种情况,通常会将不同表现的逻辑抽象为一个个组件,需要调整表现时,调整一系列组件即可,因此需要抽象出SkillComponent基类

技能类型

从持续时间划分:

  • 瞬发类:指瞬间释放的技能,这类技能的表现通常都会随着技能释放过程全部触发,注意技能释放完成不等于技能表现完成,有些表现不跟随技能生命周期,例如王昭君大招
  • 限时类:指需要在特定时间内释放的技能,释放的是瞬发类技能,通常只能在该段时间内释放一次
  • 触发类:指需要在特定条件下才释放的技能,释放的是瞬发类技能
  • 状态类:指会持续一段时间的技能,这类技能有部分表现是跟随生命周期,例如拖尾特效、隐身特效等,有部分表现是特殊情况下的触发。状态类能力更多的像是一个开关,其可以存在多个限时类或瞬发类或触发类技能,通常该技能打开时会有瞬发的表现

能力类型

从释放次数划分:

  • 单次类:指能力只能释放一次,释放后就进入CD阶段,通常就是瞬发类技能
  • 多段类:指能力可以做多次技能释放,通常是瞬发类搭配限时类技能

【技能具体数据(以王者为例)】

能力数据

  • 能力Id
  • 能力输入类型:普通技能1、普通技能2、普通技能3、大招、通用技能1、通用技能2
  • 能力图标
  • 能力的技能组合
  • 能力名称
  • 能力简要描述
  • 能力详细描述
  • 能力视频描述
  • 能力语音描述
  • 能力成长数据(可以展开成更为具体的)

技能数据

  • 技能Id
  • 技能图标
  • 技能其他各类图标
  • 技能功能类型:位移、强化、净化、回血、控制、伤害、免控等
  • 技能CD
  • 技能最小CD
  • 技能持续时间:0表示释放就结束的技能
  • 技能最大长按时间:0表示该技能不能长按
  • 技能耗血值
  • 技能耗蓝值
  • 技能耗体力值
  • 技能优先级
  • 联动技能:例如大招放了后其他技能招式会变化或者基础数据会变化
  • 其他各种和具体游戏相关的数据
  • 技能逻辑组件:包括逻辑组件类型和配置数据ID

逻辑组件

每个逻辑组件都有各自的Data和System,具体数据是什么,需要具体的逻辑是什么,同时各类System可以用统一的映射和管理,即有个LogicSystemMgr

注意,这里的逻辑组件只是相对于技能组件是逻辑,其本身也有自己的逻辑和数据

运行时数据

此时只考虑本地数据,远程数据不考虑

  • CD
  • 剩余CD
  • 当前持续时间
  • 当前按住时间
  • 等等

运行时数据与配置数据的区别在于:每个配置数据都对应有个当前数据,如果配置数据的默认值在机制上允许动态改变,那么运行时数据中需要存在默认值。

例如:技能会有个初始的默认CD,但通常技能CD可以改变,那么配置的CD数据有两个对应的运行时数据

【具体的生命周期】

初始化Init

初始化时有两个主要工作

  1. 给常用属性赋值,这些常用属性通常是在其他地方传递过来的,因此Init方法需要增加一些参数
  2. 数据组件初始化,需要给数据初始化传递必要的参数,因此其Init方法需要增加一些参数,与常用值的Init方法参数的区别是,前者是固定的,而数据同类不同,其初始化参数不同。

面对这样的情况,一种简单的方式是每种数据各自写初始化方法;另一种方式是将参数合并做上下文传递各取所需,我们先采用简单的方式

销毁Destroy

对于常用属性,其值设回初始值即可,因为每次都会重新初始化,有些值可以不必再设置

对于配置数据置空,对于运行时数据要销毁

对于逻辑要停止

检查Check
检查用于判断是否可以到Enter,具体有什么检查与具体的概念相关,对于技能而言,其意思是技能当前是否可以使用,包括以下检查:

通用检查:即CD、蓝量、血量、体力是否足够,考虑到不同技能检查的差异性,这些都要分开允许重写

特殊检查:即每个技能有各自可以释放的前提条件

进入Enter/执行Excute/退出Exit

技能释放时有各种各样的表现,所有这些表现都是在技能释放期间,也即Excute阶段触发的

注意,Execute阶段只管各种表现什么时候触发,不管表现什么时候出现或者结束,也即技能释放完成和技能的表现完成是两个不同的概念

例如,有的技能特效需要延迟一段时间才会出现,但该技能特效实际已经触发了;有的技能可以冰冻敌人,技能已经释放完成,但敌人可能还会被冻住5s不能移动

通常,会假设有个技能时间轴,可以在该时间轴上配置不同表现的触发时间以相关数据。

众多表现中最为特殊的表现是动作,因为玩家操作角色相当于控制了角色的动作表现,继而影响技能的动作表现,而特效等其他表现不受玩家控制。

此时,技能表现如何触发有两种方式:一是技能驱动动作;二是动作驱动技能

简单来说,对于技能的动作,可以分为前摇施法后摇三个阶段,共三个动作(可根据实际需要增加更多的动作阶段)

技能驱动动作时,按照技能时间轴依次播三个不同的动作,一般在Enter时就需要播放动画,在技能时间轴某一时刻触发其他特效、音效等表现。例如攻击动画0.5s时有个剑气特效,就将剑气的触发改为0.5s

动作驱动技能时,动作系统控制播放三个不同的动作,并按照动作时间轴,在施法动作的某些时间点触发特效、音效等表现。

无论在什么游戏中,角色动作总是远多余角色技能的。两者方式的区别如下:

动作驱动:

  • 可以打出各种丰富的技能表现
  • 具有更好的操作感、打击感、爽感
  • 输入延迟要求严格,通常需要有输入缓冲
  • 网络同步较难
  • 操作上线高,APM(每分钟操作数)在120-180
  • 技能数值基本固定,获胜看玩家操作,会追求炫酷,皮肤/外观付费较高

技能驱动:

  • 可以提供确定性反馈,例如在技能释放前摇阶段做可视化提示
  • 动作与技能容易割裂不同步,技能的动作表现和其他特效表现容易出现不同步的情况
  • 容易存在技能冲突,通常需要有技能优先级队列
  • 操作有上限,技能释放需要有公共冷却机制,APM为60-80
  • 技能的动作表现有限,获胜看技能数值,数值加成道具付费较多
  • 网络同步简单些

通常动作类游戏用动作驱动的方式,例如鬼泣5、只狼

RPG和MOBA用技能驱动的方式,例如王者荣耀、英雄联盟、暗黑破坏神。

严格来说,动作游戏、RPG、MOBA中的技能并不是一回事,动作游戏中的技能更像是一种特殊的招式

这里我们以动作驱动为主

打断Break

动作驱动中,技能是否可以打断主要看动作是否可以被打断,动作被打断往往表示技能释放结束,进入Exit

技能驱动中,技能是否可以被打断看技能优先级和克制关系,技能被打断不一定释放结束

【触发逻辑】

主要看生命周期各方法在何时调用

初始化和销毁

角色在装备技能或动态切换技能时会调用初始化,卸载技能时调用销毁,通常伴随角色一块销毁,角色技能RoleSkillComponent可以看作是角色实体CombatEntity的一个组件

检查

检查需要在释放技能时才开始,技能释放通常是由玩家主动触发的,也即其调用最终来自与玩家交互的控件,玩家输入和技能是两个模块,为了隔离好,需要有个代理模式SkillInputAgent

交互输入基本有三种类型Down\Press\Up,不同的按钮区分出不同的输入类型,因为是两个不同的系统,需要有一个类型转换

进入/退出/执行/打断

在动作驱动中,通过动作系统调用,具体而言是在施法动作的某个时间点触发调用

对于状态类技能,其退出往往是自己调用

技能的某些表现可能需要在特定时机触发,做特殊调用

【代码实现】

    public class CombatEntity{public int playerId;public GameObject roleObject;public Transform transform;public RoleSkillComponent roleSkillComponent;}public class RoleSkillComponent:Component{public AbilityConfigData abilityConfig;public List<SkillEntity> skillEntities = new List<SkillEntity>();public int roleId;public int playerId;private int curSkillIndex;//少量的运行时数据,可以不用专门的Datapublic void Init(int roleId,int playerId){this.roleId = roleId;this.playerId = playerId;abilityConfig = AbilityConfigDataSystem.Instance.GetOrCreateData(roleId);if(abilityConfig != null ){foreach (var item in abilityConfig.skillIds){SkillEntity skillEntity = new SkillEntity();skillEntity.Init(item, playerId, abilityConfig.skillType);skillEntities.Add(skillEntity);}}}public void SkillInput(){if(curSkillIndex >= skillEntities.Count) { return; }var skillEntity = skillEntities[curSkillIndex];switch(skillEntity.state){case SkillEntity.SKillState.Init:case SkillEntity.SKillState.Check:if (skillEntity.Check())//技能初始化完成,检查是否可以释放{skillEntity.EnterSkill();}break;case SkillEntity.SKillState.Exit://当前技能完成就开始下一个技能curSkillIndex++;SkillInput();break;case SkillEntity.SKillState.Enter:case SkillEntity.SKillState.Execute:skillEntity.ExecuteSkill();break;}        }public void Destroy(){AbilityConfigDataSystem.Instance.Destroy();abilityConfig = null;foreach (var item in skillEntities){item.Destroy();}skillEntities.Clear();}}public class SkillEntity{#region 常用高频属性拿出来,避免调用栈过深/// <summary>/// 技能的ID/// </summary>public int skillId;/// <summary>/// 哪个角色的技能/// </summary>public int playerId;/// <summary>/// 技能状态/// </summary>public SKillState state;/// <summary>/// 技能类型/// </summary>public SkillType skillType;#endregion#region 技能数据public SkillLocalData localData;public SkillConfigData configData;public SKillRemoteData remoteData;#endregion#region 逻辑组件public List<SkillLogicComponent> logicComs = new List<SkillLogicComponent>();#endregionpublic enum SKillState{None,Destroy,Init,Check,Enter,Execute,Break,Exit,}public void Init(int skillId,int playerId,SkillType skillType){if (state >= SKillState.Init)return;this.playerId = playerId;this.skillId = skillId;this.skillType = skillType;OnInit();SendStateEvent(SKillState.Init);}#region 检查逻辑public bool Check(){SendStateEvent(SKillState.Check);bool res = OnCheck();            return res;}protected bool OnCheck(){return CommonCheck() && SpecialCheck();}public virtual bool CommonCheck(){return CheckCD() && CheckHp() && CheckMp() && CheckSp();}public virtual bool CheckCD(){return true;}public virtual bool CheckHp(){return true;}public virtual bool CheckMp(){return true;}public virtual bool CheckSp(){return true;}public virtual bool SpecialCheck(){return true;}#endregionpublic void EnterSkill(){SendStateEvent(SKillState.Enter);OnEnterSkill();           }public void ExitSkill(){SendStateEvent(SKillState.Exit);OnExitSkill();           }public void ExecuteSkill(){SendStateEvent(SKillState.Execute);OnEexcuteSKill();       }public void BreakSkill(){SendStateEvent(SKillState.Break);OnBreakSkill();}public void Destroy(){if (state <= SKillState.Destroy)return;SendStateEvent(SKillState.Destroy);OnDestroy();}protected virtual void OnInit(){configData = SkillConfigDataSystem.Instance.GetOrCreateData(skillId);localData = SKillLocalDataSystem.Instance.GetOrCreateData(playerId,true);localData.Init(playerId, configData);remoteData = SKillRemoteDataSystem.Instance.GetOrCreateData(playerId,true);if(configData != null){if(configData.skillLogics != null){foreach (var item in configData.skillLogics){var logicSystem = LogicComSystemMgr.Instance.GetLogicCom(item.type);var logic = logicSystem.GetOrCreateLogic(0, true);logicComs.Add(logic);logic.Init(item.type, item.configId);}}}}protected virtual void OnEnterSkill(){foreach (var item in logicComs){item.Start();//告知各技能表现逻辑可以开始释放}localData.curStartTime = Time.realtimeSinceStartup;}protected virtual void OnExitSkill(){foreach (var item in logicComs){item.End();//告知技能表现逻辑该技能释放完毕}}protected virtual void OnEexcuteSKill(){var time = Time.realtimeSinceStartup - localData.curStartTime;foreach (var item in logicComs){if(!item.executed)item.Excute(time);//告知技能释放完毕}}protected virtual void OnBreakSkill(){foreach (var item in logicComs){item.Break();}}protected virtual void OnDestroy(){skillId = 0;playerId = 0;configData?.Dispose();configData = null;localData?.Dispose();localData = null;remoteData?.Dispose();remoteData = null;foreach (var item in logicComs){Component.Destroy(item);}logicComs.Clear();}private void SendStateEvent(SKillState state){this.state = state;//调用消息系统发消息}}public class SkillLocalData : IData{public int playerId;public float cd;public float curCd;public float curStartTime;public float curPressTime;public void Init(int playerId,SkillConfigData configData){this.playerId = playerId;cd = configData.cd;curCd = 0;curStartTime = 0;curPressTime = 0;}public void Dispose(){SKillLocalDataSystem.Instance.ClearData(playerId);}}public class SKillRemoteData : IData{public int playerId;public void Dispose(){SKillRemoteDataSystem.Instance.ClearData(playerId);}}public class SkillConfigData : IData{public int skillId;public SkillPlayType skillPalyType;public SkillTimeType skillTimeType;public string skillIcon;public string skillOtherIcon1;public string skillOtherIcon2;public string skillOtherIcon3;public float cd;public float minCd;public float continueDuration;public float pressDuration;public int hpCost;public int mpCost;public int spCost;public int priority;public int relatedSkillId;public List<ComponentID> skillLogics = new List<ComponentID>();public void Dispose(){}}public struct ComponentID{public SkillLogicComType type;public int configId;}public enum SkillTimeType{Instantaneous,TimeLimited,LogicTrigger,ContinueTime,}public class SkillConfigDataSystem : DataSystem<SkillConfigDataSystem,SkillConfigData>{public override string dataPath => "Assets/ConfigData/SkillConfigData.asset";protected override bool ParseData(SkillConfigData data){return true;}}public class SKillLocalDataSystem:DataSystem<SKillLocalDataSystem,SkillLocalData> {}public class SKillRemoteDataSystem: DataSystem<SKillRemoteDataSystem, SKillRemoteData> {}public enum SkillType{NormalSkill1,NormalSkill2,NormalSkill3,SpecialSkill,CommonSkill1,CommonSkill2,}public enum SkillPlayType{Healing,Control,Shield,Mobility,}public enum SkillLogicComType{Move,Effect,}public class AbilityConfigData:IData{public int abilityId;public string abilityName;public string abilityIcon;public List<int> skillIds = new List<int>();public SkillType skillType;public string abilitySimpleDes;public string abilityDetailDes;public string abilityVideoDes;public string abilityAuidoDes;public void Dispose(){}}public class AbilityConfigDataSystem:DataSystem<AbilityConfigDataSystem, AbilityConfigData> {public override string dataPath => "Assets/ConfigData/AbilityConfigData.asset";}public class Singleton<T> where T : class,new(){protected Singleton() { }private static T _instance;public static T Instance => _instance??new T();public static void Disopse(){_instance = null;}}public interface IData{void Dispose();}public class DataSystem<T,Data>: Singleton<T> where T : class, new() where Data : IData,new(){protected Dictionary<int,Data> data = new Dictionary<int,Data>();public bool init;public virtual string dataPath{get{return null;}private set { }}public virtual void Init(){if(init) return;if(!string.IsNullOrEmpty(dataPath)){Data data = LoadData();if(ParseData(data)){init = true;}}}public virtual Data GetOrCreateData(int dataId,bool create = false){if(!init){Init();}if(!data.TryGetValue(dataId,out var dataValue)){if(create){dataValue = new Data();data.Add(dataId, dataValue);}else{Debug.LogError($"{GetType().Name} don not has data for :{dataId}");}}return dataValue;}public virtual void ClearData(int dataId){if (!init) return;var data = GetOrCreateData(dataId);if(data != null){data.Dispose();this.data.Remove(dataId);}}public virtual void Destroy(){if (!init) return;foreach (var item in data.Values){item.Dispose();}data.Clear();init = false;}protected virtual bool ParseData(Data data){//不同类型的数据做各自的解析return true;}protected virtual Data LoadData(){//调用资源管理系统的接口加载数据return default(Data);}}public class Component{private bool enable = false;public bool Enable{set{if (enable == value) return;enable = value;if (enable) OnEnable();else OnDisable();}get{return enable;}}public bool IsDisposed { get; set; }public long instanceId;public static long StartInstance = 0;public virtual void OnEnable() { if(StartInstance == 0){StartInstance = DateTime.UtcNow.Ticks;}StartInstance++;instanceId = StartInstance;}public virtual void OnDisable(){}public virtual void OnDestroy(){}private void Dispose(){Enable = false;IsDisposed = true;}public static void Destroy(Component com){try{com.OnDestroy();}catch (Exception e){Debug.LogError(e);}com.Dispose();}}public class SkillLogicComponent:Component{public SkillLogicComType type;public int dataId;public float startTime;//开始触发的时间public bool executed;public void Init(SkillLogicComType type, int dataId){this.type = type;this.dataId = dataId;           OnInit();}public virtual void OnInit(){//根据逻辑type和dataId从其对应的DataSystem中获取数据}public void Start(){}public void Excute(float time){if (time < startTime) return;OnExcute();executed = true;}public void End(){}public void Break(){}protected virtual void OnExcute(){}public override void OnDestroy(){}}  public interface ISystem<T>{T GetOrCreateLogic(long logicId, bool create);}public class ComponentSystem<T,Logic>:Singleton<T>,ISystem<Logic> where T:class,new() where Logic:SkillLogicComponent,new(){private Dictionary<long, Logic> logics = new Dictionary<long, Logic>();public Logic GetOrCreateLogic(long logicId,bool create = false){if(!logics.TryGetValue(logicId, out Logic logic)){if(create){logic = new Logic();logic.Enable = true;}else{Debug.LogError($"{GetType().Name} don not has logic for :{logicId}");}}return logic;}}public class MoveComponent : SkillLogicComponent{public override void OnInit(){base.OnInit();}}public class MoveComponentSystem: ComponentSystem<MoveComponentSystem,MoveComponent>{}public class LogicComSystemMgr:Singleton<LogicComSystemMgr>{public Dictionary<SkillLogicComType, ISystem<SkillLogicComponent>> typeMap = new Dictionary<SkillLogicComType, ISystem<SkillLogicComponent>> () {};public bool init;public void Init(){if (init) return;typeMap[SkillLogicComType.Move] = (ISystem<SkillLogicComponent>)MoveComponentSystem.Instance;init = true;}public ISystem<SkillLogicComponent> GetLogicCom(SkillLogicComType type){if(!init){Init();}typeMap.TryGetValue(type, out var logicSystem);return logicSystem;}}

http://www.xdnf.cn/news/14473.html

相关文章:

  • mysql 学习
  • 45-Oracle 索引的新建与重建
  • 6-16阿里前端面试记录
  • RAG 架构地基工程-Retrieval 模块的系统设计分享
  • 学习STC51单片机41(芯片为STC89C52RCRC)智能小车8(测速显示到OLED显示屏)
  • git最常用命令
  • RISC-V向量扩展与GPU协处理:开源加速器设计新范式——对比NVDLA与香山架构的指令集融合方案
  • 汽车 CDC威胁分析与风险评估
  • HTTP 请求中的 `Content-Type` 类型详解及前后端示例(Vue + Spring Boot)
  • 腾讯云国际站缩容:策略、考量与实践
  • Vue-7-前端框架Vue之应用基础从Vue2语法到Vue3语法的演变
  • C/C++中的位段(Bit-field)是什么?
  • 单片机 - STM32读取GPIO某一位时为什么不能直接与1判断为高电平?
  • 【开源工具】Windows屏幕控制大师:息屏+亮度调节+快捷键一体化解决方案
  • Day03_数据结构(顺序结构单向链表单向循环链表双向链表双向循环链表)
  • 【一天一个知识点】RAG(Retrieval-Augmented Generation,检索增强生成)构建的第一步
  • ARIMA 模型
  • Linux运维新人自用笔记(部署 ​​LAMP:Linux + Apache + MySQL + PHP、部署discuz论坛)
  • 内存泄漏到底是个什么东西?如何避免内存泄漏
  • 楞伽经怎么读
  • 23种设计模式图解
  • ragflow中的pyicu安装与测试
  • 基于YOLOv8+Deepface的人脸检测与识别系统
  • WSL备份与还原
  • 车载网关框架 --- CAN/CANFD网段路由到Ethernet网段时间
  • sparseDrive(2):环境搭建及效果演示
  • C++11函数封装器 std::function
  • 卫星通信链路预算之一:信噪比分配
  • JavaSE: 数组详解
  • JSONP 跨域请求原理解析与实践