技能系統(一)
一.Demo展示
二.功能介紹
整合了技能,冷卻,buff,UI顯示,倒數計時,動畫等;
技能型別:彈道技能,動畫事件根據幀數採用延遲呼叫技能,自定義釋放位置(偏移,發射點兩種),buff型別技能(自身增益buff,敵人減益buff,比如加防禦和毒);
技能傷害判定:碰撞判定,圓形判定(自定義圓心和半徑),扇形(角度和半徑),線性(長寬),選中目標才可釋放;
技能傷害支援多段;
Buff型別:燃燒,減速,感電,眩暈,中毒,擊退,擊飛,拉拽;增益:回血,加防禦;
三.工具類介紹
CollectionHelper——陣列工具,泛型,可以傳入陣列和條件委託,返回陣列中符合條件的所有物件,以及排序功能;
TransformHelper——遞迴查詢指定父節點下所有子節點,返回找到的目標;
SingletonMono——繼承了MonoBehaviour的單例;
四.基類
1.Skill
技能資料類,所有可以外部匯入的技能資料都放在這個類中,以便於可以外部匯入資料;
由於測試demo,我另外寫了一個SkillTemp類,繼承了ScriptaleObject,方便填寫測試資料;
/// <summary>
/// 技能型別,可疊加
/// </summary>
public enum DamageType
{
Bullet = 4, //特效粒子碰撞傷害
None = 8, //無傷害,未使用,為none可以不選
Buff = 32, //buff技能
//二選一
FirePos = 128, //有發射位置點
FxOffset = 256, //發射偏移,無偏移偏移量為0
//四選一
Circle = 512, //圈判定
Sector = 1024, //扇形判定
Line = 4096, //線性判定
Select = 8192, //選中才可釋放
}
DamageType用來確定技能的行為,賦值都是2的倍數,可以使用與或非來減少變數個數;
後來發現直接用List好像也行,後面的Buff就使用了List來儲存疊加的情況;
[CreateAssetMenu(menuName="Create SkillTemp")]
public class SkillTemp : ScriptableObject
{
public Skill skill = new Skill();
/// <summary>技能型別,可用 | 拼接</summary>>
public DamageType[] damageType;
}
繼承了ScriptableObject可以右鍵建立技能模板,直接在inspector介面編輯;
2.SkillData
組合了Skill類,在Skill類的基礎上,新增了更多的不可外部傳參的資料;
比如技能特效的引用,技能所有者引用,儲存技能攻擊目標物件用來在技能模組之間傳遞,以及技能等級冷卻等動態變化的資料;
public class SkillData
{
[HideInInspector] public GameObject Owner;
/// <summary>技能資料</summary>
[SerializeField]
public Skill skill;
/// <summary>技能等級</summary>
public int level;
/// <summary>冷卻剩餘</summary>
[HideInInspector]
public float coolRemain;
/// <summary>攻擊目標</summary>
[HideInInspector] public GameObject[] attackTargets;
/// <summary>是否啟用</summary>
[HideInInspector]
public bool Activated;
/// <summary>技能預製物件</summary>
[HideInInspector]
public GameObject skillPrefab;
[HideInInspector]
public GameObject hitFxPrefab;
}
3.CharacterStatus
準確來說這個類不屬於技能系統,他用來記錄人物屬性資料,以及提供受傷,重新整理UI條等介面;
同時這個類儲存著技能系統必須用到的受擊特效掛載點HitFxPos,發射點FirePos,選中Mesh或特效物體selected,傷害數值出現點hudPos,自身頭像血條UI物體uiPortrait;
最好是英雄和敵人單獨寫一個類繼承這個基類,但是測試的話這個類就夠用了;
public class CharacterStatus : MonoBehaviour
{
/// <summary>生命 </summary>
public float HP = 100;
/// <summary>生命 </summary>
public float MaxHP=100;
/// <summary>當前魔法 </summary>
public float SP = 100;
/// <summary>最大魔法 </summary>
public float MaxSP =100;
/// <summary>傷害基數</summary>
public float damage = 100;
///<summary>命中</summary>
public float hitRate = 1;
///<summary>閃避</summary>
public float dodgeRate = 1;
/// <summary>防禦</summary>
public float defence = 10f;
/// <summary>主技能攻擊距離 ,用於設定AI的攻擊範圍,與目標距離此範圍內發起攻擊</summary>
public float attackDistance = 2;
/// <summary>受擊特效掛點 掛點名為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);
//儲存所有的uiPortarit在單例中
MonsterMgr.I.AddEnemyPortraits(uiPortrait);
}
uiPortrait.cstatus = this;
//更新血藍條
uiPortrait.RefreshHpMp();
damagePopup = Resources.Load<GameObject>("HUD");
//初始化資料
selected = TransformHelper.FindChild(transform, "Selected").gameObject;
HitFxPos = TransformHelper.FindChild(transform, "HitFxPos");
FirePos = TransformHelper.FindChild(transform, "FirePos");
hudPos = TransformHelper.FindChild(transform, "HUDPos");
}
/// <summary>受擊 模板方法</summary>
public virtual void OnDamage(float damage, GameObject killer,bool isBuff = false)
{
//應用傷害
var damageVal = ApplyDamage(damage, killer);
//應用PopDamage
DamagePopup pop = Instantiate(damagePopup).GetComponent<DamagePopup>();
pop.target = hudPos;
pop.transform.rotation = Quaternion.identity;
pop.Value = damageVal.ToString();
//ApplyUI畫像
if (!isBuff)
{
uiPortrait.gameObject.SetActive(true);
uiPortrait.transform.SetAsLastSibling();
uiPortrait.RefreshHpMp();
}
}
/// <summary>應用傷害</summary>
public virtual float ApplyDamage(float damage, GameObject killer)
{
HP -= damage;
//應用死亡
if (HP <= 0)
{
HP = 0;
Destroy(killer, 5f);
}
return damage;
}
}
4.IAttackSelector
目標選擇器介面,只定義了一個方法,選擇符合條件的目標並返回;
//策略模式 將選擇演算法進行抽象
/// <summary>攻擊目標選擇演算法</summary>
public interface IAttackSelector
{
///<summary>目標選擇演算法</summary>
GameObject[] SelectTarget(SkillData skillData, Transform skillTransform);
}
LineAttackSelector,CircleAttackSelector,SectorAttackSelector線性,圓形,扇形目標選擇器,繼承該介面;
就只展示一個了CircleAttackSelector;
class CircleAttackSelector : IAttackSelector
{
public GameObject[] SelectTarget(SkillData skillData, Transform skillTransform)
{
//發一個球形射線,找出所有碰撞體
var colliders = Physics.OverlapSphere(skillTransform.position, skillData.skill.attackDisntance);
if (colliders == null || colliders.Length == 0) return null;
//通過碰撞體拿到所有的gameobject物件
String[] attTags = skillData.skill.attckTargetTags;
var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject);
//挑選出物件中能攻擊的,血量大於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;
//根據技能是單體還是群攻,決定返回多少敵人物件
if (skillData.skill.attackNum == 1)
{
//將所有的敵人,按與技能的發出者之間的距離升序排列,
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;
}
}
這裡有個問題,技能的目標選擇器每次釋放技能都會呼叫,因此會重複頻繁的建立,但其實這只是提供方法而已;
解決:使用工廠來快取目標選擇器;
//簡單工廠
//建立敵人選擇器
public class SelectorFactory
{
//攻擊目標選擇器快取
private static Dictionary<string, IAttackSelector> cache = new Dictionary<string, IAttackSelector>();
public static IAttackSelector CreateSelector(DamageMode mode)
{
//沒有快取則建立
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);
}
//從快取中取得建立好的選擇器物件
return cache[mode.ToString()];
}
}
小結
所有基類,前期準備資料只有這些,另外想Demo更有體驗感,還需要有角色控制,相機跟隨指令碼;
之後就是技能管理系統,技能釋放器等;