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を変えると幅広い雰囲気が出せるので、一般的な用途にも使えそうです。








