技能系统是建立在其他基础系统之上的,这些基础系统包括属性系统、运动和动画系统、打击系统、特效系统、网络同步系统、资源管理系统等。
如果这些系统的实现提供了丰富的接口支持或者便捷扩展支持,那么技能系统的实现会简单很多。
然而,通常情况下是上层的技能系统会对下层的系统提出新的需求,已有的系统不能直接支持,需要对这些已有系统做改造。
技能系统本身又包括数值、技能、状态、buff、伤害、特效、运动等多个方面。先看一看技能。
系统内的核心对象必然存在生命周期的相关问题,这里技能系统的核心对象是技能,生命周期如下:
从业务逻辑考虑生命周期时,是没有Init和Destroy的。Init和Destroy是从程序实现上附加在业务逻辑上的数据和资源相关的步骤
不同的生命周期对应不同的阶段状态SkillState,进入不同状态时需要通过消息的方式发布出去,以便其他地方监听
技能数据SkillData
数据可以分为静态的配置数据SkillConfigData、动态的运行数据
其中运行数据又可以分为本地运行数据SKillLocalData、远程运行数据SkillRemoteData,也即有些运行数据需要网络同步
更进一步的,静态配置数据可以根据业务或功能做出来不同的划分。但这里只对数据做最上层的三类区分。
通常,这三类数据Data通过技能的唯一标识SkillID(也即主键)做关联,也,由不同的DataSystem做各自的管理,System和Data应该有一套生成规范
技能的生命周期逻辑和数据构成技能实体(SkillEntity)的主要内容
(上文中的生命周期和数据也可以是任何其他概念的)
技能组合
当谈论到组合时,需要知道系统中核心对象的最小粒度。
技能是技能系统中设计时的对象,不是实现时的对象。
如果认为技能是更基础的对象,不同技能就可以组合出能力Ability(叫天赋、特性什么的都OK)组合而成。而能力对玩家来说就是技能。
也难辞,一个能力,是由多段技能组成。
技能表现
释放技能时所看到的一切都可以看作是技能的表现:包括角色动作、角色移动、角色变身、技能特效、召唤物、环境变化、伤害飘字等
在技能释放过程中会有什么表现是多种多样的,取决于具体的需求,事先无法预料
因此,不能在Exceute中做固定的逻辑,其只是开启表现逻辑的入口,需要将具体的逻辑拆分开来
对于这种情况,通常会将不同表现的逻辑抽象为一个个组件,需要调整表现时,调整一系列组件即可,因此需要抽象出SkillComponent基类
技能类型
从持续时间划分:
能力类型
从释放次数划分:
能力数据
技能数据
逻辑组件
每个逻辑组件都有各自的Data和System,具体数据是什么,需要具体的逻辑是什么,同时各类System可以用统一的映射和管理,即有个LogicSystemMgr
注意,这里的逻辑组件只是相对于技能组件是逻辑,其本身也有自己的逻辑和数据
运行时数据
此时只考虑本地数据,远程数据不考虑
运行时数据与配置数据的区别在于:每个配置数据都对应有个当前数据,如果配置数据的默认值在机制上允许动态改变,那么运行时数据中需要存在默认值。
例如:技能会有个初始的默认CD,但通常技能CD可以改变,那么配置的CD数据有两个对应的运行时数据
初始化Init
初始化时有两个主要工作
面对这样的情况,一种简单的方式是每种数据各自写初始化方法;另一种方式是将参数合并做上下文传递各取所需,我们先采用简单的方式
销毁Destroy
对于常用属性,其值设回初始值即可,因为每次都会重新初始化,有些值可以不必再设置
对于配置数据置空,对于运行时数据要销毁
对于逻辑要停止
检查Check
检查用于判断是否可以到Enter,具体有什么检查与具体的概念相关,对于技能而言,其意思是技能当前是否可以使用,包括以下检查:
通用检查:即CD、蓝量、血量、体力是否足够,考虑到不同技能检查的差异性,这些都要分开允许重写
特殊检查:即每个技能有各自可以释放的前提条件
进入Enter/执行Excute/退出Exit
技能释放时有各种各样的表现,所有这些表现都是在技能释放期间,也即Excute阶段触发的
注意,Execute阶段只管各种表现什么时候触发,不管表现什么时候出现或者结束,也即技能释放完成和技能的表现完成是两个不同的概念
例如,有的技能特效需要延迟一段时间才会出现,但该技能特效实际已经触发了;有的技能可以冰冻敌人,技能已经释放完成,但敌人可能还会被冻住5s不能移动
通常,会假设有个技能时间轴,可以在该时间轴上配置不同表现的触发时间以相关数据。
众多表现中最为特殊的表现是动作,因为玩家操作角色相当于控制了角色的动作表现,继而影响技能的动作表现,而特效等其他表现不受玩家控制。
此时,技能表现如何触发有两种方式:一是技能驱动动作;二是动作驱动技能。
简单来说,对于技能的动作,可以分为前摇施法后摇三个阶段,共三个动作(可根据实际需要增加更多的动作阶段)
技能驱动动作时,按照技能时间轴依次播三个不同的动作,一般在Enter时就需要播放动画,在技能时间轴某一时刻触发其他特效、音效等表现。例如攻击动画0.5s时有个剑气特效,就将剑气的触发改为0.5s
动作驱动技能时,动作系统控制播放三个不同的动作,并按照动作时间轴,在施法动作的某些时间点触发特效、音效等表现。
无论在什么游戏中,角色动作总是远多余角色技能的。两者方式的区别如下:
动作驱动:
技能驱动:
通常动作类游戏用动作驱动的方式,例如鬼泣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;
}
}