untiy 技能系統及編輯器

項目已經被解散了,但是項目中實現的技能系統真的是一套非常優秀的系統,可以脫離配置表,實現高度靈活配置,只要把基本的行爲實現了,就可以讓策劃自由組合,設計出不同的技能,完全不用程序配合,這裏技能系統大概實現一遍,子彈系統和buff系統大同小異,只是事件觸發的時機不一樣

照例先看技能編輯器效果圖


一、設計原理

unity的動畫系統在播放的時候在指定的時間觸發一些指定的事件,再由這些事件購成整個技能

二、實現過程

首先我們新建一個Skill.cs,讓其繼承ScriptableObject,這樣可以把技能數據保存成asset文件

具體代碼如下

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Skill : ScriptableObject
{
    public Skill() :
        base()
    {
    }

    public int skillId; //技能id
    public string desc = ""; //技能描述
    [HideInInspector]
    public CenterController centerController;
    public List<ActiveEvent> activeEvent = new List<ActiveEvent>(); //技能激活事件
    public List<AnimaEvent> animaEvent = new List<AnimaEvent>(); //技能的動畫事件

    private bool _isExecute = false; //是否在執行

    //使用技能
    public void Use()
    {
        //每次使用完都要重置一次
        Reset();
        _isExecute = true;
        for (int i = 0; i < activeEvent.Count; ++i)
        {
            activeEvent[i].OwerSkill = this;
            activeEvent[i].Execute();
        }

        for (int i = 0; i < animaEvent.Count; ++i)
        {
            animaEvent[i].OwerSkill = this;
        }
    }

    //更新action的Update邏輯
    public void Update()
    {
        if (_isExecute == false)
        {
            return;
        }

        if (centerController.statusController.IsSameStatus())
        {
            for (int i = 0; i < animaEvent.Count; ++i)
            {
                if(animaEvent[i].isTrigger)
                {
                    continue;
                }
                //動畫播放到指定的時間,觸發事件
                if (centerController.statusController.GetNormalizedTime() >= animaEvent[i].normalTime)
                {
                    animaEvent[i].Execute();
                }
            }

            for (int i = 0; i < activeEvent.Count; ++i)
            {
                if (activeEvent[i].isTrigger)
                {
                    activeEvent[i].Update();
                }
            }

            for (int i = 0; i < animaEvent.Count; ++i)
            {
                if (animaEvent[i].isTrigger)
                {
                    animaEvent[i].Update();
                }
            }
        }
    }

    //更新action的FixedUpdate邏輯
    public void FixedUpdate()
    {
        if (_isExecute == false)
        {
            return;
        }

        if (centerController.statusController.IsSameStatus())
        {
            for (int i = 0; i < activeEvent.Count; ++i)
            {
                if (activeEvent[i].isTrigger)
                {
                    activeEvent[i].FixedUpdate();
                }
            }

            for (int i = 0; i < animaEvent.Count; ++i)
            {
                if (animaEvent[i].isTrigger)
                {
                    animaEvent[i].FixedUpdate();
                }
            }
        }        
    }

    //正常使用完成
    public void Finish()
    {
        _isExecute = false;
        for (int i = 0; i < activeEvent.Count; ++i)
        {
            activeEvent[i].Finish();
        }

        for (int i = 0; i < animaEvent.Count; ++i)
        {
            animaEvent[i].Finish();
        }
    }

    //重置技能數據
    public void Reset()
    {
        _isExecute = false;

        for (int i = 0; i < activeEvent.Count; ++i)
        {
            activeEvent[i].isTrigger = false;
            activeEvent[i].Reset();
        }

        for (int i = 0; i < animaEvent.Count; ++i)
        {
            animaEvent[i].isTrigger = false;
            animaEvent[i].Reset();
        }
    }

    //被打斷
    public void Interrupt()
    {
        Finish();
    }
}


同理,我們新建一個SkillEvent.cs,具體代碼如下

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class SkillEvent : ScriptableObject
{
    public SkillEvent()
        :base()
    {
    }

    public bool isTrigger = false; //是否已經被觸發了    
    public List<SkillAction> skillActions = new List<SkillAction>(); //action列表
    protected Skill _skill; //擁有該事件的技能
    public Skill OwerSkill
    {
        set { _skill = value; }
    }

    //執行
    public virtual void Execute()
    {
        isTrigger = true;
        for (int i = 0; i < skillActions.Count; ++i)
        {
            skillActions[i].OwerSkill = _skill;
            skillActions[i].Execute();
        }
    }

    //Update由skill調用
    public virtual void Update()
    {
        for (int i = 0; i < skillActions.Count; ++i)
        {
            if (skillActions[i].isDurative)
            {
                skillActions[i].Update();
            }
        }
    }

    //FixedUpdate由skill調用
    public virtual void FixedUpdate()
    {
        for (int i = 0; i < skillActions.Count; ++i)
        {
            if (skillActions[i].isDurative)
            {
                skillActions[i].FixedUpdate();
            }
        }
    }

    //完成,把所有的技能action都執行完畢
    public virtual void Finish()
    {
        for (int i = 0; i < skillActions.Count; ++i)
        {
            skillActions[i].Finish();
        }
    }

    //重置數據
    public virtual void Reset()
    {
        for (int i = 0; i < skillActions.Count; ++i)
        {
            skillActions[i].Reset();
        }
    }
}

animator在播放的時候可以觸發很多事件,所以我們再定義一個ActiveEvent和AnimaEvent,並且都繼承SkillEvent,ActiveEvent主要是技能激活的時間觸發,最主要是用來播放動畫,因爲後面的事件都是根據動畫時間來觸發,AnimaEvent則是在指定的時間觸發

具體代碼如下

ActiveEvent.cs

public class ActiveEvent : SkillEvent
{
    //激活事件,技能激活馬上執行
    public ActiveEvent():
        base()
    {
    }
}

AnimaEvent.cs

public class AnimaEvent: SkillEvent
{
    //動畫事件,動畫執行到指定的時間再執行
    public AnimaEvent()
        : base()
    {
    }

    public float normalTime = 0.0f; //觸發的時間
}

上面這些都是動畫觸發一些事件,但是這些事件被觸發了之後具體行爲還沒定義,所以下面我們定義一個SkillAction,其他所有的行爲都繼承它

具體代碼如下

SkillAction.cs

using UnityEngine;
using System.Collections;

public class SkillAction : ScriptableObject
{
    public bool isDurative = false; //該action是否是技續性的
    protected Skill _skill; //擁有該action的技能
    public Skill OwerSkill
    {
        set { _skill = value; }
    }

    public virtual void Execute()
    {
    }

    public virtual void Finish()
    {
    }

    public virtual void Update()
    {
    }

    public virtual void FixedUpdate()
    {
    }

    public virtual void Reset()
    {
    }
}

由於我們的技能事件都是都動畫時間觸發的,所以最重要一個action就是播放動畫

具體代碼如下

PlayAnimator.cs

using UnityEngine;
using System.Collections;

public class PlayAnimator : SkillAction 
{
    public PlayAnimator()
        :base()
    {
    }

    //要播放的動畫
    public Status name = Status.Idle;

    public override void Execute()
    {
        _skill.centerController.statusController.Play(name);
    }
}


還有一個就是技能播放完後回家初始狀態

FinishSkill.cs

using System.Collections;
public class FinishSkill : SkillAction
{
    public FinishSkill():
        base()
    {
    }

    //動畫播放的時間
    public float normaledTime = 1.0f;

    private Task _waitingAnimatorStop = null;

    public override void Execute()
    {
        if (_waitingAnimatorStop != null)
        {
            _waitingAnimatorStop.Stop();
        }
        _waitingAnimatorStop = new Task(WaitingAnimatorStop());
    }

    private IEnumerator WaitingAnimatorStop()
    {
        while (true)
        {
            if (_skill.centerController.statusController.GetNormalizedTime() >= normaledTime)
            {
                EndSkill();
                break;
            }
            yield return 0;
        }
        yield return 0;
    }

    private void EndSkill()
    {
        if (_waitingAnimatorStop != null)
        {
            _waitingAnimatorStop.Stop();
            _waitingAnimatorStop = null;
        }
        _skill.Finish();
        //放完技能自動回到idle
        _skill.centerController.statusController.Play(Status.Idle);
    }

    public override void Reset()
    {
        if (_waitingAnimatorStop != null)
        {
            _waitingAnimatorStop.Stop();
            _waitingAnimatorStop = null;
        }
    }
}

其他action照着這兩個添加就可以了


最後我們再回到技能編輯器

我們的技能編輯器用一個樹形插件,叫treeviewcontrol,但是要修改一下它的源碼,這裏不細說,最後會附上源碼,你也可以不用它,編輯器主要是爲了方便我們創建技能數據,你用其他的也可以,只要覺得方便就行了

技能編輯器主要是用來創建一些asset文件,然後把技能數據保存進去

具體代碼如下

SkillEditor.cs 

using UnityEngine;
using System.Collections;
using UnityEditor;
using System.Reflection;
using System;
using System.IO;
using System.Collections.Generic;

public enum ItemType
{
    None,
    Root,
    SkillEvent,
    SkillAction
}

public enum SkillEventType
{
    ActiveEvent,
    AnimaEvent 
}

public class SkillBase
{
    public ItemType type = ItemType.None;
    public string resPath = "";
    public int skillId = 0;
}

public class SkillEditor : EditorWindow
{
    static TreeViewControl m_treeViewControl = null;
    static TreeViewItem _root = null;
    static TreeViewItem _curItem = null;
    static string FixPath = "Assets/Resources/FightData/Skill/"; //技能數據的位置
    static string ResPath = "FightData/Skill/";
    static List<string> _skillActionName = new List<string>();
    static Dictionary<int, Skill> _skillDic = new Dictionary<int, Skill>();

    [MenuItem("GameEditor/Skill Editor")]
    public static void ShowSkillTreeViewPanel()
    {
        _skillDic.Clear();
        GetSkillActionName();
        GetSkillData();
        CreateTreeView();
        RefreshPanel();
    }

    static SkillEditor m_instance = null;

    public static SkillEditor GetPanel()
    {
        if (null == m_instance)
        {
            m_instance = EditorWindow.GetWindow<SkillEditor>(false, "技能編輯器", false);
        }
        return m_instance;
    }

    public static void RefreshPanel()
    {
        SkillEditor panel = GetPanel();
        panel.Repaint();
    }

    static void CreateTreeView()
    {     
        m_treeViewControl = TreeViewInspector.AddTreeView();
        m_treeViewControl.DisplayInInspector = false;
        m_treeViewControl.DisplayOnGame = false;
        m_treeViewControl.DisplayOnScene = false;
        m_treeViewControl.X = 600;
        m_treeViewControl.Y = 500;       

        _root = m_treeViewControl.RootItem;
        _root.Header = "所有技能";
        SkillBase data1 = new SkillBase();
        data1.type = ItemType.None;
        _root.DataContext = data1;
        _curItem = _root;
        AddEvents(_root);

        CreateSkillItem();
    }

    static void AddEvents(TreeViewItem item)
    {
        AddHandlerEvent(out item.Selected);
    }

    public static void Handler(object sender, System.EventArgs args)
    {
        _curItem = sender as TreeViewItem;
        Selection.activeObject = Resources.Load((_curItem.DataContext as SkillBase).resPath);
    }

    static void AddHandlerEvent(out System.EventHandler handler)
    {
        handler = new System.EventHandler(Handler);
    }

    void OnEnable()
    {
        wantsMouseMove = true;
    }

    int skillId = 0; //技能id
    int selectIdx = 0; //選擇的事件
    void OnGUI()
    {
        if (null == m_treeViewControl)
        {
            return;
        }

        if (_curItem == null)
        {
            return;
        }

        wantsMouseMove = true;
        if (null != Event.current &&
            Event.current.type == EventType.MouseMove)
        {
            Repaint();
        }
        m_treeViewControl.DisplayTreeView(TreeViewControl.DisplayTypes.USE_SCROLL_VIEW);

        if ((_curItem.DataContext as SkillBase).type == ItemType.None)
        {
            skillId = EditorGUILayout.IntField("技能id:",skillId);
            GUILayout.BeginVertical();
            if (GUILayout.Button("創建技能"))
            {
                AddSkill(skillId);
            }
            GUILayout.EndVertical();
        }
        else if ((_curItem.DataContext as SkillBase).type == ItemType.Root)
        {
            GUILayout.BeginHorizontal();
            string[] list = new string[] { "-------請選擇------", SkillEventType.AnimaEvent.ToString() };
            selectIdx = EditorGUILayout.Popup("選擇事件", selectIdx, list);
            if (GUILayout.Button("添加事件"))
            {
                AddSkillEventNode(_curItem, list[selectIdx]);
            }
            GUILayout.EndHorizontal();
        }
        else if ((_curItem.DataContext as SkillBase).type == ItemType.SkillEvent)
        {
            GUILayout.BeginHorizontal();
            List<string> list = new List<string>();
            list.Add("-------請選擇------");
            list.AddRange(_skillActionName);
            selectIdx = EditorGUILayout.Popup("選擇行爲", selectIdx, list.ToArray());
            if (GUILayout.Button("添加行爲"))
            {
                AddSkillAction(_curItem, list[selectIdx]);
            }
            GUILayout.EndHorizontal();
        }
    }

    /// <summary>
    /// 添加技能
    /// </summary>
    /// <param name="newSkillId">技能id</param>
    /// <param name="isCreateAsset">是否創建新的資源</param>
    static TreeViewItem AddSkill(int newSkillId, bool isCreateAsset = true)
    {
        Skill skill = ScriptableObject.CreateInstance<Skill>();
        _skillDic[skill.skillId] = skill;
        skill.skillId = newSkillId;
        if (isCreateAsset)
        {
            AssetEditor.CreateAsset(skill, FixPath + newSkillId, "Skill");
        }

        TreeViewItem skillItem = _root.AddItem(newSkillId.ToString());
        SkillBase data = new SkillBase();
        data.type = ItemType.Root;
        data.resPath = ResPath + newSkillId + "/Skill";
        data.skillId = newSkillId;
        skillItem.DataContext = data;
        AddEvents(skillItem);
        if (isCreateAsset)
        {
            TreeViewItem evtItem = AddSkillEventNode(skillItem, SkillEventType.ActiveEvent.ToString());
            if (evtItem != null)
            {
                AddSkillAction(evtItem, "PlayAnimator");
                AddSkillAction(evtItem, "FinishSkill");
            }
            AddSkillEventNode(skillItem, SkillEventType.AnimaEvent.ToString());
        }

        return skillItem;
    }

    /// <summary>
    /// 添加事件
    /// </summary>
    /// <param name="item">父節點</param>
    /// <param name="name">事件名</param>
    /// <param name="isCreateAsset">是否創建新的資源</param>
    /// <returns></returns>
    static TreeViewItem AddSkillEventNode(TreeViewItem item, string name, bool isCreateAsset = true)
    {
        if (name == String.Empty)
        {
            return null;
        }
        Skill skill = RegetditSkill(item);
        if (skill == null)
        {
            return null;
        }
        string evtName = name;
        Assembly ass = typeof(SkillEvent).Assembly;
        string[] nameList = name.Split('_');
        System.Type type = ass.GetType(nameList[0]);
        SkillEvent evt = System.Activator.CreateInstance(type) as SkillEvent;
        SkillBase evtData = new SkillBase();
        if (evt is ActiveEvent)
        {
            if (isCreateAsset)
            {
                skill.activeEvent.Add(evt as ActiveEvent);
            }         
        }
        else
        {
            if (skill.animaEvent.Count > 0)
            {
                evtName = name + "_" + skill.animaEvent.Count;
            }
            if (isCreateAsset)
            {
                skill.animaEvent.Add(evt as AnimaEvent);
            }          
        }
        if (isCreateAsset)
        {
            AssetEditor.CreateAsset(evt, FixPath + skill.skillId + "/" + evtName, evtName);
        }
        else
        {
            evtName = name;
        }
        TreeViewItem evtItem = item.AddItem(evtName);                   
        evtData.type = ItemType.SkillEvent;
        evtData.skillId = skill.skillId;
        evtData.resPath = ResPath + skill.skillId + "/" + evtName + "/" + evtName;
        evtItem.DataContext = evtData;
        AddEvents(evtItem);
        EditorUtility.SetDirty(skill);
        return evtItem;
    }

    /// <summary>
    /// 添加技能action
    /// </summary>
    /// <param name="item">父節點</param>
    /// <param name="name">action名</param>
    /// <param name="isCreateAsset">是否創建新資源</param>
    static void AddSkillAction(TreeViewItem item, string name, bool isCreateAsset = true)
    {
        SkillBase data = item.DataContext as SkillBase;
        SkillBase actData = new SkillBase();
        Skill skill = RegetditSkill(item);
        SkillEvent evt = Resources.Load(data.resPath) as SkillEvent;
        if (skill == null || evt == null)
        {
            return;
        }
        Assembly ass = typeof(SkillAction).Assembly;
        string[] nameList = name.Split('_');
        System.Type type = ass.GetType(nameList[0]);
        SkillAction act = System.Activator.CreateInstance(type) as SkillAction;
        
        string actName = name;
        int num = 0;
        foreach (var act2 in evt.skillActions)
        {
            if (act2.name.StartsWith(name))
            {
                num++;
            }
        }
        if (num > 0)
        {
            actName = actName + "_" + num;
        }
        if (isCreateAsset)
        {
            evt.skillActions.Add(act);
            AssetEditor.CreateAsset(act, FixPath + skill.skillId + "/" + evt.name + "/SkillAction", actName);
        }
        else
        {
            actName = name;
        }
       
        TreeViewItem evtItem = item.AddItem(actName);
        actData.type = ItemType.SkillAction;
        actData.skillId = skill.skillId;
        actData.resPath = ResPath + skill.skillId + "/" + evt.name + "/SkillAction/" + actName;
        evtItem.DataContext = actData;
        AddEvents(evtItem);
        EditorUtility.SetDirty(evt);
        EditorUtility.SetDirty(skill);
    }

    void OnDestroy()
    {
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
    }

    static Skill RegetditSkill(TreeViewItem item)
    {
        SkillBase data = item.DataContext as SkillBase;
        Skill skill = null;
        if (_skillDic.ContainsKey(data.skillId))
        {
            skill = _skillDic[data.skillId];
        }
        else
        {
            skill = Resources.Load(data.resPath) as Skill;
            if (skill != null)
            {
                _skillDic[data.skillId] = skill;
            }
        }
        return skill;
    }

    static void CreateSkillItem()
    {
        List<Skill> skills = new List<Skill>();
        foreach (var node in _skillDic)
        {
            if (node.Value != null)
            {
                skills.Add(node.Value);
            }
        }
        for (int i = 0; i < skills.Count; ++i)
        {
            TreeViewItem skillItem = AddSkill(skills[i].skillId, false);
            if (skillItem == null)
            {
                continue;
            }
            for (int n = 0; n < skills[i].activeEvent.Count; n++)
            {
                TreeViewItem evtItem = AddSkillEventNode(skillItem, skills[i].activeEvent[n].name, false);
                for (int m = 0; m < skills[i].activeEvent[n].skillActions.Count; m++)
                {
                    AddSkillAction(evtItem, skills[i].activeEvent[n].skillActions[m].name, false);
                }
            }

            for (int n = 0; n < skills[i].animaEvent.Count; n++)
            {
                TreeViewItem evtItem = AddSkillEventNode(skillItem, skills[i].animaEvent[n].name, false);
                for (int m = 0; m < skills[i].animaEvent[n].skillActions.Count; m++)
                {
                    AddSkillAction(evtItem, skills[i].animaEvent[n].skillActions[m].name, false);
                }
            }
        }
    }

    static void GetSkillData()
    {
        try
        {
            DirectoryInfo parentFolder = new DirectoryInfo(FixPath);
            //遍歷文件夾
            foreach (DirectoryInfo folder in parentFolder.GetDirectories())
            {
                Skill skill = Resources.Load("FightData/Skill/" + folder.Name + "/Skill") as Skill;
                if (skill == null)
                {
                    continue;
                }
                _skillDic[skill.skillId] = skill;
            }  
        }
        catch(Exception e)
        {
            Debug.LogError(e);
        }
    }

    static void GetSkillActionName()
    {
        try
        {
            _skillActionName.Clear();
            int i = 0;
            string _skillActionPath = "Assets/Script/Skill/SkillEvent/SkillAction/";
            DirectoryInfo parentFolder = new DirectoryInfo(_skillActionPath);
            //遍歷文件夾
            foreach (var file in parentFolder.GetFiles())
            {
                if (file.Extension != ".cs" || file.Name == "SkillAction.cs")
                {
                    continue;
                }
                _skillActionName.Add(file.Name.Replace(file.Extension, ""));                 
                 ++i;
            }
        }
        catch (Exception e)
        {
            Debug.LogError(e);
        }
    }
}

至此整個技能系統已經設計完了,到底怎麼用呢?很簡單直接Resources.Load()把數據讀出就可以了,如果爲了熱更,也可以把數據打成assetbundle,具體的看自己的項目需求

最後附上整個工程源碼 https://github.com/caolaoyao/SkillEditor


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章