2020/11/09
今回はUnityに関するちょっとしたお話です。
今まで私が作ったゲームにはキャラクターの「足音」を入れていませんでした。2Dならともかく、3Dで動いているのに無音というのは違和感がありますね。
調べると割と何とかなりそうなので、自作ゲームに導入するべく実装方法を考えてみました。
実装方法
InportSettingsのEventを使用する方法
インポートしたモデルを選択し、ImportSettingsのAnimationタブ→Eventsから「特定のタイミングで実行する関数」を設定できます。
足がちょうど着くくらいのタイミングでイベントを設定します。下のプレビューウィンドウのシークバーを操作してタイミングを選びましょう。ここでは呼び出す関数名をPlay
としました。
次に呼び出される側のスクリプトを作成します。
using UnityEngine; using UnityEngine.Audio; public class AnimationEventSEPlayer : MonoBehaviour { [SerializeField] private AudioClip audioClip; [SerializeField] private AudioMixerGroup audioMixerGroup; private AudioSource audioSource; private void Start() { audioSource = CreateAudioSource(); } public void Play(string eventName) { audioSource.Play(); } private AudioSource CreateAudioSource() { var audioGameObject = new GameObject(); audioGameObject.name = "AnimationEventSEPlayer"; audioGameObject.transform.SetParent(gameObject.transform); var audioSource = audioGameObject.AddComponent<AudioSource>(); audioSource.clip = audioClip; audioSource.outputAudioMixerGroup = audioMixerGroup; return audioSource; } }
これをモデルのGameObjectに追加します。audioClip
に再生する足音を設定しましょう。私の場合は音量のコントロール用にAudioMixerGroup
を設定しました。
この状態で歩くモーションを再生すると音が鳴ります。
モデルのオブジェクトに直接AudioSource
コンポーネントを追加するのは嫌なので、1つ空のGameObjectを作り、それにアタッチした上で子オブジェクトとしました。これなら複数のイベントでAudioSource
を追加しても安心です。
StateMachineBehaviourを使用する方法
AnimatorControllerを使用している場合、StateMachineBehaviour
を継承したスクリプトを作成することでも実現できます。
まず以下のようなスクリプトを作成します。
using System.Collections.Generic; using UnityEngine; using UnityEngine.Audio; public class AnimationStateSEPlayer : StateMachineBehaviour { [SerializeField] private AudioClip audioClip; [SerializeField] private AudioMixerGroup audioMixerGroup; [SerializeField] private List<float> executeTimeList; private bool isInitialized = false; private int currentLoopCount = 0; private Dictionary<float, bool> executedTimeDictionary; private AudioSource audioSource; public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (!isInitialized) { audioSource = CreateAudioSource(animator.gameObject); executedTimeDictionary = new Dictionary<float, bool>(); isInitialized = true; } InitExecutedTimeDictionary(); currentLoopCount = 0; } public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { var executedTimeList = new List<float>(executedTimeDictionary.Keys); foreach (var executedTime in executedTimeList) { if (executedTime < stateInfo.normalizedTime % 1 && !executedTimeDictionary[executedTime]) { audioSource.Play(); executedTimeDictionary[executedTime] = true; } } if (currentLoopCount < Mathf.FloorToInt(stateInfo.normalizedTime)) { InitExecutedTimeDictionary(); currentLoopCount = Mathf.FloorToInt(stateInfo.normalizedTime); } } private AudioSource CreateAudioSource(GameObject animatorGameObject) { var audioGameObject = new GameObject(); audioGameObject.name = "AnimationStateSEPlayer"; audioGameObject.transform.SetParent(animatorGameObject.transform); var audioSource = audioGameObject.AddComponent<AudioSource>(); audioSource.clip = audioClip; audioSource.outputAudioMixerGroup = audioMixerGroup; return audioSource; } private void InitExecutedTimeDictionary() { executedTimeDictionary.Clear(); executeTimeList.ForEach(t => executedTimeDictionary.Add(t, false)); } }
先ほどのスクリプトより少し複雑です。1モーション中に複数回鳴らしたいタイミングがあること、ループを考慮するとOnStateEnter()
やOnStateExit()
を使えないことなどで、割と考えることは多いです。前者は実行タイミングを管理するDictionary
を作成することで、後者はnormalizedTime
の整数部分をループ回数に見立てて都度初期化することでカバーしました。
AnimatorControllerの足音を入れたいステートを選択し、上記のスクリプトを追加します。
audioClip
には足音、executeTimeList
にはモーション全体の長さを1とした時の実行タイミングを記載します。私のモデルの場合、モーションの25%と80%くらいの位置で足音を出したいので、0.25と0.8の2つを設定しました。
ちょっと込み入ったスクリプトを書かなければならない半面、モデルの差し替えや再インポート等でも設定が吹っ飛ばないのが魅力です。
足音に使用したアセット
今回足音用に使ったアセットはこちらです。
https://assetstore.unity.com/packages/audio/sound-fx/footstep-snow-and-grass-90678
音は雪と草のケースに限定されていますが、pitchを変えると幅広い雰囲気が出せるので、一般的な用途にも使えそうです。