Unity技能系統(二)
Demo展示:
五.技能管理和釋放
1.CharacterSkillSystem
技能系統類,給外部(技能按鈕,按鍵)提供技能釋放方法;
技能釋放邏輯:
按順序判定條件,成立怎繼續,否則返回;
最終呼叫CharacterSkillManager中的DeploySkill方法;傳遞引數為SkillData;
提供了隨機技能方法;
/// <summary>
/// 角色系統
/// </summary>
[RequireComponent(typeof(CharacterSkillManager))]
public class CharacterSkillSystem : MonoBehaviour
{
//技能管理
public CharacterSkillManager chSkillMgr;
//角色狀態
private CharacterStatus chStatus;
//角色動畫
private Animator mAnimator;
//當前使用的技能
private SkillData currentUseSkill;
//當前攻擊的目標
private Transform currentSelectedTarget;
//初始化
public void Start()
{
mAnimator = GetComponent<Animator>();
chSkillMgr = GetComponent<CharacterSkillManager>();
chStatus = GetComponent<CharacterStatus>();
}
/// <summary>
/// 使用指定技能
/// </summary>
/// <param name="skillid">技能編號</param>
/// <param name="isBatter">是否連擊</param>
public void AttackUseSkill(int skillid, bool isBatter = false)
{
//如果是連擊,找當前技能的下一個連擊技能
if (currentUseSkill != null && isBatter)
skillid = currentUseSkill.skill.nextBatterId;
//準備技能
currentUseSkill = chSkillMgr.PrepareSkill(skillid);
if (currentUseSkill != null)
{
//選中釋放技能呼叫
if ((currentUseSkill.skill.damageType & DamageType.Select) == DamageType.Select)
{
var selectedTaget = SelectTarget();
if (currentUseSkill.skill.attckTargetTags.Contains("Player"))
selectedTaget = gameObject;
if (selectedTaget != null)
{
CharacterStatus selectStatus = null;
//修改成獲取characterStatus中的Selected節點設定隱藏;
if (currentSelectedTarget != null)
{
selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
selectStatus.selected.SetActive(false);
}
currentSelectedTarget = selectedTaget.transform;
selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
selectStatus.selected.SetActive(true);
//buff技能
if ((currentUseSkill.skill.damageType & DamageType.Buff) == DamageType.Buff)
{
foreach (var buff in currentUseSkill.skill.buffType)
{
//加bufficon
GameObject uiPortrait = selectStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();
selectStatus.uiPortrait.AddBuffIcon(buff, currentUseSkill.skill.buffDuration);
//已有該buff重新整理
bool exist = false;
var buffs = selectedTaget.GetComponents<BuffRun>();
foreach (var it in buffs)
{
if (it.bufftype == buff)
{
it.Reset();
exist = true;
break;
}
}
if (exist)
continue;
//新增新buff
var buffRun = selectedTaget.AddComponent<BuffRun>();
buffRun.InitBuff(buff, currentUseSkill.skill.buffDuration,
currentUseSkill.skill.buffValue, currentUseSkill.skill.buffInterval);
}
}
//轉向目標
//transform.LookAt(currentSelectedTarget);
chSkillMgr.DeploySkill(currentUseSkill);
mAnimator.Play(currentUseSkill.skill.animtionName);
}
}
else
{
chSkillMgr.DeploySkill(currentUseSkill);
mAnimator.Play(currentUseSkill.skill.animtionName);
}
}
}
/// <summary>
/// 隨機選擇技能
/// </summary>
public void RandomSelectSkill()
{
if (chSkillMgr.skills.Count > 0)
{
int index = UnityEngine.Random.Range(0, chSkillMgr.skills.Count);
currentUseSkill = chSkillMgr.PrepareSkill(chSkillMgr.skills[index].skill.skillID);
if (currentUseSkill == null) //隨機技能未找到或未冷卻結束
currentUseSkill = chSkillMgr.skills[0]; //用技能表中第一個(預設技能)做補充
}
}
//選擇目標
private GameObject SelectTarget()
{
//發一個球形射線,找出所有碰撞體
var colliders = Physics.OverlapSphere(transform.position, currentUseSkill.skill.attackDisntance);
if (colliders == null || colliders.Length == 0) return null;
//從碰撞體列表中挑出所有的敵人
String[] attTags = currentUseSkill.skill.attckTargetTags;
var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject);
//正前方,tag正確,血量大於0,處於正前方的敵人
array = CollectionHelper.FindAll<GameObject>(array,
p => Array.IndexOf(attTags, p.tag) >= 0
&& p.GetComponent<CharacterStatus>().HP > 0 &&
Vector3.Angle(transform.forward, p.transform.position - transform.position) <= 90);
if (array == null || array.Length == 0) return null;
//將所有的敵人,按與技能的發出者之間的距離升序排列,
CollectionHelper.OrderBy<GameObject, float>(array,
p => Vector3.Distance(transform.position, p.transform.position));
return array[0];
}
}
2.CharacterSkillManager
技能資料的管理,載入所有技能特效模板進入物件池;
給CharacterSkillSystem提供技能釋放介面DeploySkill;
提供技能冷卻計算,預留獲取cd剩餘時間介面給UI,以及獲取技能是否在cd中;
[RequireComponent(typeof(CharacterSkillSystem))]
public class CharacterSkillManager : MonoBehaviour
{
/// <summary>管理所有技能的容器</summary>
public List<SkillData> skills = new List<SkillData>();
/// <summary>技能的擁有者</summary>
private CharacterStatus chStatus = null;
private SkillData curSkill;
//新增技能資料
private void AddSkill(string path)
{
SkillTemp skTemp = Instantiate(Resources.Load<SkillTemp>(path));
Skill sk = LoadSkill(skTemp);;
SkillData skd = new SkillData();
skd.skill = sk;
skills.Add(skd);
}
//初始化技能資料(有什麼技能)
public void Start()
{
chStatus = GetComponent<CharacterStatus>();
AddSkill("Skill_1");
AddSkill("Skill_2");
AddSkill("Skill_3");
AddSkill("Skill_4");
AddSkill("Skill_5");
foreach (var item in skills)
{
//動態載入技能特效預製體 //Resources/Skill -- 技能特效預製體
if (item.skillPrefab == null && !string.IsNullOrEmpty(item.skill.prefabName))
item.skillPrefab = LoadFxPrefab("Skill/" + item.skill.prefabName);
//Resources/Skill/HitFx 技能傷害特效預製體
if (item.hitFxPrefab == null && !string.IsNullOrEmpty(item.skill.hitFxName))
item.hitFxPrefab = LoadFxPrefab("Skill/" + item.skill.hitFxName);
}
}
//將特效預製件載入到物件池,以備將來使用
private GameObject LoadFxPrefab(string path)
{
var key = path.Substring(path.LastIndexOf("/") + 1);
var go = Resources.Load<GameObject>(path);
GameObjectPool.I.Destory(
GameObjectPool.I.CreateObject(
key, go, transform.position, transform.rotation)
);
return go;
}
//準備技能
public SkillData PrepareSkill(int id)
{
//從技能容器中找出相應ID的技能
var skillData = skills.Find(p => p.skill.skillID == id);
if (skillData != null && //查詢到技能
chStatus.SP >= skillData.skill.costSP && //檢查角色SP是否夠使用該技能
skillData.coolRemain == 0) //且該技能已經冷卻結束
{
skillData.Owner = gameObject;
return skillData;
}
return null;
}
//釋放技能
public void DeploySkill(SkillData skillData)
{
//開始冷卻計時
StartCoroutine(CoolTimeDown(skillData));
//動畫某一幀觸發技能特效,這裡寫一個延遲呼叫的方法,使用動畫時間的百分解決特效釋放時間問題
if (skillData.skill.delayAnimaTime != 0)
{
curSkill = skillData;
Invoke("DelayDeploySkill", skillData.skill.delayAnimaTime);
return;
}
GameObject tempGo = null;
//建立技能預製體+建立位置的偏移
if ((skillData.skill.damageType & DamageType.FxOffset) == DamageType.FxOffset)
tempGo = GameObjectPool.I.CreateObject(skillData.skill.prefabName, skillData.skillPrefab,
transform.position + transform.forward * skillData.skill.fxOffset, transform.rotation);
//技能有發射點
else if ((skillData.skill.damageType & DamageType.FirePos) == DamageType.FirePos)
tempGo = GameObjectPool.I.CreateObject(skillData.skill.prefabName, skillData.skillPrefab,
chStatus.FirePos.position, chStatus.FirePos.rotation);
if(tempGo == null)
return;
//從預製體物件上找到技能釋放物件
var deployer = tempGo.GetComponent<SkillDeployer>();
if (deployer == null)
deployer = tempGo.AddComponent<SkillDeployer>();
//設定要釋放的技能————劃重點
deployer.skillData = skillData;
//呼叫釋放方法
deployer.DeploySkill();
//技能持續時間過後,技能要銷燬
if ((skillData.skill.damageType & DamageType.Bullet) != DamageType.Bullet)
{
if (skillData.skill.durationTime > 0)
GameObjectPool.I.Destory(tempGo, skillData.skill.durationTime);
else
GameObjectPool.I.Destory(tempGo, 0.5f);
}
}
//延遲釋放技能
private void DelayDeploySkill()
{
GameObject tempGo = null;
//建立技能預製體+建立位置的偏移
if ((curSkill.skill.damageType & DamageType.FxOffset) == DamageType.FxOffset)
tempGo = GameObjectPool.I.CreateObject(curSkill.skill.prefabName, curSkill.skillPrefab,
transform.position + transform.forward * curSkill.skill.fxOffset, transform.rotation);
else if ((curSkill.skill.damageType & DamageType.FirePos) == DamageType.FirePos)
tempGo = GameObjectPool.I.CreateObject(curSkill.skill.prefabName, curSkill.skillPrefab,
chStatus.FirePos.position, chStatus.FirePos.rotation);
//從預製體物件上找到技能釋放物件
var deployer = tempGo.GetComponent<SkillDeployer>();
if (deployer == null)
deployer = tempGo.AddComponent<SkillDeployer>();
//設定要釋放的技能
deployer.skillData = curSkill;
//呼叫釋放方法
deployer.DeploySkill();
//技能持續時間過後,技能要銷燬
if ((curSkill.skill.damageType & DamageType.Bullet) != DamageType.Bullet)
{
if (curSkill.skill.durationTime > 0)
GameObjectPool.I.Destory(tempGo, curSkill.skill.durationTime);
else
GameObjectPool.I.Destory(tempGo, 0.5f);
}
}
//冷卻時間倒數計時
public IEnumerator CoolTimeDown(SkillData skillData)
{
skillData.coolRemain = skillData.skill.coolTime;
while (skillData.coolRemain > 0)
{
yield return new WaitForSeconds(0.1f);
skillData.coolRemain -= 0.1f;
}
skillData.coolRemain = 0;
}
//取得冷卻倒數計時的剩餘時間(秒)
public float GetSkillCoolRemain(int id)
{
return skills.Find(p => p.skill.skillID == id).coolRemain;
}
private Skill LoadSkill(SkillTemp skillTemp)
{
Skill sk = skillTemp.skill;
int count = skillTemp.damageType.Length;
for (int i = 0; i < count; ++i)
{
sk.damageType = sk.damageType | skillTemp.damageType[i];
}
return sk;
}
}
3.SkillDeployer
掛載在技能特效上, 執行技能對釋放者造成的影響(消耗MP,重新整理MPUI);
對命中目標執行傷害計算,載入受傷特效新增debuff等;
傷害觸發分為碰撞觸發和目標選擇器選中觸發;
上面劃得重點:
給技能釋放器中skillData屬性賦值的同時,建立目標選擇器,給CharacterStatrus欄位賦值;
中間有很多坑點:
1.重新整理敵人頭像顯示,必須要設定顯示層級在ui的最下層,同時設定其他UI位置,不能設定Active,禁用buff倒數計時計算會失效,也可以將buff倒數計時單獨管理;
2.檢測已有相同buff存在重新整理buff時間;
3.多段傷害,每段傷害要重新檢測攻擊目標,有擊退等buff存在;
4.傷害計算單獨寫方法,方便修改;
5.彈道和碰撞觸發傷害的技能,受擊特效掛載點不應該是HitFxPos,而是碰撞的接觸點,然而使用觸發器碰撞沒辦法返回碰撞點座標,所以又做了射線檢測;但是又會存在新的問題,射線檢測只有一條線,沒有體積,會造成邊緣碰撞時射線未檢測到,卻已經觸發碰撞了;
這裡做了處理,射線未檢測到卻碰撞特效生成在HitFxPos;
可以自行嘗試一下在技能特效的前段設定HitFxPos來設定受擊特效的位置;
public class SkillDeployer : MonoBehaviour
{
private SkillData m_skillData;
///<summary>敵人選區,選擇目標的演算法</summary>
public IAttackSelector attackTargetSelector;
private DamageMode damageMode;
//發出者
private CharacterStatus status;
/// <summary> 要釋放的技能 </summary>
public SkillData skillData
{
set
{
m_skillData = value;
damageMode = 0;
if ((skillData.skill.damageType & DamageType.Sector) == DamageType.Sector)
damageMode = DamageMode.Sector;
else if ((skillData.skill.damageType & DamageType.Circle) == DamageType.Circle)
damageMode = DamageMode.Circle;
else if ((skillData.skill.damageType & DamageType.Line) == DamageType.Line)
damageMode = DamageMode.Line;
if (damageMode != 0)
attackTargetSelector = SelectorFactory.CreateSelector(damageMode);
status = value.Owner.GetComponent<CharacterStatus>();
}
get { return m_skillData; }
}
/// <summary>技能釋放</summary>
public virtual void DeploySkill()
{
if (m_skillData == null) return;
//對自身的影響
SelfImpact(m_skillData.Owner);
//執行傷害的計算
if (damageMode != 0)
StartCoroutine(ExecuteDamage());
}
//執行傷害的計算
protected virtual IEnumerator ExecuteDamage()
{
//按持續時間及,兩次傷害間隔,
float attackTimer = 0; //已持續攻擊的時間
ResetTargets();
if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
{
//Debug.Log(skillData.attackTargets[0].name);
foreach (var item in skillData.attackTargets)
{
//重新整理敵人頭像顯示
CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();
//加buff
foreach (var buff in skillData.skill.buffType)
{
//加bufficon
targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
//已有該buff重新整理
bool exist = false;
var buffs = item.GetComponents<BuffRun>();
foreach (var it in buffs)
{
if (it.bufftype == buff)
{
it.Reset();
exist = true;
break;
}
}
if (exist)
{
continue;
}
//新增新buff
var buffRun = item.AddComponent<BuffRun>();
buffRun.InitBuff(buff, skillData.skill.buffDuration, skillData.skill.buffValue,
skillData.skill.buffInterval);
}
}
}
do
{
//通過選擇器選好攻擊目標
ResetTargets();
if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
{
//Debug.Log(skillData.attackTargets[0].name);
foreach (var item in skillData.attackTargets)
{
//對敵人的影響
TargetImpact(item);
}
}
yield return new WaitForSeconds(skillData.skill.damageInterval);
attackTimer += skillData.skill.damageInterval;
//做傷害數值的計算
} while (skillData.skill.durationTime > attackTimer);
}
private void ResetTargets()
{
if (m_skillData == null)
return;
m_skillData.attackTargets = attackTargetSelector.SelectTarget(m_skillData, transform);
}
private float CirculateDamage(GameObject goTarget)
{
CharacterStatus goStatus = goTarget.GetComponent<CharacterStatus>();
//是否命中計算
float rate = status.hitRate / (float) goStatus.dodgeRate;
if (rate < 1)
{
int max = (int) (rate * 100);
int val = Random.Range(0, 100);
if (val < max)
{
//Debug.Log("Miss");
return 0;
}
}
//普攻的技能傷害為0; 技能有固定傷害*等級加成 + 普攻傷害
var damageVal = status.damage * (1000 / (1000 + goStatus.defence)) +
skillData.skill.damage * (1 + skillData.level * skillData.skill.damageRatio);
return damageVal;
}
///對敵人的影響nag
public virtual void TargetImpact(GameObject goTarget)
{
//出受傷特效
if (skillData.hitFxPrefab != null)
{
//找到受擊特效的掛點
Transform hitFxPos = goTarget.GetComponent<CharacterStatus>().HitFxPos;
var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hitFxPos.position,
hitFxPos.rotation);
go.transform.SetParent(hitFxPos);
GameObjectPool.I.Destory(go, 2f);
}
//受傷
var damageVal = CirculateDamage(goTarget);
var targetStatus = goTarget.GetComponent<CharacterStatus>();
targetStatus.OnDamage((int) damageVal, skillData.Owner);
}
//碰撞觸發目標影響
public virtual void TargetImpact(GameObject goTarget, Collider collider)
{
//敵人buff
foreach (var buff in skillData.skill.buffType)
{
//已有該buff重新整理
bool exist = false;
var buffs = goTarget.GetComponents<BuffRun>();
foreach (var it in buffs)
{
if (it.bufftype == buff)
{
it.Reset();
exist = true;
break;
}
}
if (exist)
continue;
//新增新buff
var buffRun = goTarget.AddComponent<BuffRun>();
buffRun.InitBuff(buff, skillData.skill.buffDuration,
skillData.skill.buffValue, skillData.skill.buffInterval);
}
//出受傷特效
if (skillData.hitFxPrefab != null)
{
//找到受擊特效的掛點,碰撞但未檢測到射線點,生成受擊特效在hitFxPos處
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
Physics.Raycast((Ray) ray, out hit, 1000);
if (hit.collider == collider)
{
var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hit.point,
transform.rotation);
GameObjectPool.I.Destory(go, 2f);
}
else
{
Transform hitFxPos = goTarget.GetComponent<CharacterStatus>().HitFxPos;
var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hitFxPos.position,
hitFxPos.rotation);
GameObjectPool.I.Destory(go, 2f);
}
}
//受傷
var damageVal = CirculateDamage(goTarget);
var targetStatus = goTarget.GetComponent<CharacterStatus>();
targetStatus.OnDamage((int) damageVal, skillData.Owner);
}
///對自身的影響
public virtual void SelfImpact(GameObject goSelf)
{
//釋放者: 消耗SP
var chStaus = goSelf.GetComponent<CharacterStatus>();
if (chStaus.SP != 0)
{
chStaus.SP -= m_skillData.skill.costSP;
chStaus.uiPortrait.RefreshHpMp();
//add+2 魔法條更新
}
}
private void OnTriggerEnter(Collider other)
{
if ((skillData.skill.damageType & DamageType.Bullet) == DamageType.Bullet)
{
if (skillData.skill.attckTargetTags.Contains(other.tag))
{
if (skillData.skill.attackNum == 1)
{
CharacterStatus targetStatus = other.GetComponent<CharacterStatus>();
GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();
//加buff
foreach (var buff in skillData.skill.buffType)
{
//加bufficon
targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
}
TargetImpact(other.gameObject, other);
}
else
{
//通過選擇器選好攻擊目標
IAttackSelector selector = new CircleAttackSelector();
selector.SelectTarget(m_skillData, transform);
if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
{
foreach (var item in skillData.attackTargets)
{
//重新整理敵人頭像顯示
CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();
//加buff
foreach (var buff in skillData.skill.buffType)
{
//加bufficon
targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
}
//對敵人的影響
TargetImpact(item, other);
}
}
}
GameObjectPool.I.Destory(gameObject);
}
else if (other.CompareTag("Wall"))
{
if (skillData.hitFxPrefab != null)
{
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
Physics.Raycast((Ray) ray, out hit, 1000);
if (hit.collider != other)
return;
//找到受擊特效的掛點
var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hit.point,
other.transform.rotation);
//go.transform.SetParent(hitFxPos);
GameObjectPool.I.Destory(go, 2f);
}
GameObjectPool.I.Destory(gameObject);
}
}
}
public static Dictionary<BuffType, string> buffIconName = new Dictionary<BuffType, string>();
public static void InitBuffIconName()
{
buffIconName.Add(BuffType.Burn,"Buff_13");
buffIconName.Add(BuffType.Slow,"Buff_15");
buffIconName.Add(BuffType.Stun,"Buff_12");
buffIconName.Add(BuffType.Poison,"Buff_14");
buffIconName.Add(BuffType.BeatBack,"Buff_5");
buffIconName.Add(BuffType.BeatUp,"Buff_4");
buffIconName.Add(BuffType.Pull,"Buff_6");
buffIconName.Add(BuffType.AddDefence,"Buff_3");
buffIconName.Add(BuffType.RecoverHp,"Buff_7");
buffIconName.Add(BuffType.Light,"Buff_8");
}
}
小結
到目前,所有技能邏輯都結束;下一節介紹buff系統和UI顯示相關;