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:

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
146
147
148
149
150
151
152
153
154
155
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> ();
   // 註冊滑鼠碰到敵人的事件
   cameraRaycaster.onMouseOverEnemy += OnMouseOverEnemy;
   // 初始化武器,放置到手中
   PutWeaponInHand ();
   // 覆寫人物角色中的Animation
   OverrideAnimatorController ();
   // 替Player增加技能的Behaviour,並回傳ISpecialAbility物件
   AttachSpecialAbility();
   audioSource = GetComponent<audiosource> ();
  }
 
  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> ();
   // 將人物的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> ();
   // 計算取得有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<energy> ();
   // 取得技能需要的能量消耗量
   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);
  }
 }
}
</energy></dominanthand></animator></audiosource></cameraraycaster>

Enemy.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
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<aicharactercontrol> ();
   player = GameObject.FindObjectOfType<player>();
   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> ();
    // 設定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<rigidbody> ().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);
  }
 }
}
</rigidbody></projectile></player></aicharactercontrol>

留言