🎛️

async/awaitでAnimatorの終了待ち/切り替えをする

2021/10/26に公開

StateMachineは人類には早すぎる

Unity開発者のみなさん、AnimatorControllerは好きですか?
わたしは嫌いです
なぜ、と言われればそれこそ言葉にできないくらいの恨みがあるのですが、言いたいことをだいだい言ってくれている動画[1]があったのでそれを貼ることで説明の代わりとします。
動画の00:20 ~ 01:20あたりです。

https://youtu.be/T8wCkke32q4

UnrealEngineのBluePrintでもUE4 Blueprints From Hellみたいなことになっています。
Stateを作って矢印で状態を遷移させるStateMachineは人類には早すぎたのです[2]

実際の話、ネットでAnimatorControllerの話を漁ってみると、初学者向けのこうすればできます! という以降の難易度に踏み込んだ記事がほとんど見つからないです。誰も使っていないのか、プロジェクト毎に特化しすぎるせいで記事にできないのか。

Scriptで管理すれば苦しみは減る

AnimatorControllerのわからん殺しもさることながら、Animator周りの状態管理はC#と妙に断絶しているので、お互いの足並みを揃えて動かすのがなかなか大変です。
AnimationClip自体はAnimatorController上で編集した方がわかりやすいのは確かなのですが……。
平行に管理するのはバグの元なので、AnimatorControllerはあくまでもAnimationClipの登録場所と割り切って、操作しやすいScript一本でAnimatorの状態を管理します。

想定されるのは上のようなAnimatorControllerです。
以下のような条件で考えていきたいと思います。

  • Parameter, Layers, Transitionはなし
  • なにもない場合はdefaultで待機
  • 0~5の各stateに切り替えられた場合、再生が終わったらdefaultに戻る
  • state再生中に割り込みで変更もできる

コード

早速ですがコードを貼ってしまいます。

AwaitableAnimatorState
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
 
[RequireComponent(typeof(Animator))]
public class AwaitableAnimatorState : MonoBehaviour
{
    private void Start()
    {
        _animator = GetComponent<Animator>();
        AnimationStateLoop().Forget();
    }

    private Animator _animator;
    private const string StateDefault = "default";
    public string State = StateDefault;
    // stateを切り替える間隔.これが短いほど素早く, 長いほど緩やかに切り替わる.
    public const float DurationTimeSecond = 0.4f;

    private async UniTaskVoid AnimationStateLoop()
    {
        var token = this.GetCancellationTokenOnDestroy();
        var hashDefault = Animator.StringToHash(StateDefault);

        while (true)
        {
            // State更新のためUpdate分だけ待つ
            await UniTask.Yield();
            if (token.IsCancellationRequested)
            {
                break;
            }

            var hashExpect = Animator.StringToHash(State);
            var currentState = _animator.GetCurrentAnimatorStateInfo(0);
            if (currentState.shortNameHash != hashExpect)
            {
                // DurationTimeSecondの間隔を挟んでAnimatorのStateを切り替える
                _animator.CrossFadeInFixedTime(hashExpect, DurationTimeSecond);
                // 切り替えている間のcurrentStateは切り替える前のStateが出てくる.
                // そのためDurationTimeSecondが過ぎるまで待つ
                await UniTask.Delay(TimeSpan.FromSeconds(DurationTimeSecond), cancellationToken: token);
                continue;
            }

            // stateが終了していた場合はdefaultに戻す
            if (currentState.shortNameHash != hashDefault && currentState.normalizedTime >= 1f)
            {
                SetState(StateDefault);
            }
        }
    }

    public void SetState(string nextState)
    {
        if (_animator.HasState(0, Animator.StringToHash(nextState)))
        {
            // 存在するStateだけ受け入れる
            State = nextState;
        }
    }
}

これを再生したいAnimatorのついたGameObjectにAddしておいて、

外からこんな感じで再生したいstateを設定すると、そのstateのanimationが再生され、終わったらdefaultのstateに戻ります。

usage
awaitableAnimatorState.SetState("0");

解説

StateMachineBehaviourC#の裏切り者というかAnimatorControllerの一部だと思っているので、MonoBehaviourだけでやりきりました。
存在チェックしているとはいえstateをstringで指定しているのはあまりにアレなので、実際に運用するならばenumでキーを切るべきと思います。また、再生したいAnimationによってDurationTimeSecondとして定義されている遷移時間を変えたいという需要もあると思うので(ダメージを受けてのけぞるアニメーションには素早く遷移したい / 座り込むアニメーションにはゆっくり遷移したい)その場合はenumをキー、DurationTimeをvalueにしたDictionaryを作っておくとか。
こうすればできるよね、というのを確認した段階の実装なので、実運用に持ち込むのなら上に述べたもろもろを調整する必要があると思います。

ちなみに、本当の本当にScriptだけでやるならPlayable APIを使うべきなんでしょうが「え〜これ使ってるの〜!?」って公式に言われそうな代物なんですよね。ぜんぜん情報ないし。なんでSimpleAnimationやめちゃったんでしょうね……。

まとめ

StateMachineは人類には早すぎる

おしまい。

参考

https://yutakaseda3216.hatenablog.com/entry/2017/12/19/131311
https://www.hanachiru-blog.com/entry/2019/05/14/185408
https://nopitech.com/2019/05/23/post-1241/
https://learning.unity3d.jp/6755/

脚注
  1. Animancer というアセットの説明動画です。使ったことはないですが、動画の感じだとよさそう。 ↩︎

  2. It can be used well.って半分くらい本気で言ってそうですき ↩︎

Discussion