技能系统详解(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 skillEntities = new List();

        public int roleId;
        public int playerId;

        private int curSkillIndex;//少量的运行时数据,可以不用专门的Data

        public 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 常用高频属性拿出来,避免调用栈过深
        /// 
        /// 技能的ID
        /// 
        public int skillId;
        /// 
        /// 哪个角色的技能
        /// 
        public int playerId;
        /// 
        /// 技能状态
        /// 
        public SKillState state;
        /// 
        /// 技能类型
        /// 
        public SkillType skillType;
        #endregion

        #region 技能数据
        public SkillLocalData localData;
        public SkillConfigData configData;
        public SKillRemoteData remoteData;
        #endregion

        #region 逻辑组件
        public List logicComs = new List();
        #endregion

        public 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;
        }

        #endregion

        public 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 skillLogics = new List();

        public void Dispose()
        {

        }
    }

    public struct ComponentID
    {
        public SkillLogicComType type;
        public int configId;
    }

    public enum SkillTimeType
    {
        Instantaneous,
        TimeLimited,
        LogicTrigger,
        ContinueTime,
    }

    public class SkillConfigDataSystem : DataSystem
    {
        public override string dataPath => "Assets/ConfigData/SkillConfigData.asset";

        protected override bool ParseData(SkillConfigData data)
        {
            return true;
        }
    }

    public class SKillLocalDataSystem:DataSystem {}

    public class SKillRemoteDataSystem: DataSystem {}

    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 skillIds = new List();
        public SkillType skillType;

        public string abilitySimpleDes;
        public string abilityDetailDes;
        public string abilityVideoDes;
        public string abilityAuidoDes;

        public void Dispose(){}
    }

    public class AbilityConfigDataSystem:DataSystem 
    {
        public override string dataPath => "Assets/ConfigData/AbilityConfigData.asset";
    }

    public class Singleton 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: Singleton where T : class, new() where Data : IData,new()
    {
        protected Dictionary data = new Dictionary();
        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 GetOrCreateLogic(long logicId, bool create);
    }
    public class ComponentSystem:Singleton,ISystem where T:class,new() where Logic:SkillLogicComponent,new()
    {
        private Dictionary logics = new Dictionary();

        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
    {

    }
    public class LogicComSystemMgr:Singleton
    {
        public Dictionary> typeMap = new Dictionary> () 
        {
            
        };

        public bool init;

        public void Init()
        {
            if (init) return;
            typeMap[SkillLogicComType.Move] = (ISystem)MoveComponentSystem.Instance;

            init = true;
        }

        public ISystem GetLogicCom(SkillLogicComType type)
        {
            if(!init)
            {
                Init();
            }

            typeMap.TryGetValue(type, out var logicSystem);
            return logicSystem;
        }

    }

你可能感兴趣的:(游戏设计与实现,技能系统)