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:
Enemy.cs:
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); } } }
留言
張貼留言