async/awaitでAnimatorの終了待ち/切り替えをする
StateMachineは人類には早すぎる
Unity開発者のみなさん、AnimatorController
は好きですか?
わたしは嫌いです
なぜ、と言われればそれこそ言葉にできないくらいの恨みがあるのですが、言いたいことをだいだい言ってくれている動画[1]があったのでそれを貼ることで説明の代わりとします。
動画の00:20 ~ 01:20あたりです。
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再生中に割り込みで変更もできる
コード
早速ですがコードを貼ってしまいます。
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に戻ります。
awaitableAnimatorState.SetState("0");
解説
StateMachineBehaviour
はC#の裏切り者というかAnimatorController
の一部だと思っているので、MonoBehaviour
だけでやりきりました。
存在チェックしているとはいえstateをstring
で指定しているのはあまりにアレなので、実際に運用するならばenum
でキーを切るべきと思います。また、再生したいAnimation
によってDurationTimeSecondとして定義されている遷移時間を変えたいという需要もあると思うので(ダメージを受けてのけぞるアニメーションには素早く遷移したい / 座り込むアニメーションにはゆっくり遷移したい)その場合はenum
をキー、DurationTimeをvalueにしたDictionary
を作っておくとか。
こうすればできるよね、というのを確認した段階の実装なので、実運用に持ち込むのなら上に述べたもろもろを調整する必要があると思います。
ちなみに、本当の本当にScriptだけでやるならPlayable APIを使うべきなんでしょうが「え〜これ使ってるの〜!?」って公式に言われそうな代物なんですよね。ぜんぜん情報ないし。なんでSimpleAnimationやめちゃったんでしょうね……。
まとめ
StateMachineは人類には早すぎる
おしまい。
参考
-
It can be used well.って半分くらい本気で言ってそうですき ↩︎
Discussion