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

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
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<animator> ();
   character = GetComponent<character> ();
 
   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<healthsystem> ().healthAsPercentage >= Mathf.Epsilon;
   bool targetStillAlive = target.GetComponent<healthsystem> ().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<healthsystem>().TakeDamage(CalculateDamage());
  }
 
  public void StopRepeatedlyAttack(){
   StopAllCoroutines ();
  }
 
  public Weapon GetCurrentWeapon(){
   return currentWeaponConfig;
  }
 
  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 float CalculateDamage(){
   // 產生隨機值,比較該值是否小於致命一擊的機率
   bool isCriticalHit = Random.Range (0f, 1f) <= criticalHitChance;
   float damageBeforeCritical = baseDamage + currentWeaponConfig.GetAdditionalDamage ();
   if (isCriticalHit) {
    // 播放致命一擊的特效
    criticalHitParticle.Play ();
    // 回傳攻擊力乘上致命一擊的乘率
    return damageBeforeCritical * criticalHitMultiplier;
   } else {
    return damageBeforeCritical;
   }
  }
 }
}
</dominanthand></healthsystem></healthsystem></healthsystem></character></animator>

EnemyAI.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
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<playermovement>();
   character = GetComponent<character> ();
   weaponSystem = GetComponent<weaponsystem> ();
  }
 
  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);
  }
 }
}
</weaponsystem></character></playermovement>

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
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<animator> ();
   audioSource = GetComponent<audiosource> ();
   character = GetComponent<character> ();
 
   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<playermovement> ();
   if (player && player.isActiveAndEnabled) {
    // 玩家死亡需重載關卡
    SceneManager.LoadScene (0);
   } else {
    // 敵人死亡要銷毀物件
    Destroy(gameObject);
   }
  }
 
  private void UpdateHealthOrb(){
   if (healthOrb) {
    healthOrb.fillAmount = healthAsPercentage;
   }
  }
 
  private void SetCurrentMaxHealth(){
   currentHealthPoint = maxHealthPoints;
  }
 }
}
</playermovement></character></audiosource></animator>


留言