Related Posts Plugin for WordPress, Blogger...

3-13 Player Damage & Death Sound

本章要教大家播放受傷跟死亡的音效。首先請大家下載角色的音效,花了很多時間才找到滿意的免費音效:
http://www.affordableaudio4everyone.com/Affordable%20SFX%204%20Everyone/Samples/ActionRPG_Characters/ActionRPG_Char_Samples.zip

另外這是該音效的官網,介紹是說有4290個音效,只要約600元的樣子,大家有興趣可以參考看看:
http://www.affordableaudio4everyone.com/Affordable%20SFX%204%20Everyone/ActionRPG_Characters.html

但我還沒買,在目前的階段中,我們使用免費音效來測試程式是否能正常運作即可。往後要優化遊戲體驗時,再找更好的資源檔進行更換。

首先,先開啟File/Build Settings選項。

按下「Add Open Scenes」將目前的開啟的Scene加入,因為我們要製作當玩家死亡後會重新讀取關卡的功能,必須先將Scene登記在Build Settings中。

然後,在Player中加入Audio Source,這是用來播放音效的組件。

將Play On Awake選項關掉。

接著,請將剛剛下載的音效檔匯入,或者匯入自己預先準備的音效檔。

主要修改Player.cs及Enemy.cs。於Player.cs的TakeDamage方法中新增條件式判斷血量,血量不足時呼叫KillPlayer方法,這邊使用StartCoroutine的呼叫方式,是為了讓程式等待死亡音效播放完畢後,再重新載入場景。播放音效的部分,則用Random隨機函數挑選其中一個音效檔。

Enemy.cs的Update中,則增加了判斷玩家是否已死的條件式,當玩家死了以後就不要再攻擊玩家了。不然會發生Player喊著死亡音效的時候,Enemy還在拚命鞭屍的窘境。(ˊˋ)

最後,將音效拉入Damage Sounds跟Death Sounds即可。

以下提供本章修改的完整程式碼。

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, IDamageable {

  [SerializeField] float maxHealthPoints = 100f;
  [SerializeField] float baseDamage = 50f;
  [SerializeField] AnimatorOverrideController animatorOverrideController;
  [SerializeField] Weapon weaponInUse;
  [SerializeField] SpecialAbilityConfig[] abilities;
  [SerializeField] AudioClip[] damageSounds;
  [SerializeField] AudioClip[] deathSounds;

  float lastHitTime;
  float currentHealthPoint;
  CameraRaycaster cameraRaycaster;
  ISpecialAbility[] specialAbility;
  AudioSource audioSource;
  Animator animator;


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

  void Start(){
   // 初始化血量
   currentHealthPoint = maxHealthPoints;
   cameraRaycaster = FindObjectOfType ();
   // 註冊滑鼠碰到敵人的事件
   cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
   // 初始化武器,放置到手中
   PutWeaponInHand ();
   // 覆寫人物角色中的Animation
   OverrideAnimatorController ();
   // 替Player增加技能的Behaviour,並回傳ISpecialAbility物件
   AttachSpecialAbility();
   audioSource = GetComponent ();
  }

  private void AttachSpecialAbility(){
   specialAbility = new ISpecialAbility[abilities.Length];
   for (int i = 0; i < abilities.Length; i++) {
    // 儲存ISpecialAbility物件
    specialAbility[i] = abilities [i].AddComponent (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"] = weaponInUse.GetAnimClip ();
  }

  private void PutWeaponInHand(){
   GameObject weaponPrefab = weaponInUse.GetWeaponPrefab ();
   // 生成武器時,將會放置到指定的GameObject之下,此處為WeaponSocket之下
   // 故可將WeaponSocket設置成玩家的右手或左手
   GameObject weaponSocket = RequestDominantHand();
   GameObject weapon = Instantiate (weaponPrefab, weaponSocket.transform);
   // 將武器放置到正確的位置,並有正確的方向
   weapon.transform.localPosition = weaponInUse.gripTransform.localPosition;
   weapon.transform.localRotation = weaponInUse.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;
  }

  void OnMouseOverEnemy(Enemy enemy){
   if (Input.GetMouseButtonDown (0) && IsTargetInRange (enemy)) {
    AttackTarget (enemy);
   }else if (Input.GetMouseButtonDown (1)) {
    // 0為使用第一個技能 
    AttemptSpecialAbility(0, enemy);
   }
  }

  private void AttemptSpecialAbility(int abilityIndex, Enemy enemy){
   Energy energyComponent = GetComponent ();
   // 取得技能需要的能量消耗量
   float energyCost = abilities [abilityIndex].GetEnergyCost ();

   if (energyComponent.IsEnergyAvailable (energyCost)) {
    energyComponent.ConsumeEnergy (energyCost);
    // 發動技能,並傳入Player的baseDamage
    AbilityParams abilityParams = new AbilityParams (enemy, baseDamage);
    specialAbility [abilityIndex].Use(abilityParams);
   }

  }

  private void AttackTarget(Enemy enemy){
   // 確認本次攻擊時間離上一次攻擊時間須大於minTimeBetweenHits,相當於技能冷卻時間
   if ( (Time.time - lastHitTime > weaponInUse.GetMinTimeBetweenHits())) {
    // 呼叫Trigger啟動攻擊動畫
    animator.SetTrigger("Attack"); // TODO make const
    // 呼叫攻擊Target的方法
    enemy.TakeDamage(baseDamage);
    // 紀錄目前攻擊的時間
    lastHitTime = Time.time;
   }
  }

  // 確認敵人是否在範圍技的攻擊範圍內
  private bool IsTargetInRange(Enemy enemy){
   float distanceToTarget = (enemy.transform.position - transform.position).magnitude;
   return distanceToTarget <= weaponInUse.GetMaxAttackRange();
  }

  public void TakeDamage(float damage){
   // Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值
   currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints);
   // 隨機播放受傷的音效
   audioSource.clip = damageSounds [Random.Range (0, damageSounds.Length)];
   audioSource.Play ();
   bool playerDies = currentHealthPoint <= 0;
   if (playerDies) {
    StartCoroutine (KillPlayer ());
   }
  }

  IEnumerator KillPlayer(){
   // 播放死亡音效
   audioSource.clip = deathSounds [Random.Range (0, deathSounds.Length)];
   audioSource.Play ();
   // 播放死亡動畫
   // 等待一段時間(依音效長度而定)
   yield return new WaitForSecondsRealtime(audioSource.clip.length);
   // 重載關卡
   SceneManager.LoadScene(0);
  }
 }
}

Enemy.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityStandardAssets.Characters.ThirdPerson;
using RPG.Core;
using RPG.Weapons;

namespace RPG.Character{
 public class Enemy : MonoBehaviour, IDamageable {

  [SerializeField] float maxHealthPoints = 100f;
  [SerializeField] float attackRadius = 3.0f;
  [SerializeField] float chaseRadius = 10.0f;
  [SerializeField] float damagePerShot = 9f;
  [SerializeField] float secondsBetweenShots = 0.5f;
  [SerializeField] GameObject projectileToUse;
  [SerializeField] GameObject projectileSocket;
  [SerializeField] Vector3 aimOffset = new Vector3(0, 1f, 0);

  bool isAttacking = false;
  float currentHealthPoint;
  AICharacterControl aiCharacterControl = null;
  Player player;
  IEnumerator coroutine;
  WaitForSeconds waitForShots;

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

  void Start(){
   currentHealthPoint = maxHealthPoints;

   aiCharacterControl = GetComponent ();
   player = GameObject.FindObjectOfType();
   waitForShots = new WaitForSeconds (secondsBetweenShots);
  }

  void Update(){
   // 判斷Player是否已經死了
   if (player.healthAsPercentage <= Mathf.Epsilon) {
    StopAllCoroutines ();
    // 阻止Enemy在Player已經死亡的狀態下仍繼續攻擊
    Destroy (this);
   }

   // 計算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;
   }
  }

  public void TakeDamage(float damage){
   // Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值
   currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints);
   // 敵人血量低於0就會消失
   if (currentHealthPoint <= 0) {
    Destroy (gameObject);
   }
  }

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

留言