Related Posts Plugin for WordPress, Blogger...

4-13 Automatic Repeat Attacking

繼上一章我們成功建立了Enemy AI後,本章要來撰寫讓敵人、玩家在鎖定目標後自動攻擊的方法。首先,我們先解決之前發生在Enemy AI的一些Bug。

第一個Bug是敵人死後,物件無法成功銷毀。後來發現在KillCharacter中開頭的StopAllCoroutines,這段程式碼會導致KillCharacter方法於執行yield return後就會被自動關閉,所以後面的玩家重載關卡以及敵人死亡要銷毀物件,都不會執行。

先前這段方法是放在Player中,所以為了讓Player在死亡後不再繼續攻擊,才使用StopAllCoroutines,如今重構後的程式碼,已將攻擊程式移動到WeaponSystem,所以請大家放心將這段刪除吧。

所以,我們會在WeaponSystem內撰寫這段程式,需要讓角色停止攻擊時,便呼叫WeaponSystem.StopRepeatedlyAttack。

如下圖,PlayerMovement中的OnMouseOverWalkable方法,當玩家點擊地面要進行移動時,我們要取消Player的自動攻擊。

好的,簡單介紹一下要撰寫在WeaponSystem的自動攻擊方法,首先while迴圈的條件式需確認攻擊者跟目標都活著,若有一方死亡就會停止自動攻擊。再來計算自動攻擊的間隔時間,我們會從Weapon設定中取得MinTimeBetweenHits參數,並將動畫加速的倍率乘以間隔時間,取得真正的間隔時間。確認目前時間大於間隔時間後,呼叫AttackTargetOnce方法進行攻擊。

AttackTargetOnce的大部分程式碼皆有撰寫註解,主要注意的是我們應該延遲一小段時間在再呼叫Target的TakeDamage方法,不然玩家剛開始揮劍,還未碰到敵人便看見敵人的血量減少,實在很奇怪!延遲時間應該從Weapon設定取得,由設計師決定延遲多久。不過於此我們先暫時設定0.25秒吧。

OK!以上就是自動攻擊的寫法,以下提供有修改到的完整程式碼:
WeaponSystem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using RPG.Weapons;

namespace RPG.Character{
 public class WeaponSystem : MonoBehaviour {

  [SerializeField] float baseDamage = 50f;
  [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;
  GameObject weaponInUse;
  GameObject target;
  Character character;
  Animator animator;

  void Start () {
   animator = GetComponent ();
   character = GetComponent ();

   PutWeaponInHand (currentWeaponConfig);
   OverrideAnimatorController ();
  }

  private void OverrideAnimatorController(){
   if (!character.GetOverrideController ()) {
    Debug.Break ();
    Debug.LogAssertion ("Please provide " + gameObject + "with an animator override controller.");
   } else {
    AnimatorOverrideController animatorOverrideController = 
     character.GetOverrideController ();
    // 將人物的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;
  }

  public void AttackTarget(GameObject targetToAttack){
   target = targetToAttack;
   StartCoroutine (AttackTargetRepeatedly ());
  }

  IEnumerator AttackTargetRepeatedly(){
   // 確認攻擊者跟目標都活著
   while (AttackerAndTargetStillAlive()) {
    float weaponHitPeriod = currentWeaponConfig.GetMinTimeBetweenHits ();
    // 將動畫加速的倍率算進等待時間中
    float timeToWait = weaponHitPeriod * character.GetAnimSpeedMultiplier();

    bool isTimeToHitAgain = Time.time - lastHitTime > timeToWait;

    if (isTimeToHitAgain) {
     AttackTargetOnce ();
     lastHitTime = Time.time;
    }
    yield return new WaitForSeconds (timeToWait);
   }
  }

  private bool AttackerAndTargetStillAlive(){
   bool attackerStillAlive = GetComponent ().healthAsPercentage >= Mathf.Epsilon;
   bool targetStillAlive = target.GetComponent ().healthAsPercentage >= Mathf.Epsilon;
   return attackerStillAlive && targetStillAlive;
  }

  void AttackTargetOnce(){
   // 攻擊時要面向目標
   transform.LookAt(target.transform);
   // 每次攻擊前都重設武器的攻擊動畫
   OverrideAnimatorController();
   // 呼叫Trigger啟動攻擊動畫
   animator.SetTrigger(ATTACK_TRIGGER);
   // 延遲一段時間再讓目標受傷
   float delay = 0.25f; //TODO 延遲時間應該從Weapon取得
   StartCoroutine(DamageAfterDelay(delay));
  }

  IEnumerator DamageAfterDelay(float delay){
   yield return new WaitForSecondsRealtime (delay);
   // 呼叫攻擊Target的方法
   target.GetComponent().TakeDamage(CalculateDamage());
  }

  public void StopRepeatedlyAttack(){
   StopAllCoroutines ();
  }

  public Weapon GetCurrentWeapon(){
   return currentWeaponConfig;
  }

  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 float CalculateDamage(){
   // 產生隨機值,比較該值是否小於致命一擊的機率
   bool isCriticalHit = Random.Range (0f, 1f) <= criticalHitChance;
   float damageBeforeCritical = baseDamage + currentWeaponConfig.GetAdditionalDamage ();
   if (isCriticalHit) {
    // 播放致命一擊的特效
    criticalHitParticle.Play ();
    // 回傳攻擊力乘上致命一擊的乘率
    return damageBeforeCritical * criticalHitMultiplier;
   } else {
    return damageBeforeCritical;
   }
  }
 }
}

EnemyAI.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using RPG.Core;
using RPG.Weapons;

namespace RPG.Character{
 [RequireComponent(typeof(HealthSystem))]
 [RequireComponent(typeof(Character))]
 [RequireComponent(typeof(WeaponSystem))]
 public class EnemyAI : MonoBehaviour {

  [SerializeField] float attackRadius = 3.0f;
  [SerializeField] float chaseRadius = 10.0f;
  [SerializeField] WaypointContainer patrolPath;
  [SerializeField] float waypointToLerance = 2.0f;

  enum State {idle, patrolling, attacking, chasing}
  State state = State.idle;

  float distanceToPlayer;
  int nextWaypointIndex;
  PlayerMovement player;
  Character character;
  WeaponSystem weaponSystem;


  void Start(){
   player = GameObject.FindObjectOfType();
   character = GetComponent ();
   weaponSystem = GetComponent ();
  }

  void Update(){
   // 計算Player跟Enemy的距離
   distanceToPlayer = Vector3.Distance (player.transform.position, transform.position);

   // 自動巡邏
   if (distanceToPlayer > chaseRadius && state != State.patrolling) {
    StopAllCoroutines ();
    StartCoroutine (Patrol ());
   }

   // 開始追擊
   if (distanceToPlayer <= chaseRadius && distanceToPlayer >= attackRadius && state != State.chasing) {
    StopAllCoroutines ();
    StartCoroutine (ChasePlayer ());
   }

   // 攻擊
   if (distanceToPlayer <= attackRadius && state != State.attacking) {
    StopAllCoroutines ();
    StartCoroutine (AttackPlayer ());
   }

  }

  IEnumerator Patrol(){
   state = State.patrolling;

   while (distanceToPlayer > chaseRadius) {
    // 取得巡邏點位置
    Vector3 nextWaypointPos = patrolPath.transform.GetChild (nextWaypointIndex).position;
    // 設定巡邏點
    character.SetDestination (nextWaypointPos);
    // 檢查是否已靠近下一個巡邏點
    CycleWaypointWhenClose (nextWaypointPos);
    yield return new WaitForSeconds(0.5f);
   }
  }

  private void CycleWaypointWhenClose(Vector3 nextWaypointPos){
   // 計算Enemy是否已經抵達巡邏點
   if (Vector3.Distance (transform.position, nextWaypointPos) <= waypointToLerance) {
    // 更新巡邏點編號
    nextWaypointIndex = (nextWaypointIndex + 1) % patrolPath.transform.childCount;
   }
  }

  IEnumerator ChasePlayer(){
   state = State.chasing;
   //  追擊時停止自動攻擊
   weaponSystem.StopRepeatedlyAttack();

   while (distanceToPlayer <= chaseRadius) {
    character.SetStopDistance (attackRadius);
    character.SetDestination (player.transform.position);
    yield return new WaitForEndOfFrame ();
   }
  }

  IEnumerator AttackPlayer(){
   state = State.attacking;
   weaponSystem.AttackTarget (player.gameObject);
   yield break;
  }

  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);
  }
 }
}

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;
  Character character;

  public float healthAsPercentage{
   get{ return currentHealthPoint / maxHealthPoints; }
  }

  void Start () {
   animator = GetComponent ();
   audioSource = GetComponent ();
   character = 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(){
   // 阻止死亡後仍可以繼續移動
   character.Kill ();
   // 播放死亡動畫
   animator.SetTrigger (DEATH_TRIGGER);
   // 播放死亡音效
   audioSource.clip = deathSounds [Random.Range (0, deathSounds.Length)];
   audioSource.Play ();
   // 等待一段時間(依音效長度而定)
   yield return new WaitForSecondsRealtime (audioSource.clip.length);

   PlayerMovement 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;
  }
 }
}


留言