項目已經被解散了,但是項目中實現的技能系統真的是一套非常優秀的系統,可以脫離配置表,實現高度靈活配置,只要把基本的行爲實現了,就可以讓策劃自由組合,設計出不同的技能,完全不用程序配合,這裏技能系統大概實現一遍,子彈系統和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