4-8 Centralizing Special Ability Code
這次重構要將Player內的技能系統分離出來。首先將Energy的名稱改為SpecialAbilities,以後技能設定跟發動將統一放在這邊。
改名以後,Player原本放置Energy的地方會顯示Nothing Selected,請刪除。
重新加入SpecialAbilities。
接著,我們將AbilityConfig.cs中的struct AbilityParams刪除,這個物件太複雜了,而且傳入的參數中包含Enemy,使得我們的技能系統程式侷限在Player攻擊Enemy上,無法讓Enemy也共用技能系統。
刪除以後,會發現使用到AbilityParams的程式碼出現了紅字,請大家統一改成GameObject target。往後被指定攻擊的target可以是Player,也可以是Enemy。
以下列出修改後的SpecialAbilities,Player,AbilityBehaviour。
SpecialAbilities.cs
Player.cs
AbilityBehaviour.cs
重構完成後,大家可以看見成功將血量跟技能從Player分離出來,分別為Health System與Special Abilities,這兩個組件也能給Enemy使用。
改名以後,Player原本放置Energy的地方會顯示Nothing Selected,請刪除。
重新加入SpecialAbilities。
接著,我們將AbilityConfig.cs中的struct AbilityParams刪除,這個物件太複雜了,而且傳入的參數中包含Enemy,使得我們的技能系統程式侷限在Player攻擊Enemy上,無法讓Enemy也共用技能系統。
刪除以後,會發現使用到AbilityParams的程式碼出現了紅字,請大家統一改成GameObject target。往後被指定攻擊的target可以是Player,也可以是Enemy。
以下列出修改後的SpecialAbilities,Player,AbilityBehaviour。
SpecialAbilities.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using RPG.CameraUI; namespace RPG.Character{ public class SpecialAbilities : MonoBehaviour { [SerializeField] Image energyOrb; [SerializeField] float maxEnergyPoints = 100f; [SerializeField] float regenPointPerSecond = 1f; [SerializeField] AbilityConfig[] abilities; float currentEnergyPoints; AbilityBehaviour[] abilityBehaviour; float energyAsPersent{ get { return currentEnergyPoints / maxEnergyPoints; }} void Start () { currentEnergyPoints = maxEnergyPoints; AttachSpecialAbility(); } void Update(){ if (currentEnergyPoints < maxEnergyPoints) { AddEnergyPoint (); UpdateEnergyBar (); } } private void AttachSpecialAbility(){ abilityBehaviour = new AbilityBehaviour[abilities.Length]; for (int i = 0; i < abilities.Length; i++) { // 儲存ISpecialAbility物件 abilityBehaviour[i] = abilities [i].AttachAbilityTo (gameObject); } } public void AttemptSpecialAbility(int abilityIndex, GameObject target = null){ // 取得技能需要的能量消耗量 float energyCost = abilities [abilityIndex].GetEnergyCost (); if (energyCost <= currentEnergyPoints) { ConsumeEnergy (energyCost); // 發動技能,並傳入被鎖定的敵人 abilityBehaviour [abilityIndex].Use(target); } } public float GetNumberOfAbilities(){ return abilities.Length; } private void AddEnergyPoint(){ // 每秒自動恢復能量,由於Update是每個Frame都會執行,故須使用deltaTime取得Frame的間隔時間 float pointsToAdd = regenPointPerSecond * Time.deltaTime; currentEnergyPoints = Mathf.Clamp (currentEnergyPoints + pointsToAdd, 0, maxEnergyPoints); } // 消耗的魔力由SpecialAbilityConfig決定,傳到amount public void ConsumeEnergy(float amount){ float newEnergyPoint = currentEnergyPoints - amount; currentEnergyPoints = Mathf.Clamp (newEnergyPoint, 0, maxEnergyPoints); UpdateEnergyBar (); } void UpdateEnergyBar(){ energyOrb.fillAmount = energyAsPersent; } } }
Player.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.SceneManagement; using RPG.CameraUI; // TODO consider re-wiring using RPG.Core; using RPG.Weapons; namespace RPG.Character{ public class Player : MonoBehaviour { [SerializeField] float baseDamage = 50f; [SerializeField] AnimatorOverrideController animatorOverrideController; [SerializeField] Weapon currentWeaponConfig; [Range(.1f, 1.0f)] [SerializeField] float criticalHitChance = 0.1f; [SerializeField] float criticalHitMultiplier = 2.5f; [SerializeField] ParticleSystem criticalHitParticle; // 定義Trigger的字串常數 const string ATTACK_TRIGGER = "Attack"; const string DEFAULT_ATTACK = "DEFAULT ATTACK"; float lastHitTime; float currentHealthPoint; Enemy currentEnemy; CameraRaycaster cameraRaycaster; Animator animator; SpecialAbilities abilities; GameObject weaponInUse; void Start(){ abilities = GetComponent(); RegisterForMouseClcik (); PutWeaponInHand (currentWeaponConfig); OverrideAnimatorController (); } void Update(){ float healthAsPercentage = GetComponent ().healthAsPercentage; if (healthAsPercentage > Mathf.Epsilon) { ScanForAbilityKeyDown (); } } private void ScanForAbilityKeyDown(){ for (int keyIndex = 1; keyIndex <= abilities.GetNumberOfAbilities(); keyIndex++) { if (Input.GetKeyDown (keyIndex.ToString())) { if (currentEnemy) { abilities.AttemptSpecialAbility (keyIndex, currentEnemy.gameObject); } else { abilities.AttemptSpecialAbility (keyIndex); } } } } private void OverrideAnimatorController(){ // 取得Animator物件 animator = GetComponent (); // 將人物的Animator取代為我們剛剛新增的Player Animator Override animator.runtimeAnimatorController = animatorOverrideController; // Player Animator Override中有一預設的DEFAULT ATTACK,將其取代成Weapon物件中的Animation // 亦即人物的Animation將會依據不同的Weapon物件,而使用有不同的動畫 animatorOverrideController [DEFAULT_ATTACK] = currentWeaponConfig.GetAnimClip (); } public void PutWeaponInHand(Weapon weaponToUse){ currentWeaponConfig = weaponToUse; // 先移除之前手持的武器 Destroy(weaponInUse); GameObject weaponPrefab = weaponToUse.GetWeaponPrefab (); // 生成武器時,將會放置到指定的GameObject之下,此處為WeaponSocket之下 // 故可將WeaponSocket設置成玩家的右手或左手 GameObject weaponSocket = RequestDominantHand(); weaponInUse = Instantiate (weaponPrefab, weaponSocket.transform); // 將武器放置到正確的位置,並有正確的方向 weaponInUse.transform.localPosition = currentWeaponConfig.gripTransform.localPosition; weaponInUse.transform.localRotation = currentWeaponConfig.gripTransform.localRotation; } private GameObject RequestDominantHand(){ // 從Children中尋找包含DominantHand的物件 DominantHand[] dominantHand = GetComponentsInChildren (); // 計算取得有DominantHand的物件的數量 int numberOfDominantHands = dominantHand.Length; // 確保該數量不為0 Assert.AreNotEqual (numberOfDominantHands, 0, "No DominantHand found on player, please add one"); // 確保該數量不要大於1 Assert.IsFalse (numberOfDominantHands > 1, "Multiple Dominant script on Player, please remove one"); // 傳回屬於該DominantHand的GameObject return dominantHand [0].gameObject; } private void RegisterForMouseClcik(){ cameraRaycaster = FindObjectOfType (); // 註冊滑鼠碰到敵人的事件 cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy; } void OnMouseOverEnemy(Enemy enemy){ currentEnemy = enemy; if (Input.GetMouseButtonDown (0) && IsTargetInRange (enemy.gameObject)) { AttackTarget (); }else if (Input.GetMouseButtonDown (1)) { // 0為使用第一個技能 abilities.AttemptSpecialAbility(0, currentEnemy.gameObject); } } private void AttackTarget(){ // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間 if ( (Time.time - lastHitTime > currentWeaponConfig.GetMinTimeBetweenHits())) { // 每次攻擊前都重設武器的攻擊動畫 OverrideAnimatorController(); // 呼叫Trigger啟動攻擊動畫 animator.SetTrigger(ATTACK_TRIGGER); // 呼叫攻擊Target的方法 currentEnemy.GetComponent ().TakeDamage(CalculateDamage()); // 紀錄目前攻擊的時間 lastHitTime = Time.time; } } private float CalculateDamage(){ // 產生隨機值,比較該值是否小於致命一擊的機率 bool isCriticalHit = Random.Range (0f, 1f) <= criticalHitChance; float damageBeforeCritical = baseDamage + currentWeaponConfig.GetAdditionalDamage (); if (isCriticalHit) { // 播放致命一擊的特效 criticalHitParticle.Play (); // 回傳攻擊力乘上致命一擊的乘率 return damageBeforeCritical * criticalHitMultiplier; } else { return damageBeforeCritical; } } // 確認敵人是否在範圍技的攻擊範圍內 private bool IsTargetInRange(GameObject target){ float distanceToTarget = (target.transform.position - transform.position).magnitude; return distanceToTarget <= currentWeaponConfig.GetMaxAttackRange(); } } }
AbilityBehaviour.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace RPG.Character{ public abstract class AbilityBehaviour : MonoBehaviour { protected AbilityConfig config; const float PARTICLE_CLEAN_UP_DELAY = 20f; public abstract void Use(GameObject target = null); public void SetConfig(AbilityConfig config){ this.config = config; } protected void PlayEffectAudio(){ // 取得音效Component AudioSource myAudioSource = GetComponent(); // 從config取得技能音效 AudioClip audioClip = config.GetAudioClip (); // 使用PlayOnShot播放技能音效,可避免和其他技能音效重疊在一起 myAudioSource.PlayOneShot (audioClip); } protected void PlayParticleEffect(){ // 初始化粒子系統,綁定到『自己』身上 GameObject particlePrefab = Instantiate(config.GetParticlePrefab(), transform.position, Quaternion.identity, transform); // 啟動粒子系統 particlePrefab.GetComponent ().Play(); // 播放完後自我銷毀 StartCoroutine(DestroyParticleWhenFinished(particlePrefab)); } protected void PlayParticleEffectOnTarget(GameObject target){ // 初始化粒子系統,綁定到『目標』身上 GameObject particlePrefab = Instantiate(config.GetParticlePrefab(), target.transform.position, Quaternion.identity, target.transform); // 啟動粒子系統 particlePrefab.GetComponent ().Play(); // 播放完後自我銷毀 StartCoroutine(DestroyParticleWhenFinished(particlePrefab)); } IEnumerator DestroyParticleWhenFinished(GameObject particlePrefab){ while (particlePrefab.GetComponent ().isPlaying) { yield return new WaitForSeconds (PARTICLE_CLEAN_UP_DELAY); } Destroy (particlePrefab); yield return new WaitForEndOfFrame (); } } }
重構完成後,大家可以看見成功將血量跟技能從Player分離出來,分別為Health System與Special Abilities,這兩個組件也能給Enemy使用。
留言
張貼留言