Unity——技能系統(二)

小紫蘇發表於2021-11-11

Unity技能系統(二)

Unity技能系統(一)

Demo展示:

五.技能管理和釋放

1.CharacterSkillSystem

技能系統類,給外部(技能按鈕,按鍵)提供技能釋放方法;

技能釋放邏輯:

image-20211111122441848

按順序判定條件,成立怎繼續,否則返回;

最終呼叫CharacterSkillManager中的DeploySkill方法;傳遞引數為SkillData;

提供了隨機技能方法;

/// <summary>
/// 角色系統
/// </summary>
[RequireComponent(typeof(CharacterSkillManager))]
public class CharacterSkillSystem : MonoBehaviour
{
    //技能管理
    public CharacterSkillManager chSkillMgr;

    //角色狀態
    private CharacterStatus chStatus;

    //角色動畫
    private Animator mAnimator;

    //當前使用的技能
    private SkillData currentUseSkill;

    //當前攻擊的目標
    private Transform currentSelectedTarget;

    //初始化
    public void Start()
    {
        mAnimator = GetComponent<Animator>();
        chSkillMgr = GetComponent<CharacterSkillManager>();
        chStatus = GetComponent<CharacterStatus>();
    }

    /// <summary>
    /// 使用指定技能
    /// </summary>
    /// <param name="skillid">技能編號</param>
    /// <param name="isBatter">是否連擊</param>
    public void AttackUseSkill(int skillid, bool isBatter = false)
    {
        //如果是連擊,找當前技能的下一個連擊技能
        if (currentUseSkill != null && isBatter)
            skillid = currentUseSkill.skill.nextBatterId;
        //準備技能
        currentUseSkill = chSkillMgr.PrepareSkill(skillid);
        if (currentUseSkill != null)
        {
            //選中釋放技能呼叫
            if ((currentUseSkill.skill.damageType & DamageType.Select) == DamageType.Select)
            {
                var selectedTaget = SelectTarget();
                if (currentUseSkill.skill.attckTargetTags.Contains("Player"))
                    selectedTaget = gameObject;

                if (selectedTaget != null)
                {
                    CharacterStatus selectStatus = null;
                    //修改成獲取characterStatus中的Selected節點設定隱藏;
                    if (currentSelectedTarget != null)
                    {
                        selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
                        selectStatus.selected.SetActive(false);
                    }
                    currentSelectedTarget = selectedTaget.transform;
                    selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
                    selectStatus.selected.SetActive(true);
                    
                    //buff技能
                    if ((currentUseSkill.skill.damageType & DamageType.Buff) == DamageType.Buff)
                    {
                        foreach (var buff in currentUseSkill.skill.buffType)
                        {
                            //加bufficon
                            GameObject uiPortrait = selectStatus.uiPortrait.gameObject;
                            MonsterMgr.I.HideAllEnemyPortraits();
                            uiPortrait.SetActive(true);
                            uiPortrait.transform.SetAsLastSibling();
                            selectStatus.uiPortrait.AddBuffIcon(buff, currentUseSkill.skill.buffDuration);

                            //已有該buff重新整理
                            bool exist = false;
                            var buffs = selectedTaget.GetComponents<BuffRun>();
                            foreach (var it in buffs)
                            {
                                if (it.bufftype == buff)
                                {
                                    it.Reset();
                                    exist = true;
                                    break;
                                }
                            }

                            if (exist)
                                continue;

                            //新增新buff
                            var buffRun = selectedTaget.AddComponent<BuffRun>();
                            buffRun.InitBuff(buff, currentUseSkill.skill.buffDuration,
                                currentUseSkill.skill.buffValue, currentUseSkill.skill.buffInterval);
                        }			
                    }

                    //轉向目標
                    //transform.LookAt(currentSelectedTarget);
                    chSkillMgr.DeploySkill(currentUseSkill);
                    mAnimator.Play(currentUseSkill.skill.animtionName);
                }
            }
            else
            {
                chSkillMgr.DeploySkill(currentUseSkill);
                mAnimator.Play(currentUseSkill.skill.animtionName);
            }
        }
    }

    /// <summary>
    /// 隨機選擇技能
    /// </summary>
    public void RandomSelectSkill()
    {
        if (chSkillMgr.skills.Count > 0)
        {
            int index = UnityEngine.Random.Range(0, chSkillMgr.skills.Count);
            currentUseSkill = chSkillMgr.PrepareSkill(chSkillMgr.skills[index].skill.skillID);
            if (currentUseSkill == null) //隨機技能未找到或未冷卻結束
                currentUseSkill = chSkillMgr.skills[0]; //用技能表中第一個(預設技能)做補充
        }
    }

    //選擇目標
    private GameObject SelectTarget()
    {
        //發一個球形射線,找出所有碰撞體
        var colliders = Physics.OverlapSphere(transform.position, currentUseSkill.skill.attackDisntance);
        if (colliders == null || colliders.Length == 0) return null;

        //從碰撞體列表中挑出所有的敵人
        String[] attTags = currentUseSkill.skill.attckTargetTags;
        var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject);
       
        //正前方,tag正確,血量大於0,處於正前方的敵人
        array = CollectionHelper.FindAll<GameObject>(array,
            p => Array.IndexOf(attTags, p.tag) >= 0
                 && p.GetComponent<CharacterStatus>().HP > 0 &&
                 Vector3.Angle(transform.forward, p.transform.position - transform.position) <= 90);

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

        //將所有的敵人,按與技能的發出者之間的距離升序排列,
        CollectionHelper.OrderBy<GameObject, float>(array,
            p => Vector3.Distance(transform.position, p.transform.position));
        return array[0];
    }
}

2.CharacterSkillManager

技能資料的管理,載入所有技能特效模板進入物件池;

給CharacterSkillSystem提供技能釋放介面DeploySkill;

提供技能冷卻計算,預留獲取cd剩餘時間介面給UI,以及獲取技能是否在cd中;

[RequireComponent(typeof(CharacterSkillSystem))]
public class CharacterSkillManager : MonoBehaviour
{
    /// <summary>管理所有技能的容器</summary>
    public List<SkillData> skills = new List<SkillData>();

    /// <summary>技能的擁有者</summary>
    private CharacterStatus chStatus = null;

    private SkillData curSkill;
	
    //新增技能資料
    private void AddSkill(string path)
    {
        SkillTemp skTemp = Instantiate(Resources.Load<SkillTemp>(path));
        Skill sk = LoadSkill(skTemp);;
        SkillData skd = new SkillData();
        skd.skill = sk;
        skills.Add(skd);
    }

    //初始化技能資料(有什麼技能)
    public void Start()
    {
        chStatus = GetComponent<CharacterStatus>();

        AddSkill("Skill_1");
        AddSkill("Skill_2");
        AddSkill("Skill_3");
        AddSkill("Skill_4");
        AddSkill("Skill_5");
        
        foreach (var item in skills)
        {
            //動態載入技能特效預製體  //Resources/Skill -- 技能特效預製體 
            if (item.skillPrefab == null && !string.IsNullOrEmpty(item.skill.prefabName))
                item.skillPrefab = LoadFxPrefab("Skill/" + item.skill.prefabName);
            
            //Resources/Skill/HitFx     技能傷害特效預製體
            if (item.hitFxPrefab == null && !string.IsNullOrEmpty(item.skill.hitFxName))
                item.hitFxPrefab = LoadFxPrefab("Skill/" + item.skill.hitFxName);
        }
    }

    //將特效預製件載入到物件池,以備將來使用
    private GameObject LoadFxPrefab(string path)
    {
        var key = path.Substring(path.LastIndexOf("/") + 1);
        var go = Resources.Load<GameObject>(path);
        GameObjectPool.I.Destory(
            GameObjectPool.I.CreateObject(
                key, go, transform.position, transform.rotation)
        );
        return go;
    }

    //準備技能
    public SkillData PrepareSkill(int id)
    {
        //從技能容器中找出相應ID的技能
        var skillData = skills.Find(p => p.skill.skillID == id);
        if (skillData != null && //查詢到技能
            chStatus.SP >= skillData.skill.costSP && //檢查角色SP是否夠使用該技能
            skillData.coolRemain == 0) //且該技能已經冷卻結束
        {
            skillData.Owner = gameObject;
            return skillData;
        }

        return null;
    }

    //釋放技能
    public void DeploySkill(SkillData skillData)
    {
        //開始冷卻計時
        StartCoroutine(CoolTimeDown(skillData));

        //動畫某一幀觸發技能特效,這裡寫一個延遲呼叫的方法,使用動畫時間的百分解決特效釋放時間問題
        if (skillData.skill.delayAnimaTime != 0)
        {
            curSkill = skillData;
            Invoke("DelayDeploySkill", skillData.skill.delayAnimaTime);
            return;
        }
        
        GameObject tempGo = null;
        //建立技能預製體+建立位置的偏移
        if ((skillData.skill.damageType & DamageType.FxOffset) == DamageType.FxOffset)
            tempGo = GameObjectPool.I.CreateObject(skillData.skill.prefabName, skillData.skillPrefab,
                transform.position + transform.forward * skillData.skill.fxOffset, transform.rotation);
       	//技能有發射點
        else if ((skillData.skill.damageType & DamageType.FirePos) == DamageType.FirePos)
            tempGo = GameObjectPool.I.CreateObject(skillData.skill.prefabName, skillData.skillPrefab,
                chStatus.FirePos.position, chStatus.FirePos.rotation);

        if(tempGo == null)
            return;

        //從預製體物件上找到技能釋放物件 
        var deployer = tempGo.GetComponent<SkillDeployer>();
        if (deployer == null)
            deployer = tempGo.AddComponent<SkillDeployer>();

        //設定要釋放的技能————劃重點
        deployer.skillData = skillData;
        //呼叫釋放方法
        deployer.DeploySkill();
        
        //技能持續時間過後,技能要銷燬
        if ((skillData.skill.damageType & DamageType.Bullet) != DamageType.Bullet)
        {
            if (skillData.skill.durationTime > 0)
                GameObjectPool.I.Destory(tempGo, skillData.skill.durationTime);
            else
                GameObjectPool.I.Destory(tempGo, 0.5f);
        }
    }
	
    //延遲釋放技能
    private void DelayDeploySkill()
    {
        GameObject tempGo = null;
        //建立技能預製體+建立位置的偏移
        if ((curSkill.skill.damageType & DamageType.FxOffset) == DamageType.FxOffset)
            tempGo = GameObjectPool.I.CreateObject(curSkill.skill.prefabName, curSkill.skillPrefab,
                transform.position + transform.forward * curSkill.skill.fxOffset, transform.rotation);
        
        else if ((curSkill.skill.damageType & DamageType.FirePos) == DamageType.FirePos)
            tempGo = GameObjectPool.I.CreateObject(curSkill.skill.prefabName, curSkill.skillPrefab,
                chStatus.FirePos.position, chStatus.FirePos.rotation);

        //從預製體物件上找到技能釋放物件 
        var deployer = tempGo.GetComponent<SkillDeployer>();
        if (deployer == null)
            deployer = tempGo.AddComponent<SkillDeployer>();

        //設定要釋放的技能
        deployer.skillData = curSkill;
        //呼叫釋放方法
        deployer.DeploySkill();

        //技能持續時間過後,技能要銷燬
        if ((curSkill.skill.damageType & DamageType.Bullet) != DamageType.Bullet)
        {
            if (curSkill.skill.durationTime > 0)
                GameObjectPool.I.Destory(tempGo, curSkill.skill.durationTime);
            else
                GameObjectPool.I.Destory(tempGo, 0.5f);
        }
    }

    //冷卻時間倒數計時
    public IEnumerator CoolTimeDown(SkillData skillData)
    {
        skillData.coolRemain = skillData.skill.coolTime;
        while (skillData.coolRemain > 0)
        {
            yield return new WaitForSeconds(0.1f);
            skillData.coolRemain -= 0.1f;
        }

        skillData.coolRemain = 0;
    }

    //取得冷卻倒數計時的剩餘時間(秒)
    public float GetSkillCoolRemain(int id)
    {
        return skills.Find(p => p.skill.skillID == id).coolRemain;
    }

    private Skill LoadSkill(SkillTemp skillTemp)
    {
        Skill sk = skillTemp.skill;
        int count = skillTemp.damageType.Length;
        for (int i = 0; i < count; ++i)
        {
            sk.damageType = sk.damageType | skillTemp.damageType[i];
        }
        return sk;
    }
}

3.SkillDeployer

掛載在技能特效上, 執行技能對釋放者造成的影響(消耗MP,重新整理MPUI);

對命中目標執行傷害計算,載入受傷特效新增debuff等;

傷害觸發分為碰撞觸發和目標選擇器選中觸發;

上面劃得重點:

給技能釋放器中skillData屬性賦值的同時,建立目標選擇器,給CharacterStatrus欄位賦值;

中間有很多坑點:

1.重新整理敵人頭像顯示,必須要設定顯示層級在ui的最下層,同時設定其他UI位置,不能設定Active,禁用buff倒數計時計算會失效,也可以將buff倒數計時單獨管理;

2.檢測已有相同buff存在重新整理buff時間;

3.多段傷害,每段傷害要重新檢測攻擊目標,有擊退等buff存在;

4.傷害計算單獨寫方法,方便修改;

5.彈道和碰撞觸發傷害的技能,受擊特效掛載點不應該是HitFxPos,而是碰撞的接觸點,然而使用觸發器碰撞沒辦法返回碰撞點座標,所以又做了射線檢測;但是又會存在新的問題,射線檢測只有一條線,沒有體積,會造成邊緣碰撞時射線未檢測到,卻已經觸發碰撞了;

這裡做了處理,射線未檢測到卻碰撞特效生成在HitFxPos;

可以自行嘗試一下在技能特效的前段設定HitFxPos來設定受擊特效的位置;

public class SkillDeployer : MonoBehaviour
{
    private SkillData m_skillData;

    ///<summary>敵人選區,選擇目標的演算法</summary>
    public IAttackSelector attackTargetSelector;

    private DamageMode damageMode;

    //發出者
    private CharacterStatus status;

    /// <summary> 要釋放的技能 </summary>
    public SkillData skillData
    {
        set
        {
            m_skillData = value;
            damageMode = 0;
            if ((skillData.skill.damageType & DamageType.Sector) == DamageType.Sector)
                damageMode = DamageMode.Sector;
            else if ((skillData.skill.damageType & DamageType.Circle) == DamageType.Circle)
                damageMode = DamageMode.Circle;
            else if ((skillData.skill.damageType & DamageType.Line) == DamageType.Line)
                damageMode = DamageMode.Line;

            if (damageMode != 0)
                attackTargetSelector = SelectorFactory.CreateSelector(damageMode);

            status = value.Owner.GetComponent<CharacterStatus>();
        }
        get { return m_skillData; }
    }


    /// <summary>技能釋放</summary>
    public virtual void DeploySkill()
    {
        if (m_skillData == null) return;
        //對自身的影響
        SelfImpact(m_skillData.Owner);

        //執行傷害的計算
        if (damageMode != 0) 
            StartCoroutine(ExecuteDamage());
    }

    //執行傷害的計算
    protected virtual IEnumerator ExecuteDamage()
    {
        //按持續時間及,兩次傷害間隔,
        float attackTimer = 0; //已持續攻擊的時間
        
        ResetTargets();
        if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
        {
            //Debug.Log(skillData.attackTargets[0].name);
            foreach (var item in skillData.attackTargets)
            {
                //重新整理敵人頭像顯示
                CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
                GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
                MonsterMgr.I.HideAllEnemyPortraits();
                uiPortrait.SetActive(true);
                uiPortrait.transform.SetAsLastSibling();
                
                //加buff
                foreach (var buff in skillData.skill.buffType)
                {
                    //加bufficon
                    targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
                    
                    //已有該buff重新整理
                    bool exist = false;
                    var buffs = item.GetComponents<BuffRun>();
                    
                    foreach (var it in buffs)
                    {
                        if (it.bufftype == buff)
                        {
                            it.Reset();
                            exist = true;
                            break;
                        }
                    }

                    if (exist)
                    {
                        continue;
                    }

                    //新增新buff
                    var buffRun = item.AddComponent<BuffRun>();
                    buffRun.InitBuff(buff, skillData.skill.buffDuration, skillData.skill.buffValue,
                        skillData.skill.buffInterval);
                }
            }
        }

        do
        {
            //通過選擇器選好攻擊目標
            ResetTargets();
            if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
            {
                //Debug.Log(skillData.attackTargets[0].name);
                foreach (var item in skillData.attackTargets)
                {
                    //對敵人的影響
                    TargetImpact(item);
                }
            }

            yield return new WaitForSeconds(skillData.skill.damageInterval);
            attackTimer += skillData.skill.damageInterval;
            //做傷害數值的計算
        } while (skillData.skill.durationTime > attackTimer);
    }

    private void ResetTargets()
    {
        if (m_skillData == null)
            return;

        m_skillData.attackTargets = attackTargetSelector.SelectTarget(m_skillData, transform);
    }

    private float CirculateDamage(GameObject goTarget)
    {
        CharacterStatus goStatus = goTarget.GetComponent<CharacterStatus>();

        //是否命中計算
        float rate = status.hitRate / (float) goStatus.dodgeRate;
        if (rate < 1)
        {
            int max = (int) (rate * 100);
            int val = Random.Range(0, 100);
            if (val < max)
            {
                //Debug.Log("Miss");
                return 0;
            }
        }

        //普攻的技能傷害為0; 技能有固定傷害*等級加成 + 普攻傷害
        var damageVal = status.damage * (1000 / (1000 + goStatus.defence)) +
                        skillData.skill.damage * (1 + skillData.level * skillData.skill.damageRatio);
        return damageVal;
    }

    ///對敵人的影響nag
    public virtual void TargetImpact(GameObject goTarget)
    {
        //出受傷特效
        if (skillData.hitFxPrefab != null)
        {
            //找到受擊特效的掛點
            Transform hitFxPos = goTarget.GetComponent<CharacterStatus>().HitFxPos;

            var go = GameObjectPool.I.CreateObject(
                skillData.skill.hitFxName,
                skillData.hitFxPrefab,
                hitFxPos.position,
                hitFxPos.rotation);
            go.transform.SetParent(hitFxPos);
            GameObjectPool.I.Destory(go, 2f);
        }

        //受傷
        var damageVal = CirculateDamage(goTarget);
        var targetStatus = goTarget.GetComponent<CharacterStatus>();
        targetStatus.OnDamage((int) damageVal, skillData.Owner);
    }

	//碰撞觸發目標影響
    public virtual void TargetImpact(GameObject goTarget, Collider collider)
    {
        //敵人buff
        foreach (var buff in skillData.skill.buffType)
        {
            //已有該buff重新整理
            bool exist = false;
            var buffs = goTarget.GetComponents<BuffRun>();
            foreach (var it in buffs)
            {
                if (it.bufftype == buff)
                {
                    it.Reset();
                    exist = true;
                    break;
                }
            }

            if (exist)
                continue;

            //新增新buff
            var buffRun = goTarget.AddComponent<BuffRun>();
            buffRun.InitBuff(buff, skillData.skill.buffDuration,
                skillData.skill.buffValue, skillData.skill.buffInterval);
        }
        


        //出受傷特效
        if (skillData.hitFxPrefab != null)
        {
            //找到受擊特效的掛點,碰撞但未檢測到射線點,生成受擊特效在hitFxPos處
            Ray ray = new Ray(transform.position, transform.forward);
            RaycastHit hit;
            Physics.Raycast((Ray) ray, out hit, 1000);
            if (hit.collider == collider)
            {
                var go = GameObjectPool.I.CreateObject(
                    skillData.skill.hitFxName,
                    skillData.hitFxPrefab,
                    hit.point,
                    transform.rotation);
                GameObjectPool.I.Destory(go, 2f);
            }
            else
            {
                Transform hitFxPos = goTarget.GetComponent<CharacterStatus>().HitFxPos;
                var go = GameObjectPool.I.CreateObject(
                    skillData.skill.hitFxName,
                    skillData.hitFxPrefab,
                    hitFxPos.position,
                    hitFxPos.rotation);
                GameObjectPool.I.Destory(go, 2f);
            }
        }

        //受傷
        var damageVal = CirculateDamage(goTarget);
        var targetStatus = goTarget.GetComponent<CharacterStatus>();
        targetStatus.OnDamage((int) damageVal, skillData.Owner);
    }

    ///對自身的影響
    public virtual void SelfImpact(GameObject goSelf)
    {
        //釋放者: 消耗SP
        var chStaus = goSelf.GetComponent<CharacterStatus>();
        if (chStaus.SP != 0)
        {
            chStaus.SP -= m_skillData.skill.costSP;
            chStaus.uiPortrait.RefreshHpMp();
            //add+2 魔法條更新
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if ((skillData.skill.damageType & DamageType.Bullet) == DamageType.Bullet)
        {
            if (skillData.skill.attckTargetTags.Contains(other.tag))
            {
                if (skillData.skill.attackNum == 1)
                {
                    CharacterStatus targetStatus = other.GetComponent<CharacterStatus>();
                    GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
                    MonsterMgr.I.HideAllEnemyPortraits();
                    uiPortrait.SetActive(true);
                    uiPortrait.transform.SetAsLastSibling();
                    
                    //加buff
                    foreach (var buff in skillData.skill.buffType)
                    {
                        //加bufficon
                        targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
                    }
                    
                    TargetImpact(other.gameObject, other);
                }
                else
                {
                    //通過選擇器選好攻擊目標
                    IAttackSelector selector = new CircleAttackSelector();
                    selector.SelectTarget(m_skillData, transform);
                    if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
                    {
                        foreach (var item in skillData.attackTargets)
                        {
                            //重新整理敵人頭像顯示
                            CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
                            GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
                            MonsterMgr.I.HideAllEnemyPortraits();
                            uiPortrait.SetActive(true);
                            uiPortrait.transform.SetAsLastSibling();
                            
                            //加buff
                            foreach (var buff in skillData.skill.buffType)
                            {
                                //加bufficon
                                targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
                            }

                            //對敵人的影響
                            TargetImpact(item, other);
                        }
                    }
                }

                GameObjectPool.I.Destory(gameObject);
            }
            else if (other.CompareTag("Wall"))
            {
                if (skillData.hitFxPrefab != null)
                {
                    Ray ray = new Ray(transform.position, transform.forward);
                    RaycastHit hit;
                    Physics.Raycast((Ray) ray, out hit, 1000);

                    if (hit.collider != other)
                        return;

                    //找到受擊特效的掛點
                    var go = GameObjectPool.I.CreateObject(
                        skillData.skill.hitFxName,
                        skillData.hitFxPrefab,
                        hit.point,
                        other.transform.rotation);
                    //go.transform.SetParent(hitFxPos);
                    GameObjectPool.I.Destory(go, 2f);
                }

                GameObjectPool.I.Destory(gameObject);
            }
        }
    }
    
    
    public static Dictionary<BuffType, string> buffIconName = new Dictionary<BuffType, string>();
    
    public static void InitBuffIconName()
    {
        buffIconName.Add(BuffType.Burn,"Buff_13");
        buffIconName.Add(BuffType.Slow,"Buff_15");
        buffIconName.Add(BuffType.Stun,"Buff_12");
        buffIconName.Add(BuffType.Poison,"Buff_14");
        buffIconName.Add(BuffType.BeatBack,"Buff_5");
        buffIconName.Add(BuffType.BeatUp,"Buff_4");
        buffIconName.Add(BuffType.Pull,"Buff_6");
        buffIconName.Add(BuffType.AddDefence,"Buff_3");
        buffIconName.Add(BuffType.RecoverHp,"Buff_7");
        buffIconName.Add(BuffType.Light,"Buff_8");
    }
}

小結

到目前,所有技能邏輯都結束;下一節介紹buff系統和UI顯示相關;

相關文章