Unity——技能系统(一)

时间:2024-04-17 13:03:55

技能系统(一)

一.Demo展示

二.功能介绍

集成了技能,冷却,buff,UI显示,倒计时,动画等;

技能类型:弹道技能,动画事件根据帧数采用延迟调用技能,自定义释放位置(偏移,发射点两种),buff类型技能(自身增益buff,敌人减益buff,比如加防御和毒);

技能伤害判定:碰撞判定,圆形判定(自定义圆心和半径),扇形(角度和半径),线性(长宽),选中目标才可释放;

技能伤害支持多段;

Buff类型:燃烧,减速,感电,眩晕,中毒,击退,击飞,拉拽;增益:回血,加防御;

三.工具类介绍

CollectionHelper——数组工具,泛型,可以传入数组和条件委托,返回数组中符合条件的所有对象,以及排序功能;

TransformHelper——递归查找指定父节点下所有子节点,返回找到的目标;

SingletonMono——继承了MonoBehaviour的单例;

GameObjectPool——对象池

DamagePopup——掉血数值显示

四.基类

1.Skill

技能数据类,所有可以外部导入的技能数据都放在这个类中,以便于可以外部导入数据;

由于测试demo,我另外写了一个SkillTemp类,继承了ScriptaleObject,方便填写测试数据;

/// <summary>
/// 技能类型,可叠加
/// </summary>
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的倍数,可以使用与或非来减少变量个数;

后来发现直接用List好像也行,后面的技能就使用了List来存储叠加的情况;

[CreateAssetMenu(menuName="Create SkillTemp")]
public class SkillTemp : ScriptableObject
{
    public Skill skill = new Skill();

    /// <summary>技能类型,可用 | 拼接</summary>>
    public DamageType[] damageType;
}

继承了ScriptableObject可以右键创建技能模板,直接在inspector界面编辑;

image-20211111010925395

2.SkillData

组合了Skill类,在Skill类的基础上,添加了更多的不可外部传参的数据;

比如技能特效的引用,技能所有者引用,存储技能攻击目标对象用来在技能模块之间传递,以及技能等级冷却等动态变化的数据;

public class SkillData
{
    [HideInInspector] public GameObject Owner;
   
    /// <summary>技能数据</summary>
    [SerializeField]
    public Skill skill;

    /// <summary>技能等级</summary>
    public int level;
    
    /// <summary>冷却剩余</summary>
    [HideInInspector]
    public float coolRemain;
    
    /// <summary>攻击目标</summary>
    [HideInInspector] public GameObject[] attackTargets;

    /// <summary>是否激活</summary>
    [HideInInspector]
    public bool Activated;

    /// <summary>技能预制对象</summary>
    [HideInInspector] 
    public GameObject skillPrefab;
    
    [HideInInspector] 
    public GameObject hitFxPrefab;
}

3.CharacterStatus

准确来说这个类不属于技能系统,他用来几率人物属性数据,以及提供受伤,刷新UI条等接口;

同时这个类存储着技能系统必须用到的受击特效挂载点HitFxPos,发射点FirePos,选中Mesh或特效物体selected,伤害数值出现点hudPos,自身头像血条UI物体uiPortrait;

最好是英雄和敌人单独写一个类继承这个基类,但是测试的话这个类就够用了;

public class CharacterStatus : MonoBehaviour
{
    /// <summary>生命 </summary>
    public float HP = 100;
    /// <summary>生命 </summary>
    public float MaxHP=100;
    /// <summary>当前魔法 </summary>
    public float SP = 100;
    /// <summary>最大魔法 </summary>
    public float MaxSP =100;
    /// <summary>伤害基数</summary>
    public float damage = 100;
    ///<summary>命中</summary>
    public float hitRate = 1;
    ///<summary>闪避</summary>
    public float dodgeRate = 1;
    /// <summary>防御</summary>  
    public float defence = 10f;
    /// <summary>主技能攻击距离 ,用于设置AI的攻击范围,与目标距离此范围内发起攻击</summary>
    public float attackDistance = 2;
    /// <summary>受击特效挂点 挂点名为HitFxPos </summary>
    [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<UIPortrait>();
        }
        else if (CompareTag("Enemy"))
        {
            Transform canvas = GameObject.FindGameObjectWithTag("Canvas").transform;
            uiPortrait = Instantiate(Resources.Load<GameObject>("UIEnemyPortrait"), canvas).GetComponent<UIPortrait>();
            uiPortrait.gameObject.SetActive(false);
            //存储所有的uiPortarit在单例中
            MonsterMgr.I.AddEnemyPortraits(uiPortrait);
        }
        uiPortrait.cstatus = this;
        //更新血蓝条
        uiPortrait.RefreshHpMp();
        
        damagePopup = Resources.Load<GameObject>("HUD");
      	//初始化数据
        selected = TransformHelper.FindChild(transform, "Selected").gameObject;
        HitFxPos = TransformHelper.FindChild(transform, "HitFxPos");
        FirePos = TransformHelper.FindChild(transform, "FirePos");
        hudPos = TransformHelper.FindChild(transform, "HUDPos");
    }
    
    /// <summary>受击 模板方法</summary>
    public virtual void OnDamage(float damage, GameObject killer,bool isBuff = false)
    {
        //应用伤害
        var damageVal = ApplyDamage(damage, killer);
        
        //应用PopDamage
        DamagePopup pop = Instantiate(damagePopup).GetComponent<DamagePopup>();
        pop.target = hudPos;
        pop.transform.rotation = Quaternion.identity;
        pop.Value = damageVal.ToString();
        
        //ApplyUI画像
        if (!isBuff)
        {
            uiPortrait.gameObject.SetActive(true);
            uiPortrait.transform.SetAsLastSibling();
            uiPortrait.RefreshHpMp();
        }
    }

    /// <summary>应用伤害</summary>
    public virtual float ApplyDamage(float damage, GameObject killer)
    {
        HP -= damage;
        //应用死亡
        if (HP <= 0)
        {
            HP = 0;
            Destroy(killer, 5f);
        }
        
        return damage;
    }
}

4.IAttackSelector

目标选择器接口,只定义了一个方法,选择符合条件的目标并返回;

//策略模式 将选择算法进行抽象
/// <summary>攻击目标选择算法</summary>
public interface IAttackSelector
{
    ///<summary>目标选择算法</summary>
    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<Collider, GameObject>(colliders, p => p.gameObject);
      	//挑选出对象中能攻击的,血量大于0的
        array = CollectionHelper.FindAll<GameObject>(array,
            p => Array.IndexOf(attTags, p.tag) >= 0
                 && p.GetComponent<CharacterStatus>().HP > 0);

        if (array == null || array.Length == 0) return null;

        GameObject[] targets = null;
        //根据技能是单体还是群攻,决定返回多少敌人对象
        if (skillData.skill.attackNum == 1)
        {
            //将所有的敌人,按与技能的发出者之间的距离升序排列,
            CollectionHelper.OrderBy<GameObject, float>(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<string, IAttackSelector> cache = new Dictionary<string, IAttackSelector>();

    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更有体验感,还需要有角色控制,相机跟随脚本;

之后就是技能管理系统,技能释放器等;