[Unity]Animatorを任意のフレームレートで動作させる


E3で公開された「SABLE」の世界観素敵ですね・・・。

このゲームの魅力を支えているたくさんのテクニックの内のほんの一つですが、モーションのフレームレートをあえて落としてトゥーンシェーディングのグラをよりアニメ調に見せる方法 を以前Qiitaの記事にしました。

この記事ではPlayableAPIを使った実装を紹介していて、普通にAnimatorControllerを使って動かすモデルにはそのまま適用できず扱いづらいものだったので、AnimatorControllerをそのまま使ってフレームレートを落とす機能を作ったので、話題に便乗して紹介します!

こんな感じになります

※シェーダーはユニティちゃんシェーダーを当ててます

スクリプト

Animatorコンポーネントを持つオブジェクトに付与するAnimatorFPSController.csを作っています。

AnimatorFPSController.cs
using UnityEngine;

[RequireComponent(typeof(Animator))]
public class AnimatorFPSController : MonoBehaviour
{
    [SerializeField, Range(1, 30)]
    int _fps = 30;

    Animator _animator;

    /// <summary>しきい値時間</summary>
    float _thresholdTime;

    /// <summary>スキップされた更新時間</summary>
    float _skippedTime;

    private void Awake()
    {
        _animator = GetComponent<Animator>();
        _animator.enabled = false;
        InitializeThresholdTime();
    }

    /// <summary>
    /// しきい値時間の初期化
    /// </summary>
    void InitializeThresholdTime()
    {
        _thresholdTime = 1f / _fps;
    }

    private void Update()
    {
        _skippedTime += Time.deltaTime;

        if (_thresholdTime > _skippedTime)
        {
            return;
        }

        // アニメーションの時間を計算する
        _animator.Update(_skippedTime);
        _skippedTime = 0f;
    }

    /// <summary>
    /// Inspectorの値変更時の処理
    /// </summary>
    private void OnValidate()
    {
        InitializeThresholdTime();
    }
}

動作フレームレートを指定する変数_fpsに応じて計算される更新頻度(秒)変数_thresholdTimeの間だけ、Animatorの更新を止めています。
更新頻度の時間に到達したら、更新を止めていた時間を込みでAnimatorUpdateを進めて、モーションによって作られるポーズに更新する仕組みです。

雰囲気作り・絵作り以外の用途

高負荷になりがちなAnimatorの更新頻度を抑えて負荷削減する用途にも使えます。
市場のゲームだと、

  • マリオオデッセイのニュードンクシティの街の人々
  • モンハンワールドの一部の敵
  • ドラクエモンスターズ2 3DS版のフィールドの敵シンボル

などのモデルがカメラからの距離に応じてアニメーションのフレームレートを落とすような仕組みが施されていることが確認できました。
LODの一環としてこの仕組みを入れるのも良いかもしれません。

Animator以外にも、大量のオブジェクトやインスタンスの更新処理で負荷が高い場合、この手法で負荷を減らすこともできそうです。

負荷削減として扱う場合

更新頻度を減らすオブジェクトが大量にある場合、更新処理に到達するタイミングをズラす必要があります。
ズラさなかった場合、全てのオブジェクトの更新が同一フレームで走るため、一定間隔でスパイクしてしまいます。

AnimatorFPSController.cs
void InitializeThresholdTime()
{
    _thresholdTime = 1f / _fps;
    // 更新タイミングがバラつくように初期値に乱数を与える
    _skippedTime = Random.Range(0f, _thresholdTime);
}

このように_skippedTime_thresholdTimeを最大値とする乱数を与えることで、更新タイミングを散らすことができます。