Unity——技能系統(一)

小紫蘇發表於2021-11-11

技能系統(一)

一.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好像也行,後面的Buff就使用了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更有體驗感,還需要有角色控制,相機跟隨指令碼;

之後就是技能管理系統,技能釋放器等;

相關文章