Animator.StringToHashとは?

Animator.StringToHashは、Animatorのパラメータ名をハッシュ値に変換するためのメソッドです。アニメーターを操作するときに毎回文字列を渡すのではなく、起動時に一度だけハッシュ化しておいて、以降はそのハッシュ値で指定する……という最適化の定番テクニックですね。

SetFloatSetBoolSetTriggerといった主要なメソッドは、文字列を受け取るオーバーロードと、intのハッシュ値を受け取るオーバーロードの両方が用意されています。後者を使うことで、内部での文字列マッチングを回避できます。

なお、Animation Parametersそのものの種類や使い分け、基本的なキャッシュパターンは別記事でまとめてあります。本記事はそこから一歩踏み込んで、Animator.StringToHashの内部動作やstring.GetHashCodeShader.PropertyToIDとの違い、Stateハッシュへの応用、定数クラスでの管理判断など、最適化テクニック寄りの内容を中心に扱います。

なぜ高速になるのか

文字列で指定する場合、Animatorは内部で「そのパラメータ名がどのスロットに対応するか」を毎回検索します。Updateの中で何度も呼び出されると、このコストが地味に積み上がります。

一方、Animator.StringToHashで生成されるのは単純な整数値(ハッシュ)です。ハッシュ化の処理自体は起動時に1回だけ走り、以降のSetFloatなどは整数キーで直接スロットを引きにいけます。文字列比較がなくなる分、毎フレーム呼び出すパラメータでは効果が大きいです。

基本的な使い方

もっともシンプルなのは、static readonly intとしてクラス内にキャッシュしておく書き方です。readonlyにすると意図せず書き換えてしまう事故も防げます。

using UnityEngine;

public class PlayerAnimatorController : MonoBehaviour
{
    private Animator _animator;

    // パラメータ名をハッシュ値としてキャッシュ
    private static readonly int SpeedHash = Animator.StringToHash("Speed");
    private static readonly int JumpHash = Animator.StringToHash("Jump");

    private void Awake()
    {
        _animator = GetComponent<Animator>();
    }

    private void Update()
    {
        // ハッシュ値で指定して呼び出す
        _animator.SetFloat(SpeedHash, 5.0f);

        if (Input.GetKeyDown(KeyCode.Space))
        {
            _animator.SetTrigger(JumpHash);
        }
    }
}

staticにすると同じ名前のパラメータを使うすべてのインスタンスで共有できるので、メモリ的にも無駄がありません。インスタンス側に持たせるreadonly intパターンの基本形については、別記事の方で詳しく扱っているのでそちらも参考にしてみてください。

ハッシュ版オーバーロードを使える主なメソッド

Animatorの主要なメソッドはハッシュ版が揃っているので、置き換えやすいです。

// 値の設定(パラメータID版)
_animator.SetFloat(SpeedHash, 5.0f);
_animator.SetInteger(ComboCountHash, 2);
_animator.SetBool(IsGroundedHash, true);
_animator.SetTrigger(JumpHash);
_animator.ResetTrigger(JumpHash);

// 値の取得(パラメータID版)
float speed = _animator.GetFloat(SpeedHash);
bool isGrounded = _animator.GetBool(IsGroundedHash);

加えて、StateやLayerについてもハッシュで扱えます。PlayCrossFadeにステート名のハッシュを渡すと、文字列指定より軽く済みます。遷移時間を指定するCrossFadeにも同じハッシュをそのまま渡せます。

private static readonly int AttackStateHash = Animator.StringToHash("Attack");

private void PlayAttack()
{
    // ステート名のハッシュを使って再生
    _animator.Play(AttackStateHash);

    // フェードしながら遷移したい場合も同じハッシュでOK
    _animator.CrossFade(AttackStateHash, 0.2f);
}

Layer名についてもGetLayerIndexで取得したインデックスをキャッシュしておくと、SetLayerWeightの呼び出しを軽くできます。

private int _upperBodyLayerIndex;

private void Awake()
{
    _upperBodyLayerIndex = _animator.GetLayerIndex("UpperBody");
}

どれくらい効果があるのか

体感差が出やすいのは、UpdateFixedUpdateで毎フレーム値を更新するようなパラメータです。SpeedVelocityXVelocityZのように、移動中ずっと書き込み続けるものは特に効きやすいです。

特に差が顕著になりやすいのは、

  • 群衆シーンやRTS、横スクロールなど、Animatorを持つキャラクターを数十〜数百体同時に動かすケース
  • 1キャラクターあたりでSetFloatを毎フレーム何種類も呼んでいるケース
  • モバイルなどCPU性能が限られる環境

といった条件のときです。逆に、ジャンプボタンを押した瞬間にだけ呼ばれるようなSetTriggerは、文字列のままでも実害は出にくいです。

実際に効くかどうかは環境やシーン構成で変わるので、気になる場合はProfilerAnimators.WriteAnimationParametersなどを見比べてみると判断しやすいです。とはいえ統一しておいた方がコードの見た目もそろうので、基本的にはハッシュ版に寄せておくのが無難かなと思います。

注意点

タイプミスがコンパイルでは弾けない

Animator.StringToHashの引数はあくまで文字列なので、"Speed""Sped"と書いてしまってもコンパイルは通ってしまいます。実行時に「アニメーションが切り替わらないな?」と気づくことになりがちです。

対策としては、パラメータ名を一箇所に集約する専用クラスを用意しておくとミスを減らせます。命名は既存記事の例と揃えてAnimationParametersとしておくと、プロジェクト内で参照が散らばらず管理が楽です。

using UnityEngine;

/// <summary>
/// Animatorパラメータのハッシュ値を一元管理する
/// </summary>
public static class AnimationParameters
{
    // 移動関連
    public static readonly int Speed = Animator.StringToHash("Speed");
    public static readonly int VelocityX = Animator.StringToHash("VelocityX");
    public static readonly int VelocityZ = Animator.StringToHash("VelocityZ");

    // 状態関連
    public static readonly int IsGrounded = Animator.StringToHash("IsGrounded");

    // アクション関連
    public static readonly int Jump = Animator.StringToHash("Jump");
    public static readonly int Attack = Animator.StringToHash("Attack");
}

呼び出し側はこんな感じになります。

_animator.SetFloat(AnimationParameters.Speed, currentSpeed);
_animator.SetTrigger(AnimationParameters.Jump);

参照元が1箇所にまとまるので、パラメータ名を変更したときの修正も最小限で済みます。

定数クラスにまとめるかどうかの判断基準

一元管理クラスはどんなプロジェクトでも常に必須、というわけでもありません。目安はこんな感じ。

  • 同じパラメータ名を複数のスクリプトから触っている → 一元管理クラスを使う
  • 1キャラ専用のスクリプトしか触らない短いプロジェクト → クラス内にstatic readonly intを置くだけで十分
  • パラメータ数が増えてきて、命名のゆれや打ち間違えが目立ち始めた → 一元管理に切り替える

最初は小さく書いておいて、規模が膨らんできたタイミングでAnimationParametersのような形に寄せていく流れで問題ありません。

Animator Controller側との名前一致を保つ

ハッシュは「文字列をもとに計算した整数値」なので、Animator Controller側でパラメータ名を変えたら、コード側のハッシュ生成元の文字列もセットで直す必要があります。リネーム時は両方とも、と覚えておくと安心。

ハッシュ値をシリアライズしない

ハッシュ値そのものをSerializeFieldで保存するのはおすすめしません。ハッシュ生成のアルゴリズムが将来変わる可能性もありますし、パラメータ名を変更したときに気づきにくくなるためです。ハッシュは起動時に毎回計算する、という運用が基本。

string.GetHashCodeとは別物

「文字列のハッシュならstring.GetHashCode()で代用できそう」と思うかもしれませんが、これはNG。Animator内部で使われているハッシュ計算アルゴリズムはstring.GetHashCode()とは別物で、互換性もありません。さらにstring.GetHashCode()はランタイムやバージョンによって戻り値が変わる可能性があり、Animatorのスロット検索キーとして使うのは前提から外れます。素直にAnimator.StringToHashの戻り値を使ってあげてください。

Shader.PropertyToIDとの違い

UnityにはほかにShader用のShader.PropertyToIDもありますが、こちらも別物です。Shader.PropertyToIDはマテリアルやシェーダーのプロパティID用、Animator.StringToHashはAnimatorのパラメータ/ステート/レイヤー用、と内部で参照しているテーブルが完全に分かれています。

// Shader用
private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor");

// Animator用
private static readonly int SpeedHash = Animator.StringToHash("Speed");

同じ「文字列からIDへ」という形でも互換性はないので、用途ごとに使い分けてあげてください。

まとめ

Animator.StringToHashは、パラメータ名をハッシュ値に変換して文字列マッチングを回避する、地味だけど効くタイプの最適化です。基本はstatic readonly intとしてキャッシュしておき、SetFloatSetBoolSetTriggerに加えてPlayCrossFadeなどのState指定にも同じやり方が応用できます。

毎フレーム呼ぶパラメータほど効果が出やすく、タイプミスがコンパイルで弾けない弱点についてはAnimationParametersのような定数クラスで一元管理しておけばカバーできます。導入コストもほぼないので、Animatorを触るスクリプトを書くときは最初からハッシュ版で書く癖をつけておくのがおすすめです。