0.やりたいこと
同じアニメーターコントローラーを使うキャラクター同士に、同じタイミングで同じアニメーションをさせたかった。
有り体に言えば、操り人形というか、マリオネット的な挙動が作りたかったわけで。
1.既存の実装案
こういうのが出てきた。
入力とアニメーション制御を司るスクリプトを同期したいキャラクターにも付与するのも一案なのだが、自分の実装の場合StarterAssetを使おうとしていた関係で、入力関係を複数に所持させることを回避したかった。(具体的には、Player Inputなどをいちいちつけるのが面倒に思えた。)
2.実際の実装
なお、UniRxを使っているので、このスクリプトを参考にされる方はお気をつけください。
同じアニメーターコントローラーを登録したキャラクターが二人おり、そのうち一人だけがキー入力等でアニメーションが遷移することが前提です。
こちらのスクリプトは、もう一方のアニメーションを同期させたいキャラクターの方にアタッチします。
using UniRx;
using UnityEngine;
namespace Sample.Animation
{
/// <summary>
/// 同じアニメーターコントローラーを使う二者のパラメーターを同期する
/// </summary>
public class AnimSyncManager : MonoBehaviour
{
// 同期のターゲットとなるアニメーター
[SerializeField] private Animator otherAnimator = null;
private void Start()
{
// 同期するアニメーターを取得
var animator = GetComponent<Animator>();
// アニメーションによってはAnimationEventを吐いてエラーになることがあるので無効化する
animator.fireEvents = false;
// ターゲットのアニメーターのパラメーターを監視して変更があれば同期対象の方でも変更する
foreach (var parameter in animator.parameters)
{
switch (parameter.type)
{
case AnimatorControllerParameterType.Bool:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetBool(parameter.name))
.Where(_ => _!=animator.GetBool(parameter.name))
.Subscribe(_ => animator.SetBool(parameter.name, _))
.AddTo(this.gameObject);
break;
case AnimatorControllerParameterType.Float:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetFloat(parameter.name))
.Where(_ => Mathf.Abs(_ - animator.GetFloat(parameter.name)) > 0)
.Subscribe(_ => animator.SetFloat(parameter.name, _))
.AddTo(this.gameObject);
break;
case AnimatorControllerParameterType.Int:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetInteger(parameter.name))
.Where(_ => _ != animator.GetInteger(parameter.name))
.Subscribe(_ => animator.SetInteger(parameter.name, _))
.AddTo(this.gameObject);
break;
// Triggerでまともに動くかは検証不足だが「Triggerは派手なBool」らしいからいけるんじゃない?
// https://forum.unity.com/threads/get-if-trigger-is-set-or-is-a-condition-of-a-state.1004269/
case AnimatorControllerParameterType.Trigger:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetBool(parameter.name))
.Where(_ => _ != animator.GetBool(parameter.name))
.Subscribe(_ => animator.SetTrigger(parameter.name))
.AddTo(this.gameObject);
break;
default:
Debug.LogError($"パラメータのタイプが異常です \n AnimatorControllerParameterType.{parameter.type}");
break;
}
}
}
}
}
2.備考
朝の寝ぼけ頭で作ったスクリプトにしては、そこそこの出来なのでは...?(自画自賛)
UniRxは勉強したてなので、あんまり自信ないです。もっと効率の良い書き方があったら教えて欲しい...。
とりすーぷさん曰く、ObserveEveryValueChangedはパフォーマンス悪いみたいなんだよな~。
そんなことはない。ObserveEveryValueChangedはパフォーマンス的に不利なので、ReactivePropertyが使えるならそっち使ったほうが絶対良いです。
— とりすーぷ (@toRisouP) January 24, 2022
UniRx の変数の監視は、ReactiveProperty より、ObserveEveryValueChanged を使うべき4つの理由 https://t.co/0n1RjSec6K
3.追記(2023/02/08)
後日談といったところになりますが、少し追記しようと思います。
まずReactivePropertyですが、こちらはお仕事で実際にReactivePropertyを使う機会があり、今回の要件では使用できないことが明らかになりました。
ReactivePropertyは普通に便利なので、UniRx使い始めた人はまずこれ勉強したほうがいいと思う...。
続いて、上記のコードですが。
このコードがはらむ大きな問題点として、ゲームスタート時に最初からある対象ならまだしも、後々で生成したものを同期対象として追加したい場合に、そもそもAnimatorControllerのパラメーターを同期させるプログラムである関係で、少しラグがあるというか時間間隔が微妙に空いた同期になってしまうことがありました。
あとはまあ...内部のパラメーターを指定する部分をステートの名前からハッシュにするなど、いくつか最適化した結果が以下のスクリプトになります。
using System.Collections.Generic;
using UniRx;
using UnityEngine;
namespace Sample.Animation
{
/// <summary>
/// 同じアニメーターコントローラーを使う二者のパラメーターを同期する
/// </summary>
[RequireComponent(typeof(Animator))]
public class AnimSyncManager : MonoBehaviour
{
[SerializeField, Tooltip("同期のターゲットとなるアニメーター")]
private Animator otherAnimator = null;
/// <summary>
/// 同期するアニメーター
/// </summary>
private Animator animator = null;
/// <summary>
/// 全同期対象のアニメーターを管理するリスト
/// </summary>
private static List<Animator> syncAnimators = new List<Animator>();
/// <summary>
/// 該当のアニメーションコントローラーレイヤー
/// </summary>
private const int LAYER_INDEX = 0;
/// <summary>
/// プレイヤーのタグ
/// </summary>
private const string PLAYER = "Player";
private void Start()
{
// 同期するアニメーターを取得
animator = GetComponent<Animator>();
animator.runtimeAnimatorController = otherAnimator.runtimeAnimatorController;
syncAnimators.Add(animator);
var info = otherAnimator.GetCurrentAnimatorStateInfo(LAYER_INDEX);
var nameHash = info.fullPathHash;
var normalizedTime = info.normalizedTime % 1; // ループしていると1以上になるので、小数点以下のみ抽出
foreach (var syncAnimator in syncAnimators)
{
syncAnimator.Play(nameHash, LAYER_INDEX, normalizedTime);
Debug.Log(nameHash);
//syncAnimator.Rebind();
}
// アニメーションによってはAnimationEventを吐いてエラーになることがあるので無効化する
animator.fireEvents = false;
// ターゲットのアニメーターのパラメーターを監視して変更があれば同期対象の方でも変更する
foreach (var parameter in animator.parameters)
{
switch (parameter.type)
{
case AnimatorControllerParameterType.Bool:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetBool(parameter.nameHash))
.Where(_ => _!=animator.GetBool(parameter.nameHash))
.Subscribe(_ => animator.SetBool(parameter.nameHash, _))
.AddTo(this.gameObject);
break;
case AnimatorControllerParameterType.Float:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetFloat(parameter.nameHash))
.Where(_ => Mathf.Abs(_ - animator.GetFloat(parameter.nameHash)) > 0)
.Subscribe(_ => animator.SetFloat(parameter.nameHash, _))
.AddTo(this.gameObject);
break;
case AnimatorControllerParameterType.Int:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetInteger(parameter.nameHash))
.Where(_ => _ != animator.GetInteger(parameter.nameHash))
.Subscribe(_ => animator.SetInteger(parameter.nameHash, _))
.AddTo(this.gameObject);
break;
// Triggerでまともに動くかは検証不足だが「Triggerは派手なBool」らしいからいけるんじゃない?
// https://forum.unity.com/threads/get-if-trigger-is-set-or-is-a-condition-of-a-state.1004269/
case AnimatorControllerParameterType.Trigger:
otherAnimator
.ObserveEveryValueChanged(_ => _.GetBool(parameter.nameHash))
.Where(_ => _ != animator.GetBool(parameter.nameHash))
.Subscribe(_ => animator.SetTrigger(parameter.nameHash))
.AddTo(this.gameObject);
break;
default:
Debug.LogError($"パラメータのタイプが異常です \n AnimatorControllerParameterType.{parameter.type}");
break;
}
}
}
private void OnDestroy() => syncAnimators.Remove(animator);
}
}
そもそも書き方から変わっている気がしますが、まあそれは成長って言うことで。
これで、このプログラムはカンペキ!
ちなみに、コメントアウトしてありますが、Animator.Rebind()
というメソッドで当初アニメーションをここでリセットしていました。
ただこのメソッド、強制Tポーズさせてしまうので、一瞬とはいえ違和感しかなく、排除せざるを得なかったという...。