Related Posts Plugin for WordPress, Blogger...

3-3 Trigger Player Hit Animation And Audio On Radius

本章要介紹如何播放Player的攻擊動畫,以及當Player進入到某個範圍後會播放音效。因為前半段製作過程都已有文章進行說明,如大家有不懂的地方請先閱讀以下文章:

2-27 Import Mechanim Animation Pack
https://rpgcorecombat.blogspot.tw/2017/12/2-27-import-mechanim-animation-pack.html

3-2 The Animator Override Controller

主要更改的程式碼為Player.cs,只要在呼叫IDamageable的TakeDamage之前,使用animator呼叫SetTrigger方法即可,如下圖。

我在Player.cs中進行了一些重構讓方法可以自我解釋:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
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 damagePerHit = 50f;
  [SerializeField] AnimatorOverrideController animatorOverrideController;
  [SerializeField] Weapon weaponInUse;

  Animator animator;
  float currentHealthPoint;
  float lastHitTime;
  CameraRaycaster cameraRaycaster;

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

  void Start(){
   // 初始化血量
   currentHealthPoint = maxHealthPoints;

   cameraRaycaster = FindObjectOfType ();
   cameraRaycaster.notifyMouseClickObservers += OnMouseClick ;
   // 初始化武器,放置到手中
   PutWeaponInHand ();
   // 覆寫人物角色中的Animation
   OverrideAnimatorController ();
  }

  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 OnMouseClick(RaycastHit raycastHit, int layerHit){
   const int enemyLayer = 9;
   if (layerHit == enemyLayer) {
    GameObject enemy = raycastHit.collider.gameObject;
    if (IsTargetInRange (enemy)) {
     AttackTarget (enemy);
    }
   }
  }

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

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

  public void TakeDamage(float damage){
   // Math.Clamp可以確保數值運算後不會低於最小值或者高於最大值
   currentHealthPoint = Mathf.Clamp (currentHealthPoint - damage, 0f, maxHealthPoints);
  }
 }
}

然後,因為minTimeBetweenHits跟maxAttackRange這兩個變數,我認為要取決於不同的武器,所以將它們拉到Weapon.cs裡面:


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Weapons{
 [CreateAssetMenu(menuName="RPG/Weapon")]
 public class Weapon : ScriptableObject {

  public Transform gripTransform;

  [SerializeField] GameObject weaponPrefab;
  [SerializeField] AnimationClip attackAnimation;
  [SerializeField] float minTimeBetweenHits = 1f;
  [SerializeField] float maxAttackRange = 5f;

  public float GetMinTimeBetweenHits(){
   return minTimeBetweenHits;
  }

  public float GetMaxAttackRange(){
   return maxAttackRange;   
  }

  public GameObject GetWeaponPrefab(){
   return weaponPrefab;
  }

  public AnimationClip GetAnimClip(){
   RemoveAnimationEvent ();
   return attackAnimation;
  }

  // 避免Asset Pack造成我們的遊戲Crash
  private void RemoveAnimationEvent(){
   attackAnimation.events = new AnimationEvent[0];
  }
 }
}

執行遊戲以後,就會發現玩家可以發動帥帥的動畫了!

動畫的部分,除了之前介紹的免費動畫以外,也推薦大家可以購買付費版的唷!有非常多種不同武器的動畫:


接著,再來介紹如何讓Player進入特定範圍後播放音效。首先請大家建立Script名為AudioTrigger。

AudioTrigger的程式碼如下:

using UnityEngine;

public class AudioTrigger : MonoBehaviour
{
 [SerializeField] AudioClip clip;
 [SerializeField] int layerFilter = 0;
 [SerializeField] float triggerRadius = 5f;
 [SerializeField] bool isOneTimeOnly = true;

 [SerializeField] bool hasPlayed = false;
 AudioSource audioSource;

 void Start()
 {
  audioSource = gameObject.AddComponent();
  audioSource.playOnAwake = false;
  audioSource.clip = clip;

  SphereCollider sphereCollider = gameObject.AddComponent();
  sphereCollider.isTrigger = true;
  sphereCollider.radius = triggerRadius;
 }

 void OnTriggerEnter(Collider other)
 {
  if (other.gameObject.layer == layerFilter)
  {
   RequestPlayAudioClip();
  }
 }

 void RequestPlayAudioClip()
 {
  if (isOneTimeOnly && hasPlayed)
  {
   return;
  }
  else if (audioSource.isPlaying == false)
  {
   audioSource.Play();
   hasPlayed = true;
  }
 }

 void OnDrawGizmos()
 {
  Gizmos.color = new Color(0, 255f, 0, .5f);
  Gizmos.DrawWireSphere(transform.position, triggerRadius);
 }
}

再來建立一個Empty GameObject,命名為Audio Trigger,並將Script拉進去。大家可以看見有幾個選項Clip是音效資源、Layer Filter是指觸發音效的Layer、Trigger Radius是觸發範圍、Is One Time Only指是否觸發一次音效後便不再播放、Has Player是用來判斷是否已播放音效的布林變數。

接著,因為我的Player Layer設置在第10個。

所以,Layer Filter設定為10。音效的部分我在這邊下載了一些免費的音效來測試:

接著,Audio Trigger拉入Project中變成prefab。

先執行遊戲測試看看,當玩家進入綠色範圍內時便會播放音效。

好的!那我們將Audio Trigger拉入Healer內。

這樣,當玩家進入Healer的補血範圍內後,就會播放音效了。


留言