概要
AnimatorステートマシンのTriggerはUnity標準機能のはずなのにReset周りがとてもややこしいです。
ざっくり言えば、攻撃中に攻撃ボタンを押すと、攻撃が終わったあとに攻撃を始めます。
Unity Technologies Japan様の動画では「攻撃の意思が残留する」と表現しています。
AnimatorControllerを使ってコンボ攻撃する方法 - Animation Tips #2
動画内で行われているように、StateMachineBehaviourをアタッチするのも手段の一つです。
しかし、ジャンプ中に攻撃ボタンを押すなどすると、ジャンプ終了着地後に攻撃を始めるかもしれないので、結局あらゆるTriggerをResetするしかありません。
そして、AnimatorController本体に手を入れるのも正直抵抗があります。
そのため、できるだけ通常のSetTriggerと同じ感覚で使用でき、適切なタイミングでResetを行うようにしてほしいです。
対応
下記記事では1フレームでリセットする手法が紹介されていました。
LIGHT11様
【Unity】【Animator Controller】即座に遷移できなかったら無効になるトリガーを作る
今回はこちらの記事を参考に、以下の改善を行いました。
- LateUpdateのSetTriggerにも対応
using UnityEngine;
using UniRx;
using UniRx.Triggers;
using System.Threading;
using Cysharp.Threading.Tasks;
public static class AnimatorExtension
{
public static void SetTriggerOneShot(this Animator self, string name)
{
var cts = new CancellationTokenSource();
self.SetTrigger(name);
self.OnAnimatorMoveAsObservable()
.Subscribe(_ => { self.ResetTrigger(name); cts.Cancel(); })
.AddTo(cts.Token)
.AddTo(self.GetCancellationTokenOnDestroy());
}
public static void SetTriggerOneShot(this Animator self, int id)
{
var cts = new CancellationTokenSource();
self.SetTrigger(id);
self.OnAnimatorMoveAsObservable()
.Subscribe(_ => { self.ResetTrigger(id); cts.Cancel(); })
.AddTo(cts.Token)
.AddTo(self.GetCancellationTokenOnDestroy());
}
}
注意点としては、1フレームでリセットされるため
実際にStateが再生され終わるのを待って遷移させたい場合は
通常のSetTriggerを使うようにすることくらいでしょうか。
つまり、コンボ攻撃などで攻撃A中に攻撃ボタンを押したとき、攻撃Aが所定のフレームに達したあとに攻撃Bが出る(いわゆる「先行入力」)が想定の仕様の場合です。
誰か助けて
本当はUniTaskだけで完結させたかったですが、OnAnimatoMove直後に処理を実行するやり方がわかりませんでした。
using UnityEngine;
using Cysharp.Threading.Tasks;
public static class AnimatorExtension
{
public static void SetTriggerOneShot(this Animator self, string name)
{
self.SetTrigger(name);
AutoResetTrigger(self, name).Forget();
}
public static void SetTriggerOneShot(this Animator self, int id)
{
self.SetTrigger(id);
AutoResetTrigger(self, id).Forget();
}
private static async UniTaskVoid AutoResetTrigger(Animator animator, string name)
{
// Update,OnAnimatorMove,PreLateUpdate,LateUpdate だと思ったのに…….
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate, animator.GetCancellationTokenOnDestroy());
if (animator == null)
{
return;
}
animator.ResetTrigger(name);
}
private static async UniTaskVoid AutoResetTrigger(Animator animator, int id)
{
// Update,OnAnimatorMove,PreLateUpdate,LateUpdate だと思ったのに…….
await UniTask.Yield(PlayerLoopTiming.PreLateUpdate, animator.GetCancellationTokenOnDestroy());
if (animator == null)
{
return;
}
animator.ResetTrigger(id);
}
}
結論
Triggerは本記事のような拡張メソッドで1フレーム、それ以外はBoolという運用でいい気がする。