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
EnemyAI.cs
HealthSystem.cs
第一個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; } } }
留言
張貼留言