LoginSignup
7
9

More than 5 years have passed since last update.

【Unity(C#)】WaitForFixedUpdateを使って時間ぴったりの処理を行う

Last updated at Posted at 2019-02-16

  
  
  
この記事は

『プログラミング完全未経験からUnityでの開発現場に迎え入れてもらえた世界一の幸せ者』

の記事です。そのつもりでお読みください。

はじめに

前回、前々回の記事をベースに話が進みます。
前回→SteamVR Pluginを使わないVRでのフェード
前々回→3Dのフェード

別に読まなくても超初心者級のことしかしてないので大丈夫だと思います。

コルーチンで時間指定したのにズレる

コルーチンを使えば、指定した時間に呼び出せる!
調べたら↑が出てきたので「なんて便利な機能なんだ!」と思って使いまくってました。
しかし、秒数指定をしたループ処理で、何百、何千回と呼び出したところ、
微妙にずれてました。

あまり時間を気にしない場合は構わないのですが、厳密に秒数指定したい場合に
どうすればいいのかわからなかったので調べました。

コルーチンの仕組み

UnityのDocumentationを参考に説明を進めます。

スクリプトライフサイクルフローチャートのページを見てください。

説明に不要なものを省いたものがこちらです。(図①)
スクリプトライフサイクルフローチャートCoroutine.png

図①の矢印をたどると、
Updateyield WaitForSecondsとなってます。

また、
FixedUpdateyield WaitForFixedUpdateともなっています。

実際にコードを見ながら、具体例を挙げて説明していきます。

なぜズレたのか

前提条件として、FPSは90で安定しているものとします。
つまり、1秒÷90フレーム=0.011...となり、
1フレームあたり0.011秒の時間が経過するということです。
(厳密に言うと違いますがそれは後程説明します)

コルーチン内で0.1秒ごとに処理が行われるよう指定したループがあります。

while (true)
        {
            yield return new WaitForSeconds(0.1f);
            //なんかいろいろな処理
        }

yeild WaitForSeconds(指定秒数)指定秒数を超えるまで待機yeild WaitForSeconds以降の処理

となるのですが、指定秒数を超えるまで待機するというのは全ての処理が止まるわけではありません。
全ての処理が止まってしまったら
指定秒数を超えたことを誰に確認してもらえばいいんだ?という問題に陥ります。

つまり、指定秒数を超えたことを確認するために動いている人がいます。
それがUpdateです。

しかし、Updateは1フレームごとにしか呼ばれません。
毎フレーム0.1秒を超えたか確認しようと頑張ります。

1フレーム目:0.011→まだ超えていない
2フレーム目:0.022→まだ超えていない
3フレーム目:0.033→まだ超えていない
    ・
    ・
    ・
8フレーム目:0.088→まだ超えていない
9フレーム目:0.099→まだ超えていない
10フレーム目:0.11 →超えた!

いや過ぎてますやん
0.1秒後ではなく0.11秒後に超えたことを認識しています。
これが誤差の原因です。

たった1回の処理なら誤差は0.01秒ですが、これが100回、1000回となると
1秒、10秒と膨れ上がります。

WaitForFixedUpdate

先程の例で鋭い人は
「じゃあ0.011秒間隔で秒数指定すれば誤差0じゃん!」
と思ったかもしれません。

ですが、Updateは1フレームが等間隔で呼ばれ続けるものではありませんのでそれは無理です。
様々な処理に影響されるので安定してFPSをキープしたとしても、微妙にずれてしまいます。

そこで役に立つのがFixedUpdateです。
FixedUpdateは毎フレーム等間隔で呼ばれます。
そんな便利なFixedUpdateはコルーチンにも活かされています。

それがWaitForFixedUpdateです。
このように使います。

while (true)
        {
            yield return new WaitForFixedUpdate();
            //なんかいろいろな処理
        }

FixedUpdateのフレーム間隔が0.02であった場合、
誤差無く0.02秒間隔で呼ばれます。

改善したコード

前回の記事をもとに
VRのフェードの実装をいい感じに頑張りました。

サブカメラの動的な生成から設定までStart内で行うようにしてます。

VRでなくても動くので本当に指定した秒数で呼ばれているか確認したい人は
コピペして動かしてみてください。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class UtilityFadeCanvas : Monobehavior
{
    Canvas faceCanvas;

    Image facePanel;

    Camera faceCamera;

    Coroutine coroutine;

    GameObject camera_G;

    [Tooltip("フェードする色")]
    public Color panelColor = new Color(0, 0, 0, 0);

    [Tooltip("フェードの時間")]
    public float fadeTime = 1;

    [Tooltip("0~1の値で透明度を設定")]
    public float alpha_Panel = 0.5f;

    const float FIXED_UPDATE_DELTATIME = 0.02f;

    float startTime;

    void Awake()
    {
        //シーンをロードするたびに新しいカメラを生成
        if (GameObject.Find("OnlyUIRenderingCamera"))
        {
            Destroy(GameObject.Find("OnlyUIRenderingCamera"));
        }
    }

    // Start is called before the first frame update
    void Start()
    {

        //カメラ自動生成
        camera_G = new GameObject("OnlyUIRenderingCamera");
        faceCamera = camera_G.AddComponent<Camera>();
        faceCamera.clearFlags = CameraClearFlags.Depth;
        faceCamera.cullingMask = (1 << LayerMask.NameToLayer("UI"));

        //キャンバス生成&設定
        GameObject canvas_G = new GameObject("FaceCanvas");
        faceCanvas = canvas_G.AddComponent<Canvas>();
        canvas_G.AddComponent<CanvasRenderer>();


        //キャンバスのポジションを調整
        Vector3 canvasPosition = canvas_G.transform.position;
        canvasPosition.x = 0;
        canvasPosition.y = 0;
        canvasPosition.z = 0.1f;
        canvas_G.transform.localPosition = canvasPosition;

        //レンダリングをfaceCameraに
        faceCanvas.renderMode = RenderMode.ScreenSpaceCamera;
        faceCanvas.worldCamera = faceCamera;

        //パネル生成&設定
        GameObject panel_G = new GameObject("FacePanel");
        facePanel = panel_G.AddComponent<Image>();

        Color tmpColor = facePanel.color;
        tmpColor.a = 0f;
        facePanel.color = tmpColor;

        //パネルをキャンバスの子に設定
        panel_G.transform.parent = canvas_G.transform;

        //パネルのポジションを正面に調整
        Vector3 panelPosition = panel_G.transform.localPosition;
        panelPosition.x = 0;
        panelPosition.y = 0;
        panelPosition.z = 0;
        panel_G.transform.localPosition = panelPosition;


        //キャンバスをカメラの子に設定
        canvas_G.transform.parent = faceCamera.transform;

        //Layerを変更
        canvas_G.layer = LayerMask.NameToLayer("UI");
        panel_G.layer = LayerMask.NameToLayer("UI");

    }

    void Update()
    {
        //Fixed Timestepを固定
        Time.fixedDeltaTime = FIXED_UPDATE_DELTATIME;

    }

    void FixedUpdate()
    {
        //キー押してない間はreturn
        if (Input.anyKey == false)
        {
            return;
        }

        if (coroutine == null)
        {
            //テスト用 フェードアウト
            if (Input.GetKeyDown(KeyCode.O) && panelColor.a == 0)
            {
                FadeOut();
            }

            if (Input.GetKeyDown(KeyCode.I) && panelColor.a == alpha_Panel)
            {
                FadeIn();
            }
        }
    }


    public  void FadeOut()
    {
        if (panelColor.a == 0)
        {
            //スタートの時間記録
            startTime = Time.time;
            print("フェードアウト開始");

            coroutine = StartCoroutine(FadeOutCoroutine());
        }
    }


    public  void FadeIn()
    {
        if (panelColor.a == alpha_Panel)
        {
            //スタートの時間記録
            startTime = Time.time;
            print("フェードイン停止");

            coroutine = StartCoroutine(FadeInCoroutine());
        }
    }

    IEnumerator FadeOutCoroutine()
    {
        int count = 0;

        yield return new WaitForFixedUpdate();
        while (facePanel.color.a < alpha_Panel - 0.00005f)
        {
            yield return new WaitForFixedUpdate();
            panelColor.a += alpha_Panel / (fadeTime * 50);
            facePanel.color = panelColor;

            count++;

            //フェード中の時間、Alphaを確認
            print(Time.time - startTime);
            print("アルファの値:" + panelColor.a + ":" + count + "回目");

        }
        print("フェードアウト停止");
        panelColor.a = alpha_Panel;

        StopCoroutine(coroutine);
        coroutine = null;
    }


    IEnumerator FadeInCoroutine()
    {
        int count = 0;

        yield return new WaitForFixedUpdate();
        while (panelColor.a > 0 + 0.00005f)
        {
            yield return new WaitForFixedUpdate();
            panelColor.a -= alpha_Panel / (fadeTime * 50);
            facePanel.color = panelColor;

            count++;

            //フェード中の時間、Alphaを確認
            print(Time.time - startTime);
            print("アルファの値:" + panelColor.a + ":" + count + "回目");

        }
        print("フェードイン停止");
        panelColor.a = 0;

        StopCoroutine(coroutine);
        coroutine = null;
    }
}

追加したコード

FixedUpdateのフレーム間隔

const float FIXED_UPDATE_DELTATIME = 0.02f;

void Update()
    {
        //Fixed Timestepを固定
        Time.fixedDeltaTime = FIXED_UPDATE_DELTATIME;

    }

もちろんEditorで設定することもできます。
Project SettingsTimeFixed Timestepの項目です。
ProjectSettingTime.png

シーンのロード時の処理

Start内でカメラ、キャンバス、パネルを生成しているので
シーンを破棄して読み込むたびに2つ、3つと増えてしまっていました。

なのでロードの度に古いカメラ、キャンバス、パネルを消す処理を加えました。

カメラの子にキャンバス、パネルを設定しているので
カメラを消せば全て消えます。

void Awake()
    {
        //シーンをロードするたびに新しいカメラを生成
        if (GameObject.Find("OnlyUIRenderingCamera"))
        {
            Destroy(GameObject.Find("OnlyUIRenderingCamera"));
        }
    }

誤差のないループ処理

WaitForFixedUpdateを利用してフレームを固定することで
呼び出し時間による誤差は改善できましたが、
alphaを加算代入するたびに微妙にずれるという煩わしい現象に悩まされました。

恐らくこれ→浮動小数点数型のズレ

なのでそのズレを想定してあらかじめ引いておくという
パワープレーで乗り切りました。

IEnumerator FadeOutCoroutine()
    {
        yield return new WaitForFixedUpdate();
        while (facePanel.color.a < alpha_Panel - 0.00005f)
        {
            yield return new WaitForFixedUpdate();
            panelColor.a += alpha_Panel / (fadeTime * 50);
            facePanel.color = panelColor;
        }
        panelColor.a = alpha_Panel;

        StopCoroutine(coroutine);
        coroutine = null;
    }

最後に

私の記事にしては長くなりました。読んでくださった方ありがとうございます。

実際に動かすとこんな感じで実行回数と秒数が見れます。
ぴったり5.png

何秒まで誤差がないか実験したところ、
180秒まで誤差0でした。(厳密に0なのか保証はできませんが)

これを使えば
"時間制限でだんだん視界が奪われる"
といったギミックとかにも活かせそうです。

7
9
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
9