Unity - skill system

Keywords: Unity

Skill system (I)

1, Demo presentation

2, Function introduction

It integrates skills, cooling, buff, UI display, countdown, animation, etc;

Skill type: Ballistic skill, animation event adopts delayed calling skill according to the number of frames, user-defined release position (offset and launch point), buff type skill (self gain buff, enemy gain reduction buff, such as adding defense and poison);

Skill damage judgment: collision judgment, circle judgment (user-defined center and radius), sector (angle and radius), linear (length and width), and can be released only after selecting the target;

Skill damage support multi-stage;

Buff type: burning, decelerating, electrifying, vertigo, poisoning, repulsion, flying, pulling; Gain: regenerate blood and increase defence;

3, Introduction to tools

CollectionHelper - array tool, generic, can pass in array and conditional delegate, return all qualified objects in the array, and sorting function;

TransformHelper -- recursively find all child nodes under the specified parent node and return the found target;

SingletonMono -- inherits the singleton of monobehavior;

GameObjectPool -- object pool

DamagePopup - blood loss value display

4, Base class

1.Skill

Skill data class: all skill data that can be imported externally are placed in this class so that data can be imported externally;

Due to the test demo, I wrote another SkillTemp class, which inherits ScriptaleObject to facilitate filling in test data;

/// <summary>
///Skill type, can be superimposed
/// </summary>
public enum DamageType
{
    Bullet = 4,             //Special effect particle collision damage
    None = 8,               //No damage, unused, none, optional
    Buff = 32,              //buff skill
    
    //either-or
    FirePos = 128,          //Point with launch position
    FxOffset = 256,         //Transmit offset, no offset, offset 0
    
    //One out of four
    Circle = 512,          //Circle decision
    Sector = 1024,         //Sector determination
    Line = 4096,           //Linear decision
    Select = 8192,         //Select to release
}

DamageType is used to determine the behavior of skills. The assignment is a multiple of 2. And or non can be used to reduce the number of variables;

Later, it was found that it was OK to use List directly, and the later skills used List to store superposition;

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

    ///< summary > skill type, which can be spliced < / summary > >
    public DamageType[] damageType;
}

Inheriting ScriptableObject, you can right-click to create a skill template and edit it directly in the inspector interface;

2.SkillData

The Skill class is combined. Based on the Skill class, more data that cannot be transferred externally is added;

For example, the reference of skill effects, the reference of skill owners, and the storage of dynamic data such as skill attack target objects used to transfer between skill modules and skill level cooling;

public class SkillData
{
    [HideInInspector] public GameObject Owner;
   
    ///< summary > skill data < / summary >
    [SerializeField]
    public Skill skill;

    ///< summary > skill level < / summary >
    public int level;
    
    ///< summary > cooling remaining < / summary >
    [HideInInspector]
    public float coolRemain;
    
    ///< summary > attack target < / summary >
    [HideInInspector] public GameObject[] attackTargets;

    ///< summary > activate < / summary >
    [HideInInspector]
    public bool Activated;

    ///< summary > skill prefabrication object < / summary >
    [HideInInspector] 
    public GameObject skillPrefab;
    
    [HideInInspector] 
    public GameObject hitFxPrefab;
}

3.CharacterStatus

To be exact, this class does not belong to the skill system. It is used to access character attribute data, and provide interfaces such as injury and refreshing UI bar;

At the same time, this class stores the hit special effect mounting point HitFxPos, firing point FirePos, select Mesh or special effect object selected, the damage value appears at hudPos, and its head is like the blood bar UI object uiPortrait;

It is best for heroes and enemies to write a class separately to inherit the base class, but this class is enough for testing;

public class CharacterStatus : MonoBehaviour
{
    ///< summary > Life < / summary >
    public float HP = 100;
    ///< summary > Life < / summary >
    public float MaxHP=100;
    ///< summary > current magic < / summary >
    public float SP = 100;
    ///< summary > maximum magic < / summary >
    public float MaxSP =100;
    ///< summary > damage base < / summary >
    public float damage = 100;
    ///< summary > hit < / summary >
    public float hitRate = 1;
    ///< summary > dodge < / summary >
    public float dodgeRate = 1;
    ///< summary > defense < / summary >  
    public float defence = 10f;
    ///< summary > main skill attack distance is used to set the attack range of AI and the distance from the target. Attack within this range < / summary >
    public float attackDistance = 2;
    ///< summary > the hanging point of the hit effect is named 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);
            //Store all uiportarits in a single instance
            MonsterMgr.I.AddEnemyPortraits(uiPortrait);
        }
        uiPortrait.cstatus = this;
        //Update blood blue bar
        uiPortrait.RefreshHpMp();
        
        damagePopup = Resources.Load<GameObject>("HUD");
      	//Initialization data
        selected = TransformHelper.FindChild(transform, "Selected").gameObject;
        HitFxPos = TransformHelper.FindChild(transform, "HitFxPos");
        FirePos = TransformHelper.FindChild(transform, "FirePos");
        hudPos = TransformHelper.FindChild(transform, "HUDPos");
    }
    
    ///< summary > hit template method < / summary >
    public virtual void OnDamage(float damage, GameObject killer,bool isBuff = false)
    {
        //Applied injury
        var damageVal = ApplyDamage(damage, killer);
        
        //Apply PopDamage
        DamagePopup pop = Instantiate(damagePopup).GetComponent<DamagePopup>();
        pop.target = hudPos;
        pop.transform.rotation = Quaternion.identity;
        pop.Value = damageVal.ToString();
        
        //ApplyUI portrait
        if (!isBuff)
        {
            uiPortrait.gameObject.SetActive(true);
            uiPortrait.transform.SetAsLastSibling();
            uiPortrait.RefreshHpMp();
        }
    }

    ///< summary > apply damage < / summary >
    public virtual float ApplyDamage(float damage, GameObject killer)
    {
        HP -= damage;
        //Application death
        if (HP <= 0)
        {
            HP = 0;
            Destroy(killer, 5f);
        }
        
        return damage;
    }
}

4.IAttackSelector

The target selector interface defines only one method to select qualified targets and return;

//The policy pattern abstracts the selected algorithm
///< summary > attack target selection algorithm < / summary >
public interface IAttackSelector
{
    ///< summary > target selection algorithm < / summary >
    GameObject[] SelectTarget(SkillData skillData, Transform skillTransform);
}

LineAttackSelector,CircleAttackSelector,SectorAttackSelector, linear, circular, sector target selector, inheriting the interface;

Only one CircleAttackSelector is displayed;

class CircleAttackSelector : IAttackSelector
{
    public GameObject[] SelectTarget(SkillData skillData, Transform skillTransform)
    {
        //Send a spherical ray and find all the colliders
        var colliders = Physics.OverlapSphere(skillTransform.position, skillData.skill.attackDisntance);
        if (colliders == null || colliders.Length == 0) return null;

        //Get all gameobject objects through collision body
        String[] attTags = skillData.skill.attckTargetTags;
        var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject);
      	//Select those who can attack and those whose HP is greater than 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;
        //Decide how many enemy objects to return according to whether the skill is single or group attack
        if (skillData.skill.attackNum == 1)
        {
            //Rank all enemies in ascending order of distance from the skill sender,
            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;
    }
}

There is a problem here. The target selector of a skill will be called every time the skill is released, so it will be created repeatedly, but in fact, this is just a method;

Solution: use the factory to cache the target selector;

//Simple factory  
//Create enemy selector
public class SelectorFactory
{
    //Attack target selector cache
    private static Dictionary<string, IAttackSelector> cache = new Dictionary<string, IAttackSelector>();

    public static IAttackSelector CreateSelector(DamageMode mode)
    {
        //Create without cache
        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);
        }

        //Get the created selector object from the cache
        return cache[mode.ToString()];
    }
}

Summary

For all base classes, this is the only data prepared in the early stage. In addition, if you want to make the Demo more experiential, you also need character control and the camera follows the script;

Then comes the skill management system, skill releaser, etc;

Posted by Paws on Wed, 10 Nov 2021 11:30:39 -0800