Unity - skill system

Unity skill system (II)

Unity skill system (I)

Demo presentation:

5, Skill management and release

1.CharacterSkillSystem

Skill system class, which provides skill release methods for external (skill buttons, keys);

Skill release logic:

Determine the conditions in order. If they are true, how to continue. Otherwise, return;

Finally, call the DeploySkill method in CharacterSkillManager; The transfer parameter is SkillData;

Random skill method is provided;

/// <summary>
///Role system
/// </summary>
[RequireComponent(typeof(CharacterSkillManager))]
public class CharacterSkillSystem : MonoBehaviour
{
    //Skill management
    public CharacterSkillManager chSkillMgr;

    //Role status
    private CharacterStatus chStatus;

    //Character animation
    private Animator mAnimator;

    //Current skills
    private SkillData currentUseSkill;

    //Target of current attack
    private Transform currentSelectedTarget;

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

    /// <summary>
    ///Use specified skills
    /// </summary>
    ///< param name = "skillid" > skill No. < / param >
    ///< param name = "isbattery" > whether to combo < / param >
    public void AttackUseSkill(int skillid, bool isBatter = false)
    {
        //If it is combo, find the next combo skill of the current skill
        if (currentUseSkill != null && isBatter)
            skillid = currentUseSkill.skill.nextBatterId;
        //Preparation skills
        currentUseSkill = chSkillMgr.PrepareSkill(skillid);
        if (currentUseSkill != null)
        {
            //Check release skill call
            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;
                    //Modify to get the Selected node in characterStatus and set hidden;
                    if (currentSelectedTarget != null)
                    {
                        selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
                        selectStatus.selected.SetActive(false);
                    }
                    currentSelectedTarget = selectedTaget.transform;
                    selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
                    selectStatus.selected.SetActive(true);
                    
                    //buff skill
                    if ((currentUseSkill.skill.damageType & DamageType.Buff) == DamageType.Buff)
                    {
                        foreach (var buff in currentUseSkill.skill.buffType)
                        {
                            //Add bufficon
                            GameObject uiPortrait = selectStatus.uiPortrait.gameObject;
                            MonsterMgr.I.HideAllEnemyPortraits();
                            uiPortrait.SetActive(true);
                            uiPortrait.transform.SetAsLastSibling();
                            selectStatus.uiPortrait.AddBuffIcon(buff, currentUseSkill.skill.buffDuration);

                            //The buff has been refreshed
                            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;

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

                    //Turn to target
                    //transform.LookAt(currentSelectedTarget);
                    chSkillMgr.DeploySkill(currentUseSkill);
                    mAnimator.Play(currentUseSkill.skill.animtionName);
                }
            }
            else
            {
                chSkillMgr.DeploySkill(currentUseSkill);
                mAnimator.Play(currentUseSkill.skill.animtionName);
            }
        }
    }

    /// <summary>
    ///Random selection skills
    /// </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) //Random skill not found or not cooled down
                currentUseSkill = chSkillMgr.skills[0]; //Supplement with the first (default) skill in the skill table
        }
    }

    //Select target
    private GameObject SelectTarget()
    {
        //Send a spherical ray and find all the colliders
        var colliders = Physics.OverlapSphere(transform.position, currentUseSkill.skill.attackDisntance);
        if (colliders == null || colliders.Length == 0) return null;

        //Pick out all enemies from the collider list
        String[] attTags = currentUseSkill.skill.attckTargetTags;
        var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject);
       
        //In front, the tag is correct, the HP is greater than 0, and the enemy in front
        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;

        //Rank all enemies in ascending order of distance from the skill sender,
        CollectionHelper.OrderBy<GameObject, float>(array,
            p => Vector3.Distance(transform.position, p.transform.position));
        return array[0];
    }
}

2.CharacterSkillManager

Manage skill data, load all skill effect templates into the object pool;

Provide a skill release interface DeploySkill to CharacterSkillSystem;

Provide skill cooling calculation, reserve the remaining time interface for acquiring cd to UI, and whether the acquired skill is in cd;

[RequireComponent(typeof(CharacterSkillSystem))]
public class CharacterSkillManager : MonoBehaviour
{
    ///< summary > container for managing all skills < / summary >
    public List<SkillData> skills = new List<SkillData>();

    ///< summary > skill owner < / summary >
    private CharacterStatus chStatus = null;

    private SkillData curSkill;
	
    //Add skill data
    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);
    }

    //Initialize skill data (what skills do you have)
    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)
        {
            //Dynamically load skill effect preform / / Resources/Skill -- skill effect preform 
            if (item.skillPrefab == null && !string.IsNullOrEmpty(item.skill.prefabName))
                item.skillPrefab = LoadFxPrefab("Skill/" + item.skill.prefabName);
            
            //Resources/Skill/HitFx skill damage effect prefab
            if (item.hitFxPrefab == null && !string.IsNullOrEmpty(item.skill.hitFxName))
                item.hitFxPrefab = LoadFxPrefab("Skill/" + item.skill.hitFxName);
        }
    }

    //Load the effect preform into the object pool for future use
    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;
    }

    //Preparation skills
    public SkillData PrepareSkill(int id)
    {
        //Find the skill with the corresponding ID from the skill container
        var skillData = skills.Find(p => p.skill.skillID == id);
        if (skillData != null && //Skills found
            chStatus.SP >= skillData.skill.costSP && //Check if the role SP is able to use this skill
            skillData.coolRemain == 0) //And this skill has been cooled down
        {
            skillData.Owner = gameObject;
            return skillData;
        }

        return null;
    }

    //Release skills
    public void DeploySkill(SkillData skillData)
    {
        //Start cooling timing
        StartCoroutine(CoolTimeDown(skillData));

        //A frame of animation triggers skill special effects. Here is a delayed call method, which uses the percentage of animation time to solve the problem of special effect release time
        if (skillData.skill.delayAnimaTime != 0)
        {
            curSkill = skillData;
            Invoke("DelayDeploySkill", skillData.skill.delayAnimaTime);
            return;
        }
        
        GameObject tempGo = null;
        //Create skill preform + offset of creation position
        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);
       	//Skills have launch points
        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;

        //Find the skill release object from the preform object 
        var deployer = tempGo.GetComponent<SkillDeployer>();
        if (deployer == null)
            deployer = tempGo.AddComponent<SkillDeployer>();

        //Set skills to release - Focus
        deployer.skillData = skillData;
        //Call release method
        deployer.DeploySkill();
        
        //After the skill duration, the skill must be destroyed
        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);
        }
    }
	
    //Delayed release skill
    private void DelayDeploySkill()
    {
        GameObject tempGo = null;
        //Create skill preform + offset of creation position
        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);

        //Find the skill release object from the preform object 
        var deployer = tempGo.GetComponent<SkillDeployer>();
        if (deployer == null)
            deployer = tempGo.AddComponent<SkillDeployer>();

        //Set skills to release
        deployer.skillData = curSkill;
        //Call release method
        deployer.DeploySkill();

        //After the skill duration, the skill must be destroyed
        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);
        }
    }

    //Cooldown countdown
    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;
    }

    //Gets the remaining time (in seconds) for the cooldown countdown
    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

Mount on the skill effect and execute the impact of the skill on the releaser (consume MP and refresh MPUI);

Perform damage calculation on the hit target, load injury effects, add debuff, etc;

Injury trigger is divided into collision trigger and target selector selected trigger;

Key points drawn above:

While assigning a value to the skillData attribute in the skill releaser, create a target selector and assign a value to the CharacterStatrus field;

There are many pits in the middle:

1. To refresh the enemy's Avatar display, you must set the display level at the lowest level of the UI and set other UI positions. You can't set Active. Disabling the buff countdown calculation will fail. You can also manage the buff countdown separately;

2. It is detected that there is a refresh buff time for the same buff;

3. Multi segment damage. For each segment of damage, the attack target should be re detected, and there are buff s such as repulsion;

4. The injury calculation is written separately to facilitate modification;

5. For the skills of ballistic and collision trigger damage, the mounting point of the hit special effect should not be HitFxPos, but the contact point of the collision. However, the collision with the trigger can not return the coordinates of the collision point, so the ray detection is done again; However, there will be new problems. The ray detection has only one line and no volume, which will cause the edge collision, but the ray is not detected, but the collision has been triggered;

The processing is done here. The collision effect is generated in HitFxPos when the ray is not detected;

You can try setting HitFxPos in the front of the skill effect to set the position of the hit effect;

public class SkillDeployer : MonoBehaviour
{
    private SkillData m_skillData;

    ///< summary > enemy selection, algorithm for selecting target < / summary >
    public IAttackSelector attackTargetSelector;

    private DamageMode damageMode;

    //Sender
    private CharacterStatus status;

    ///< summary > skills to release < / 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 > skill release < / summary >
    public virtual void DeploySkill()
    {
        if (m_skillData == null) return;
        //Impact on oneself
        SelfImpact(m_skillData.Owner);

        //Perform damage calculation
        if (damageMode != 0) 
            StartCoroutine(ExecuteDamage());
    }

    //Perform damage calculation
    protected virtual IEnumerator ExecuteDamage()
    {
        //According to the duration and interval between two injuries,
        float attackTimer = 0; //Duration of attack
        
        ResetTargets();
        if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
        {
            //Debug.Log(skillData.attackTargets[0].name);
            foreach (var item in skillData.attackTargets)
            {
                //Refresh enemy avatar display
                CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
                GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
                MonsterMgr.I.HideAllEnemyPortraits();
                uiPortrait.SetActive(true);
                uiPortrait.transform.SetAsLastSibling();
                
                //Add buff
                foreach (var buff in skillData.skill.buffType)
                {
                    //Add bufficon
                    targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
                    
                    //The buff has been refreshed
                    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;
                    }

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

        do
        {
            //Select the attack target through the selector
            ResetTargets();
            if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
            {
                //Debug.Log(skillData.attackTargets[0].name);
                foreach (var item in skillData.attackTargets)
                {
                    //Impact on the enemy
                    TargetImpact(item);
                }
            }

            yield return new WaitForSeconds(skillData.skill.damageInterval);
            attackTimer += skillData.skill.damageInterval;
            //Calculate the damage value
        } 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>();

        //Hit calculation
        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;
            }
        }

        //The skill damage of common attack is 0; Skill has fixed damage * level bonus + common attack damage
        var damageVal = status.damage * (1000 / (1000 + goStatus.defence)) +
                        skillData.skill.damage * (1 + skillData.level * skillData.skill.damageRatio);
        return damageVal;
    }

    ///Impact on the enemy nag
    public virtual void TargetImpact(GameObject goTarget)
    {
        //Injury effects
        if (skillData.hitFxPrefab != null)
        {
            //Find the hanging point of the hit effect
            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);
        }

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

	//Impact trigger target
    public virtual void TargetImpact(GameObject goTarget, Collider collider)
    {
        //Enemy buff
        foreach (var buff in skillData.skill.buffType)
        {
            //The buff has been refreshed
            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;

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


        //Injury effects
        if (skillData.hitFxPrefab != null)
        {
            //Find the hanging point of the hit effect, collide but no ray point is detected, and generate the hit effect at 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);
            }
        }

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

    ///Impact on oneself
    public virtual void SelfImpact(GameObject goSelf)
    {
        //Released by: consumed SP
        var chStaus = goSelf.GetComponent<CharacterStatus>();
        if (chStaus.SP != 0)
        {
            chStaus.SP -= m_skillData.skill.costSP;
            chStaus.uiPortrait.RefreshHpMp();
            //add+2 magic bar update
        }
    }

    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();
                    
                    //Add buff
                    foreach (var buff in skillData.skill.buffType)
                    {
                        //Add bufficon
                        targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
                    }
                    
                    TargetImpact(other.gameObject, other);
                }
                else
                {
                    //Select the attack target through the selector
                    IAttackSelector selector = new CircleAttackSelector();
                    selector.SelectTarget(m_skillData, transform);
                    if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
                    {
                        foreach (var item in skillData.attackTargets)
                        {
                            //Refresh enemy avatar display
                            CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
                            GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
                            MonsterMgr.I.HideAllEnemyPortraits();
                            uiPortrait.SetActive(true);
                            uiPortrait.transform.SetAsLastSibling();
                            
                            //Add buff
                            foreach (var buff in skillData.skill.buffType)
                            {
                                //Add bufficon
                                targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
                            }

                            //Impact on the enemy
                            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;

                    //Find the hanging point of the hit effect
                    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");
    }
}

Summary

So far, all skill logic is over; The next section describes the buff system and UI display;

Tags: Unity

Posted on Thu, 11 Nov 2021 06:15:49 -0500 by snrecords