ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】キャラクターの移動に合わせて足音を付ける方法

time 2019/09/03

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

down

コメントする