Unity——技能系统(一)

技能系统(一) 一.Demo展示 二.功能介绍 集成了技能,冷却,buff,UI显示,倒计时,动画等;
技能类型:弹道技能,动画事件根据帧数采用延迟调用技能,自定义释放位置(偏移,发射点两种),buff类型技能(自身增益buff,敌人减益buff,比如加防御和毒);
技能伤害判定:碰撞判定,圆形判定(自定义圆心和半径),扇形(角度和半径),线性(长宽),选中目标才可释放;
技能伤害支持多段;
Buff类型:燃烧,减速,感电,眩晕,中毒,击退,击飞,拉拽;增益:回血,加防御;
三.工具类介绍 CollectionHelper——数组工具,泛型,可以传入数组和条件委托,返回数组中符合条件的所有对象,以及排序功能;
TransformHelper——递归查找指定父节点下所有子节点,返回找到的目标;
SingletonMono——继承了MonoBehaviour的单例;
GameObjectPool——对象池;
DamagePopup——掉血数值显示;
四.基类 1.Skill
技能数据类,所有可以外部导入的技能数据都放在这个类中,以便于可以外部导入数据;
由于测试demo,我另外写了一个SkillTemp类,继承了ScriptaleObject,方便填写测试数据;

/// /// 技能类型,可叠加 /// public enum DamageType { Bullet = 4,//特效粒子碰撞伤害 None = 8,//无伤害,未使用,为none可以不选 Buff = 32,//buff技能//二选一 FirePos = 128,//有发射位置点 FxOffset = 256,//发射偏移,无偏移偏移量为0//四选一 Circle = 512,//圈判定 Sector = 1024,//扇形判定 Line = 4096,//线性判定 Select = 8192,//选中才可释放 }

DamageType用来确定技能的行为,赋值都是2的倍数,可以使用与或非来减少变量个数;
【Unity——技能系统(一)】后来发现直接用List好像也行,后面的技能就使用了List来存储叠加的情况;
[CreateAssetMenu(menuName="Create SkillTemp")] public class SkillTemp : ScriptableObject { public Skill skill = new Skill(); /// 技能类型,可用 | 拼接> public DamageType[] damageType; }

继承了ScriptableObject可以右键创建技能模板,直接在inspector界面编辑;
Unity——技能系统(一)
文章图片

2.SkillData
组合了Skill类,在Skill类的基础上,添加了更多的不可外部传参的数据;
比如技能特效的引用,技能所有者引用,存储技能攻击目标对象用来在技能模块之间传递,以及技能等级冷却等动态变化的数据;
public class SkillData { [HideInInspector] public GameObject Owner; /// 技能数据 [SerializeField] public Skill skill; /// 技能等级 public int level; /// 冷却剩余 [HideInInspector] public float coolRemain; /// 攻击目标 [HideInInspector] public GameObject[] attackTargets; /// 是否激活 [HideInInspector] public bool Activated; /// 技能预制对象 [HideInInspector] public GameObject skillPrefab; [HideInInspector] public GameObject hitFxPrefab; }

3.CharacterStatus
准确来说这个类不属于技能系统,他用来几率人物属性数据,以及提供受伤,刷新UI条等接口;
同时这个类存储着技能系统必须用到的受击特效挂载点HitFxPos,发射点FirePos,选中Mesh或特效物体selected,伤害数值出现点hudPos,自身头像血条UI物体uiPortrait;
最好是英雄和敌人单独写一个类继承这个基类,但是测试的话这个类就够用了;
public class CharacterStatus : MonoBehaviour { /// 生命 public float HP = 100; /// 生命 public float MaxHP=100; /// 当前魔法 public float SP = 100; /// 最大魔法 public float MaxSP =100; /// 伤害基数 public float damage = 100; ///命中 public float hitRate = 1; ///闪避 public float dodgeRate = 1; /// 防御 public float defence = 10f; /// 主技能攻击距离 ,用于设置AI的攻击范围,与目标距离此范围内发起攻击 public float attackDistance = 2; /// 受击特效挂点 挂点名为HitFxPos [HideInInspector] public Transform HitFxPos; [HideInInspector] public Transform FirePos; public GameObject selected; private GameObject damagePopup; private Transform hudPos; public UIPortrait uiPortrait; public virtual void Start() { if (CompareTag("Player")) { uiPortrait = GameObject.FindGameObjectWithTag("HeroHead").GetComponent(); } else if (CompareTag("Enemy")) { Transform canvas = GameObject.FindGameObjectWithTag("Canvas").transform; uiPortrait = Instantiate(Resources.Load("UIEnemyPortrait"), canvas).GetComponent(); uiPortrait.gameObject.SetActive(false); //存储所有的uiPortarit在单例中 MonsterMgr.I.AddEnemyPortraits(uiPortrait); } uiPortrait.cstatus = this; //更新血蓝条 uiPortrait.RefreshHpMp(); damagePopup = Resources.Load("HUD"); //初始化数据 selected = TransformHelper.FindChild(transform, "Selected").gameObject; HitFxPos = TransformHelper.FindChild(transform, "HitFxPos"); FirePos = TransformHelper.FindChild(transform, "FirePos"); hudPos = TransformHelper.FindChild(transform, "HUDPos"); }/// 受击 模板方法 public virtual void OnDamage(float damage, GameObject killer,bool isBuff = false) { //应用伤害 var damageVal = ApplyDamage(damage, killer); //应用PopDamage DamagePopup pop = Instantiate(damagePopup).GetComponent(); pop.target = hudPos; pop.transform.rotation = Quaternion.identity; pop.Value = https://www.it610.com/article/damageVal.ToString(); //ApplyUI画像 if (!isBuff) { uiPortrait.gameObject.SetActive(true); uiPortrait.transform.SetAsLastSibling(); uiPortrait.RefreshHpMp(); } }/// 应用伤害 public virtual float ApplyDamage(float damage, GameObject killer) { HP -= damage; //应用死亡 if (HP <= 0) { HP = 0; Destroy(killer, 5f); }return damage; } }

4.IAttackSelector
目标选择器接口,只定义了一个方法,选择符合条件的目标并返回;
//策略模式 将选择算法进行抽象 /// 攻击目标选择算法 public interface IAttackSelector { ///目标选择算法 GameObject[] SelectTarget(SkillData skillData, Transform skillTransform); }

LineAttackSelector,CircleAttackSelector,SectorAttackSelector线性,圆形,扇形目标选择器,继承该接口;
就只展示一个了CircleAttackSelector;
class CircleAttackSelector : IAttackSelector { public GameObject[] SelectTarget(SkillData skillData, Transform skillTransform) { //发一个球形射线,找出所有碰撞体 var colliders = Physics.OverlapSphere(skillTransform.position, skillData.skill.attackDisntance); if (colliders == null || colliders.Length == 0) return null; //通过碰撞体拿到所有的gameobject对象 String[] attTags = skillData.skill.attckTargetTags; var array = CollectionHelper.Select(colliders, p => p.gameObject); //挑选出对象中能攻击的,血量大于0的 array = CollectionHelper.FindAll(array, p => Array.IndexOf(attTags, p.tag) >= 0 && p.GetComponent().HP > 0); if (array == null || array.Length == 0) return null; GameObject[] targets = null; //根据技能是单体还是群攻,决定返回多少敌人对象 if (skillData.skill.attackNum == 1) { //将所有的敌人,按与技能的发出者之间的距离升序排列, CollectionHelper.OrderBy(array, p => Vector3.Distance(skillData.Owner.transform.position, p.transform.position)); targets = new GameObject[] {array[0]}; } else { int attNum = skillData.skill.attackNum; if (attNum >= array.Length) targets = array; else { for (int i = 0; i < attNum; i++) { targets[i] = array[i]; } } }return targets; } }

这里有个问题,技能的目标选择器每次释放技能都会调用,因此会重复频繁的创建,但其实这只是提供方法而已;
解决:使用工厂来缓存目标选择器;
//简单工厂 //创建敌人选择器 public class SelectorFactory { //攻击目标选择器缓存 private static Dictionary cache = new Dictionary(); public static IAttackSelector CreateSelector(DamageMode mode) { //没有缓存则创建 if (!cache.ContainsKey(mode.ToString())) { var nameSpace = typeof(SelectorFactory).Namespace; string classFullName = string.Format("{0}AttackSelector", mode.ToString()); if (!String.IsNullOrEmpty(nameSpace)) classFullName = nameSpace + "." + classFullName; Type type = Type.GetType(classFullName); cache.Add(mode.ToString(), Activator.CreateInstance(type) as IAttackSelector); }//从缓存中取得创建好的选择器对象 return cache[mode.ToString()]; } }

小结 所有基类,前期准备数据只有这些,另外想Demo更有体验感,还需要有角色控制,相机跟随脚本;
之后就是技能管理系统,技能释放器等;

    推荐阅读