この記事は
『プログラミング完全未経験からUnityでの開発現場に迎え入れてもらえた世界一の幸せ者』
の記事です。そのつもりでお読みください。
##はじめに
前回、前々回の記事をベースに話が進みます。
前回→SteamVR Pluginを使わないVRでのフェード
前々回→3Dのフェード
別に読まなくても超初心者級のことしかしてないので大丈夫だと思います。
##コルーチンで時間指定したのにズレる
コルーチンを使えば、指定した時間に呼び出せる!
調べたら↑が出てきたので「なんて便利な機能なんだ!」と思って使いまくってました。
しかし、秒数指定をしたループ処理で、何百、何千回と呼び出したところ、
微妙にずれてました。
あまり時間を気にしない場合は構わないのですが、厳密に秒数指定したい場合に
どうすればいいのかわからなかったので調べました。
##コルーチンの仕組み
UnityのDocumentationを参考に説明を進めます。
スクリプトライフサイクルフローチャートのページを見てください。
図①の矢印をたどると、
Update
→yield WaitForSeconds
となってます。
また、
FixedUpdate
→yield 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 Settings
→Time
→Fixed Timestep
の項目です。
###シーンのロード時の処理
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;
}
##最後に
私の記事にしては長くなりました。読んでくださった方ありがとうございます。
何秒まで誤差がないか実験したところ、
180秒まで誤差0でした。(厳密に0なのか保証はできませんが)
これを使えば
"時間制限でだんだん視界が奪われる"
といったギミックとかにも活かせそうです。