Related Posts Plugin for WordPress, Blogger...

4-7 Extracting a DamageSystem Component

這次要重構IDamageable跟HealthOrb,主要是希望將血量、傷害等方法跟Player分離出來,統一放置於新的類別HealthSystem中,並讓Player跟Enemy兩方都能共用。首先,於Character資料夾內新增HealthSystem。


接著將有實作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);
     }
 }
}

留言