【Unity】【C#】UniRx で「1フレームごと待機して処理」してみる 
2018/10/19 Fri [edit]
今まで「VRM Live Viewer」では非同期処理に「Await Extensions」というライブラリを使っていたのだが、「UniRx」の非同期処理関連の資料を見てたら、同じようなことができるとわかったので、移行してみようと考えた。

ただ注意しなくてはならないのは、「UniRx」の非同期処理は Scripting Runtime を「.NET 4.x」にすることと、C# 7.0が必須のため、現時点(Unity2018.2.x)では「Incremental Compiler」を導入する必要があるとのことだ。
・UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合
「Incremental Compiler」はまだ Preview 版のため、商用利用は控えたほうが良いともあるね。まぁ、「VRM Live Viewer」はフリーなのと、色々実験してみたが、安定性にも問題無いようなので(どちらかというと Stable[安定版] が出たら、メソッドの仕様(引数など)が変わって、書き換え必須となることの方が辛いね(笑))、導入を試みてみたら成功した。実際「UniRx」上で使える「UniTask」は「async/await/Task」の上位互換で、現在私が使っている機能もそのまま移行できたので、基本的な使い方ならば、バージョンアップ・正式版が出てもそれほど問題ないとも思う。また今後UI処理などを少し強化したいしね。そういったものは「UniRx」はとても強い(笑)。
なので、現時点では導入する人は少ないかも知れないが、「.NET 4.x」の非同期処理「async/await/Task」が使えるようになると、コルーチンで書いていたものがスッキリする上に、別スレッドでバックグラウンド処理などもとても簡単になるので、いずれは誰しも使うようになるだろう(笑)。そしてはじめに使いたいのはやはり、今までコルーチンなどで書いていた「1フレーム待機して処理」かなと(笑)。ググったらなぜかあまりハッキリとした答えがなかったので、ちょっと実験してみた感じ。たぶん他の人も同じことを調べるだろうしね。
●コルーチンで1フレームごとに待機して処理(ログ出力のみ。動作の基準)
●Observable.NextFrame() で1フレームごとに待機して処理?
●Observable.TimerFrame() で1フレームごとに待機して処理(引数=1)?
●Observable.TimerFrame() で1フレームごとに待機して処理(引数=0)?
●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=1)?
●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=0)?
●UniTask.DelayFrame() で1フレームごとに待機して処理?
●UniTask.WaitUntil() で1フレームごとに待機して処理?
●UniTask.Yield() で1フレームごとに待機して処理?
■とりあえず「1フレームごとに待機して処理」を簡単に(実験してみた結果)
(※) Unity 2018.2.1f1 / UniRx 6.2.2 / Incremental Complier 0.0.42(Preview) / Windows10(x64) で確認
■「1フレームごとに待機して処理」っぽくなりそうなものを、色々やってみる
まずは今まで通り、コルーチンで「1フレームごとに待機して処理」をしてみる。処理自体はただのログ出力なので、実際の処理の重さは考慮に入れてない。ちなみに「Await Extensions」では「yield return new WaitForEndOfFrame()」の代わりに「await new WaitForEndOfFrame()」が使える(そしてメソッドに async が使えるので非同期処理が簡潔に書ける。StartCoroutine() もいらない)。
●コルーチンで1フレームごとに待機して処理(ログ出力のみ。動作の基準)
using System.Collections;
using UnityEngine;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
StartCoroutine(WaitForEndOfFrameCoroutineTest());
}
IEnumerator WaitForEndOfFrameCoroutineTest()
{
Debug.Log("frame : " + Time.frameCount); //1
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //2
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //3
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //4
yield return new WaitForEndOfFrame();
Debug.Log("frame : " + Time.frameCount); //5
}
}
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5
なんてことはないコルーチンで順次処理していくときのようなコード(本来は同じ処理ならループにするだろうが、これはあくまでサンプルなので、実際には別々の処理が入ると考えて欲しい)。これを基準として「UniRx」でオペレータなども使って色々試してみよう。なお、ここでは「yield return new WaitForEndOfFrame()」を使っているが、実行タイミングを気にしないなら「yield return null」でも良い。
・イベント関数の実行順
●Observable.NextFrame() で1フレームごとに待機して処理?
using System.Collections;
using UnityEngine;
using UniRx;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
ObservableNextFrameTest();
}
async void ObservableNextFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //3
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //5
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //7
await Observable.NextFrame();
Debug.Log("frame : " + Time.frameCount); //9
}
}
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9
UniRx で一番始めに目についたのは「Observable.NextFrame()」だったが、どうやら await で1フレーム、NextFrame() で1フレーム待機のようだ。フレームが1つ飛びになっていた。期待していた処理とは違う。
●Observable.TimerFrame() で1フレームごとに待機して処理(引数=1)?
using System.Collections;
using UnityEngine;
using UniRx;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
ObservableTimerFrameTest();
}
async void ObservableTimerFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //5
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //7
await Observable.TimerFrame(1);
Debug.Log("frame : " + Time.frameCount); //9
}
}
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9
Observable.NextFrame() と結果は同じになった。ではちょっと引数を0にしてみようと実験してみると…
●Observable.TimerFrame() で1フレームごとに待機して処理(引数=0)
using System.Collections;
using UnityEngine;
using UniRx;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
ObservableTimerFrameTest();
}
async void ObservableTimerFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
await Observable.TimerFrame(0);
Debug.Log("frame : " + Time.frameCount); //5
}
}
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5
いけた(笑)。これは期待していた処理と合致する。
●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=1)?
using System.Collections;
using UnityEngine;
using UniRx;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
ObservableDelayFrameTest();
}
async void ObservableDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //5
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //7
await Observable.ReturnUnit().DelayFrame(1);
Debug.Log("frame : " + Time.frameCount); //9
}
}
frame : 1
frame : 3
frame : 5
frame : 7
frame : 9
これも Observable.NextFrame() と結果は同じになった。では引数を0にしてみると…
●Observable.ReturnUnit().DelayFrame() で1フレームごとに待機して処理(引数=0)
using System.Collections;
using UnityEngine;
using UniRx;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
ObservableDelayFrameTest();
}
async void ObservableDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
await Observable.ReturnUnit().DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //5
}
}
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5
これもいけた。これは期待していた処理と合致する。実装自体は Observable.TimerFrame() とは違うみたいだけどね。
●UniTask.DelayFrame() で1フレームごとに待機して処理?
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
UniTaskDelayFrameTest();
}
async void UniTaskDelayFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //2
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //3
await UniTask.DelayFrame(0);
Debug.Log("frame : " + Time.frameCount); //4
}
}
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
せっかくのなで、UniTask も試してみた。するとあれ?なぜか初めの1回は待機されてない。なんか理由があるのだろうけど、今回は放おっておこう(←誰か調べて(笑))。こちらは「using UniRx.Async;」が必要。
●UniTask.WaitUntil() で1フレームごとに待機して処理?
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
UniTaskWaitUntilTest();
}
async void UniTaskWaitUntilTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //2
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //3
await UniTask.WaitUntil(() => true);
Debug.Log("frame : " + Time.frameCount); //4
}
}
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
UniTask.DelayFrame() と同じ結果になった。これもはじめの1回が待機されてない…?ま、まぁ、今回はコルーチンでの表記をそのまま代替できる書き方を探していただけなので(パフォーマンスなども考慮に入れてない)、これで勘弁してやろう(笑)。誰かそのうち調べてくれるだろうと期待してる(←投げっぱなし(笑))。
●UniTask.Yield() で1フレームごとに待機して処理?
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
UniTaskYieldTest();
}
async void UniTaskYieldTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.Yield();
Debug.Log("frame : " + Time.frameCount); //1
await UniTask.Yield();
Debug.Log("frame : " + Time.frameCount); //2
await UniTask.Yield();
Debug.Log("frame : " + Time.frameCount); //3
await UniTask.Yield();
Debug.Log("frame : " + Time.frameCount); //4
}
}
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4
これも UniTask.DelayFrame() と同じ結果になった。これもはじめの1回が待機されてない?
本来はメインスレッドに切り替えたり、PlayerLoopに同期したりするのに使うみたいだが、次のフレームになるので、近い動作になるっぽい。
[追記] 以降のバージョンでは以下のようなことが書かれてます。
簡単に言えば「UniTask.NextFrame()」を使えば、「yield return null」と同じように、確実に次のフレームになる、だそうです。
■とりあえず「1フレームごとに待機して処理」を簡単に(実験してみた結果)
他にも色々実験している記事もあったが、単純に面倒なので、とりあえず「Observable.TimerFrame(0)」または「Observable.ReturnUnit().DelayFrame(0)」を代替として使うことにしてみた。もしからしたら正しい使い方ではないかも知れないので、static な関数(WaitForFrame)にしておいて、後で書き換えられるようにしておけば、修正も楽かも知れない。「Observable.TimerFrame(0)」は UniRx 特有の「マイクロコルーチン」というものを使っているので(負荷が軽いらしい)、今回はこちらで書いておこう。まぁ、戻値の型の問題はあるが、あくまでコルーチンでの「yield return null」みたいな使い方を想定しているので、今回は気にしないとする(笑)。Observable のように拡張メソッドにしても良いと思うけど(「Observable_Joins.cs」に書くとか)、その辺はご自由に。
●とりあえず1フレームごとに待機して処理
using System.Collections;
using UnityEngine;
using UniRx;
using UniRx.Async;
public class EachFrameTest : MonoBehaviour {
// Use this for initialization
void Start () {
Debug.Log("(Start) frame : " + Time.frameCount); //1
WaitForFrameTest();
}
async void WaitForFrameTest()
{
Debug.Log("frame : " + Time.frameCount); //1
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //2
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //3
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //4
await WaitForFrame();
Debug.Log("frame : " + Time.frameCount); //5
}
public static IObservable<long> WaitForFrame()
{
return Observable.TimerFrame(0);
}
}
frame : 1
frame : 2
frame : 3
frame : 4
frame : 5
「Observable.TimerFrame()」だと戻値の型が long となっているので、Unit にしたいなら、以下のようにしても良いかも知れない(まぁ、今回の使い方のように、捨て値なら無駄な処理が増えるだけだが…)。
●戻値を Unit に変えた例
public static IObservable<Unit> WaitForFrame()
{
return Observable.TimerFrame(0).AsUnitObservable();
}
こちらの記事にも書いてあるけど「コルーチンをほぼ駆逐」できると表記がすっきりし、しかもコルーチンと違って、別スレッド動作や非アクティブなオブジェクトでも動くので(というより、MonoBehaviour に依存しないで動作できるので)、いずれは「async/await」的な書き方の方が主流になるかもね。「VRM Live Viewer」は商用アプリにするつもりは無いし、実験的にやってみることに意義があると思ってるので(ファイルドロップ→非同期なファイル読み込みは既に実装されてる)、前のめりで新しい機能を導入していきたい(笑)。
(参考)
・Unity UniRxとasync/awaitでフレーム管理
・UniRx.Async機能紹介
・UniTask - Unity + async/awaitの完全でハイパフォーマンスな統合
・Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映
(関連記事)
【Unity】UniRx 7.x にアップグレードすると UniTask 系のエラーが出る
- 関連記事
トラックバック
トラックバックURL
→http://fantom1x.blog130.fc2.com/tb.php/313-d88ac9a3
この記事にトラックバックする(FC2ブログユーザー)
| h o m e |