4-7 Extracting a DamageSystem Component
這次要重構IDamageable跟HealthOrb,主要是希望將血量、傷害等方法跟Player分離出來,統一放置於新的類別HealthSystem中,並讓Player跟Enemy兩方都能共用。首先,於Character資料夾內新增HealthSystem。
接著將有實作IDamageable的地方都刪掉,如Player跟Enemy,同時也需要將覆寫的方法TakeDamage一併刪除。
接著刪除IDamageable.cs檔案。
PlayerHealthBar.cs檔案也不需要了,請刪除。
接著撰寫HealthSystem.cs,主要包含血量、受傷、死亡、控制血量條等方法都統一寫在這個類別中,完整程式碼如下:
由於原本在Player跟Enemy中的TakeDamage方法不見了,血量屬性也消失了,大家應該於Console視窗中會看見如下圖一大堆的錯誤訊息。
所以接下來大致修改方向就是使用GetComponent<HealthSystem>去取得血量,或是呼叫TakeDamage方法,如下圖。
如下圖Enemy.GetComponent<HealthSystem>().TakeDamage就可以對敵人造成傷害了。
給玩家補血的Heal方法也同樣放在HealthSystem中。
最後記得將Health System放進Player中,並設定相關參數唷!
接著將有實作IDamageable的地方都刪掉,如Player跟Enemy,同時也需要將覆寫的方法TakeDamage一併刪除。
接著刪除IDamageable.cs檔案。
PlayerHealthBar.cs檔案也不需要了,請刪除。
接著撰寫HealthSystem.cs,主要包含血量、受傷、死亡、控制血量條等方法都統一寫在這個類別中,完整程式碼如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.SceneManagement; namespace RPG.Character{ public class HealthSystem : MonoBehaviour { [SerializeField] float maxHealthPoints = 800f; [SerializeField] Image healthOrb; [SerializeField] AudioClip[] damageSounds; [SerializeField] AudioClip[] deathSounds; const string DEATH_TRIGGER = "Death"; float currentHealthPoint = 0; Animator animator; AudioSource audioSource; CharacterMovement characterMovement; public float healthAsPercentage{ get{ return currentHealthPoint / maxHealthPoints; } } void Start () { animator = GetComponent(); audioSource = GetComponent (); characterMovement = GetComponent (); SetCurrentMaxHealth (); } // Update is called once per frame void Update () { UpdateHealthOrb (); } public void Heal(float points){ currentHealthPoint = Mathf.Clamp (currentHealthPoint + points, 0f, maxHealthPoints); } public void TakeDamage(float damage){ // Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值 currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints); // 隨機播放受傷的音效 AudioClip clip = damageSounds [Random.Range (0, damageSounds.Length)]; audioSource.PlayOneShot (clip); bool characterDies = currentHealthPoint <= 0; if (characterDies) { StartCoroutine (KillCharacter ()); } } IEnumerator KillCharacter(){ // 阻止Enemy在Player已經死亡的狀態下仍繼續攻擊 StopAllCoroutines (); // 阻止死亡後仍可以繼續移動 characterMovement.Kill (); // 播放死亡動畫 animator.SetTrigger (DEATH_TRIGGER); // 播放死亡音效 audioSource.clip = deathSounds [Random.Range (0, deathSounds.Length)]; audioSource.Play (); // 等待一段時間(依音效長度而定) yield return new WaitForSecondsRealtime (audioSource.clip.length); Player player = GetComponent (); if (player && player.isActiveAndEnabled) { // 玩家死亡需重載關卡 SceneManager.LoadScene (0); } else { // 敵人死亡要銷毀物件 Destroy(gameObject); } } private void UpdateHealthOrb(){ if (healthOrb) { healthOrb.fillAmount = healthAsPercentage; } } private void SetCurrentMaxHealth(){ currentHealthPoint = maxHealthPoints; } } }
由於原本在Player跟Enemy中的TakeDamage方法不見了,血量屬性也消失了,大家應該於Console視窗中會看見如下圖一大堆的錯誤訊息。
所以接下來大致修改方向就是使用GetComponent<HealthSystem>去取得血量,或是呼叫TakeDamage方法,如下圖。
如下圖Enemy.GetComponent<HealthSystem>().TakeDamage就可以對敵人造成傷害了。
給玩家補血的Heal方法也同樣放在HealthSystem中。
最後記得將Health System放進Player中,並設定相關參數唷!
以下提供重構後的程式碼給大家參考:
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; [SerializeField] AbilityConfig[] abilities; [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; AbilityBehaviour[] abilityBehaviour; Animator animator; GameObject weaponInUse; void Start(){ RegisterForMouseClcik (); PutWeaponInHand (currentWeaponConfig); OverrideAnimatorController (); AttachSpecialAbility(); } void Update(){ float healthAsPercentage = GetComponent().healthAsPercentage; if (healthAsPercentage > Mathf.Epsilon) { ScanForAbilityKeyDown (); } } private void ScanForAbilityKeyDown(){ for (int keyIndex = 1; keyIndex <= abilities.Length; keyIndex++) { if (Input.GetKeyDown (keyIndex.ToString())) { AttemptSpecialAbility (keyIndex); } } } private void AttachSpecialAbility(){ abilityBehaviour = new AbilityBehaviour[abilities.Length]; for (int i = 0; i < abilities.Length; i++) { // 儲存ISpecialAbility物件 abilityBehaviour[i] = abilities [i].AttachAbilityTo (gameObject); } } 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為使用第一個技能 AttemptSpecialAbility(0); } } private void AttemptSpecialAbility(int abilityIndex){ Energy energyComponent = GetComponent (); // 取得技能需要的能量消耗量 float energyCost = abilities [abilityIndex].GetEnergyCost (); if (energyComponent.IsEnergyAvailable (energyCost)) { energyComponent.ConsumeEnergy (energyCost); // 發動技能,並傳入Player的baseDamage AbilityParams abilityParams = new AbilityParams (currentEnemy, baseDamage); abilityBehaviour [abilityIndex].Use(abilityParams); } } 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(); } } }
Enemy.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; using RPG.Core; using RPG.Weapons; namespace RPG.Character{ public class Enemy : MonoBehaviour { [SerializeField] float attackRadius = 3.0f; [SerializeField] float chaseRadius = 10.0f; [SerializeField] float damagePerShot = 9f; [SerializeField] float firingPeriodInS = 0.5f; [SerializeField] float firingPeriodVariation = 0.1f; [SerializeField] GameObject projectileToUse; [SerializeField] GameObject projectileSocket; [SerializeField] Vector3 aimOffset = new Vector3(0, 1f, 0); bool isAttacking = false; float currentHealthPoint; Player player; IEnumerator coroutine; WaitForSeconds waitForShots; void Start(){ player = GameObject.FindObjectOfType(); float randomisedDelay = Random.Range (firingPeriodInS - firingPeriodVariation, firingPeriodInS + firingPeriodVariation); waitForShots = new WaitForSeconds (randomisedDelay); } void Update(){ // 計算Player跟Enemy的距離 float distanceToPlayer = Vector3.Distance (player.transform.position, transform.position); // 若彼此之間的距離小於attackRadius,就讓Enemy開始攻擊Player if (distanceToPlayer <= attackRadius && !isAttacking) { isAttacking = true; coroutine = SpawnProjectile (); StartCoroutine (coroutine); } if (distanceToPlayer > attackRadius && isAttacking) { isAttacking = false; StopCoroutine (coroutine); } // 若彼此之間的距離小於chaseRadius,就讓Enemy開始追蹤Player if (distanceToPlayer <= chaseRadius) { // aiCharacterControl.SetTarget (player.transform); } else { // aiCharacterControl.SetTarget (transform); } } IEnumerator SpawnProjectile(){ while (true) { yield return waitForShots; // Instantiate可以生成GameObject,Quaternion.identity為方向不進行任何轉向 GameObject newProjectile = Instantiate(projectileToUse, projectileSocket.transform.position, Quaternion.identity); // 取得Projectile Projectile projectileComponent = newProjectile.GetComponent (); // 設定Projectile的攻擊威力 projectileComponent.SetDamage(damagePerShot); // 設定Projectile的Shooter為自己 projectileComponent.SetShooter (gameObject); // 計算發射Projectile到Player之間的單位向量 Vector3 unitVectorToPlayer = (player.transform.position + aimOffset - projectileSocket.transform.position).normalized; float projectileSpeed = projectileComponent.GetDefaultLaunchSpeed(); // 將單位向量乘以發射速度,透過velocity將Projectile發射出去吧! newProjectile.GetComponent ().velocity = unitVectorToPlayer * projectileSpeed; } } void OnDrawGizmos(){ //繪製攻擊範圍 Gizmos.color = new Color(255f, 0f, 0f, 0.5f); Gizmos.DrawWireSphere (transform.position, attackRadius); //繪製移動範圍 Gizmos.color = new Color(0f, 0f, 255f, 0.5f); Gizmos.DrawWireSphere (transform.position, chaseRadius); } } }
CharacterMovement.cs
using System; using UnityEngine; using UnityEngine.AI; using RPG.CameraUI; namespace RPG.Character{ [RequireComponent(typeof (NavMeshAgent))] public class CharacterMovement : MonoBehaviour { [SerializeField] float walkMoveStopRadius = 0.2f; [SerializeField] float attackMoveStopRadius = 5f; [SerializeField] float moveSpeedMultiplier = 1f; [SerializeField] float animationSpeedMultiplier = 1f; [SerializeField] float movingTurnSpeed = 1800; [SerializeField] float stationaryTurnSpeed = 1800; [SerializeField] float moveThreshold = 1f; NavMeshAgent agent = null; Animator animator = null; Rigidbody myRigidbody = null; float turnAmount; float forwardAmount; private void Start() { CameraRaycaster cameraRaycaster = Camera.main.GetComponent(); animator = GetComponent (); myRigidbody = GetComponent (); myRigidbody.constraints = RigidbodyConstraints.FreezeRotation; agent = GetComponent (); agent.updateRotation = false; agent.updatePosition = true; cameraRaycaster.onMouseOverWalkable += OnMouseOverWalkable; cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy; } public void Move(Vector3 movement) { SetForwardAndTurn (movement); ApplyExtraTurnRotation(); UpdateAnimator(); } public void Kill(){ // TODO 需要禁止角色移動 } private void SetForwardAndTurn(Vector3 movement){ // 將Wolrd相對的向量轉換成Local相對的向量 if (movement.magnitude > moveThreshold) { movement.Normalize (); } // 將Worldspace的方向轉換成LocalSpace Vector3 localMovement = transform.InverseTransformDirection(movement); turnAmount = Mathf.Atan2(localMovement.x, localMovement.z); forwardAmount = localMovement.z; } void UpdateAnimator() { // update the animator parameters animator.SetFloat("Forward", forwardAmount, 0.1f, Time.deltaTime); animator.SetFloat("Turn", turnAmount, 0.1f, Time.deltaTime); animator.speed = animationSpeedMultiplier; } void ApplyExtraTurnRotation() { // help the character turn faster (this is in addition to root rotation in the animation) float turnSpeed = Mathf.Lerp(stationaryTurnSpeed, movingTurnSpeed, forwardAmount); transform.Rotate(0, turnAmount * turnSpeed * Time.deltaTime, 0); } void Update(){ if (agent.remainingDistance > agent.stoppingDistance) { Move(agent.desiredVelocity); } else { Move (Vector3.zero); } } void OnMouseOverWalkable(Vector3 destination){ if (Input.GetMouseButton (0)) { // 設置地點為滑鼠點擊的位置 agent.SetDestination(destination); agent.stoppingDistance = walkMoveStopRadius; } } void OnMouseOverEnemy(Enemy enemy){ if (Input.GetMouseButton (0)) { // 設置地點為Enemy的位置 agent.SetDestination(enemy.transform.position); agent.stoppingDistance = attackMoveStopRadius; } } void OnAnimatorMove(){ if (Time.deltaTime > 0) { Vector3 velocity = (animator.deltaPosition * moveSpeedMultiplier) / Time.deltaTime; // 保持y軸的速度不變 velocity.y = myRigidbody.velocity.y; myRigidbody.velocity = velocity; } } void OnDrawGizmos(){ //繪製攻擊範圍 Gizmos.color = new Color(255f, 0f, 0f, 0.5f); Gizmos.DrawWireSphere (transform.position, attackMoveStopRadius); } } }
Projectile.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using RPG.Core; using RPG.Character; namespace RPG.Weapons{ public class Projectile : MonoBehaviour { [SerializeField] float projectileSpeed; [SerializeField] GameObject shooter; float damageCaused; public void SetShooter(GameObject shooter){ this.shooter = shooter; } public void SetDamage(float damage){ damageCaused = damage; } public float GetDefaultLaunchSpeed(){ return projectileSpeed; } void OnCollisionEnter(Collision collision){ int layerCollidedWith = collision.gameObject.layer; // 若被碰撞的物體所處的Layer跟shooter的Layer不同,才會觸發攻擊機制 // 如敵人就不會攻擊到敵人 if (shooter && layerCollidedWith != shooter.layer) { DamageIfDamageable (collision); } } private void DamageIfDamageable(Collision collision){ // 取得HealthSystem HealthSystem healthSystem = collision.gameObject.GetComponent(); if (healthSystem) { healthSystem.TakeDamage (damageCaused); } Destroy(gameObject); } } }
PowerAttackBehaviour.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; namespace RPG.Character{ public class PowerAttackBehaviour : AbilityBehaviour{ public override void Use(AbilityParams useParams){ DealDamage (useParams); PlayParticleEffectOnTarget (useParams); PlayEffectAudio (); } private void DealDamage(AbilityParams useParams){ float damageToDeal = (config as PowerAttackConfig).GetExtraDamage () + useParams.baseDamage; useParams.target.GetComponent().TakeDamage (damageToDeal); } } }
SelfHealBehaviour.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using RPG.Core; namespace RPG.Character{ public class SelfHealBehaviour : AbilityBehaviour { Player player; void Start(){ player = GetComponent(); } public override void Use(AbilityParams useParams){ player.GetComponent ().Heal ((config as SelfHealConfig).GetExtraHealth ()); PlayParticleEffect (); PlayEffectAudio (); } } }
EnemyHealthBar.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace RPG.Character{ public class EnemyHealthBar : MonoBehaviour { RawImage healthBarRawImage = null; Enemy enemy = null; void Start() { enemy = GetComponentInParent(); // Different to way player's health bar finds player healthBarRawImage = GetComponent (); } void Update() { float healthAsPercentage = enemy.GetComponent ().healthAsPercentage; float xValue = -(healthAsPercentage / 2f) - 0.5f; healthBarRawImage.uvRect = new Rect(xValue, 0f, 0.5f, 1f); } } }
留言
張貼留言