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 = new List(); //技能激活事件
    public List animaEvent = new List(); //技能的动画事件

    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 skillActions = new List(); //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 _skillActionName = new List();
    static Dictionary _skillDic = new Dictionary();

    [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(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 list = new List();
            list.Add("-------请选择------");
            list.AddRange(_skillActionName);
            selectIdx = EditorGUILayout.Popup("选择行为", selectIdx, list.ToArray());
            if (GUILayout.Button("添加行为"))
            {
                AddSkillAction(_curItem, list[selectIdx]);
            }
            GUILayout.EndHorizontal();
        }
    }

    /// 
    /// 添加技能
    /// 
    /// 技能id
    /// 是否创建新的资源
    static TreeViewItem AddSkill(int newSkillId, bool isCreateAsset = true)
    {
        Skill skill = ScriptableObject.CreateInstance();
        _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;
    }

    /// 
    /// 添加事件
    /// 
    /// 父节点
    /// 事件名
    /// 是否创建新的资源
    /// 
    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;
    }

    /// 
    /// 添加技能action
    /// 
    /// 父节点
    /// action名
    /// 是否创建新资源
    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 skills = new List();
        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


你可能感兴趣的:(Editor,unity)