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,主要包含血量、受傷、死亡、控制血量條等方法都統一寫在這個類別中,完整程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 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<animator> (); audioSource = GetComponent<audiosource> (); characterMovement = GetComponent<charactermovement> (); 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<player> (); if (player && player.isActiveAndEnabled) { // 玩家死亡需重載關卡 SceneManager.LoadScene (0); } else { // 敵人死亡要銷毀物件 Destroy(gameObject); } } private void UpdateHealthOrb(){ if (healthOrb) { healthOrb.fillAmount = healthAsPercentage; } } private void SetCurrentMaxHealth(){ currentHealthPoint = maxHealthPoints; } } } </player></charactermovement></audiosource></animator> |
由於原本在Player跟Enemy中的TakeDamage方法不見了,血量屬性也消失了,大家應該於Console視窗中會看見如下圖一大堆的錯誤訊息。
所以接下來大致修改方向就是使用GetComponent<HealthSystem>去取得血量,或是呼叫TakeDamage方法,如下圖。
如下圖Enemy.GetComponent<HealthSystem>().TakeDamage就可以對敵人造成傷害了。
給玩家補血的Heal方法也同樣放在HealthSystem中。
最後記得將Health System放進Player中,並設定相關參數唷!
以下提供重構後的程式碼給大家參考:
Player.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | 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<healthsystem> ().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> (); // 將人物的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> (); // 計算取得有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> (); // 註冊滑鼠碰到敵人的事件 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<energy> (); // 取得技能需要的能量消耗量 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<healthsystem>().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(); } } } </healthsystem></energy></cameraraycaster></dominanthand></animator></healthsystem> |
Enemy.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | 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<player>(); 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> (); // 設定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<rigidbody> ().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); } } } </rigidbody></projectile></player> |
CharacterMovement.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | 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<cameraraycaster>(); animator = GetComponent<animator> (); myRigidbody = GetComponent<rigidbody> (); myRigidbody.constraints = RigidbodyConstraints.FreezeRotation; agent = GetComponent<navmeshagent> (); 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); } } } </navmeshagent></rigidbody></animator></cameraraycaster> |
Projectile.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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<healthsystem>(); if (healthSystem) { healthSystem.TakeDamage (damageCaused); } Destroy(gameObject); } } } </healthsystem> |
PowerAttackBehaviour.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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<healthsystem>().TakeDamage (damageToDeal); } } } </healthsystem> |
SelfHealBehaviour.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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<player> (); } public override void Use(AbilityParams useParams){ player.GetComponent<healthsystem> ().Heal ((config as SelfHealConfig).GetExtraHealth ()); PlayParticleEffect (); PlayEffectAudio (); } } } </healthsystem></player> |
EnemyHealthBar.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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<enemy>(); // Different to way player's health bar finds player healthBarRawImage = GetComponent<rawimage>(); } void Update() { float healthAsPercentage = enemy.GetComponent<healthsystem> ().healthAsPercentage; float xValue = -(healthAsPercentage / 2f) - 0.5f; healthBarRawImage.uvRect = new Rect(xValue, 0f, 0.5f, 1f); } } } </healthsystem></rawimage></enemy> |
留言
張貼留言