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





留言
張貼留言