FC2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »
最近の記事

【Unity】【C#】ランタイム時にファイルをドラッグ&ドロップして取得する(Windows のみ)  


 Unity では標準でランタイム時のファイルのドラッグ&ドロップには対応してないそうで、結局ググって見つけたオープンソースライブラリで解決できたので、同じように検索している人のためにメモとして残しておこう。



(※) Unity 2018.2.1f1 - 2.16f1 / Windows10(x64) で確認



■Bunny83/UnityWindowsFileDrag-Drop をインポートする

 利用するオープンソースライブラリは「Bunny83/UnityWindowsFileDrag-Drop」だ。Git で落としても良いが、実は dll なども一切無く、実際に利用するファイルは C# ファイル1つだけなので(サンプルは除くとして)、zip で落とした方が楽だろう。


 zip を解凍したら、4つのファイルが出てくるのでプロジェクトにインポートする(プロジェクトビューにドロップで良い)。ちなみにドラッグ&ドロップのライブラリは「B83.Win32.cs」だけで、「FileDragAndDrop.cs」はサンプルとなっている。





■簡単なサンプルを作ってみる

 実際にランタイムで試すために、「FileDragAndDrop.cs」を適当にアタッチして、UI-Text にドロップしたファイル名を表示するようにしてみよう。適当に Canvas を作って、Text にドロップしたときのログを出力すれば良いだけだ。コードは元のサンプルコードに少し手を加えて、の Debug.Log() 部分をまるっとコピーすれば良い。




●「FileDragAndDrop.cs」に UI-Text への出力を付け加える
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; //追加
using System.Linq;
using B83.Win32;

public class FileDragAndDrop : MonoBehaviour
{
public Text display; //インスペクタで出力するUI-Textを登録

UnityDragAndDropHook hook;

//ここでドラッグ&ドロップ機能を登録
void OnEnable ()
{
hook = new UnityDragAndDropHook();
hook.InstallHook();
hook.OnDroppedFiles += OnFiles;
}

//ここでドラッグ&ドロップ機能を解除
void OnDisable()
{
hook.UninstallHook();
}

//ここでドラッグ&ドロップされたファイル名とドロップ位置を取得
void OnFiles(List<string> aFiles, POINT aPos)
{
Debug.Log("Dropped "+aFiles.Count+" files at: " + aPos + "\n"+
aFiles.Aggregate((a, b) => a + "\n" + b));

//UI-Text に出力
if (display != null)
{
display.text = ("Dropped "+aFiles.Count+" files at: " + aPos + "\n"+
aFiles.Aggregate((a, b) => a + "\n" + b));
}
}
}




■実際にビルドしてランタイムで試してみる

 「File>Build Settings...」で「Standalone」に切り替えて、作ったサンプルシーンを追加し、「Build And Run」でビルドして起動してみよう。適当にファイルをウィンドウにドラッグ&ドロップすれば、ファイル名が表示されるハズだ。



 とても簡単で、既に「VRM Live Viewer」で実装していて、何の問題も無く利用できている。インポートファイルも少ないので管理も楽だ。

 また、日本語が入ったパス名でも大丈夫のようだ。パス名だけでなく、ドロップした座標も取得できるので、ドロップした UI によって処理を変えるなんてこともできるだろう。これは色々使えると思う。てゆかこの機能はぜひ標準で欲しいね(笑)。





(関連記事)
【Unity】VRM(VRoid)をライブステージで踊らせるアプリを作ってみた
【Unity】【C#】uGUI ドロップダウンの要素をコードで設定と取得、外観のカスタマイズなど


スポンサーサイト

category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityオープンソースライブラリ  C# 
tb: 0   cm: --

【Unity】【C#】LINQとfor, ListとArray(配列)での実行速度を比較してみる  


 LINQ や List はとても便利だけど、実行速度が遅いという欠点があるので、改めてよく使いそうなコードで比較してみたのを記録しておく。

 私はよくこういう実験をするのだが、気をつけて欲しいのは実行環境やマシンスペック、言語などによっても結果は異なるという点だ。また、コンパイラ等もバージョンによって最適化が入ったりして、高速になる場合もある。今回は C# で Unity 上(主にエディタ上)での実験になってるが、実機ではまた異なる結果が出る場合があるので、あくまでも目安として考えて欲しい。

 ちなみに元ネタではないが、以下の記事でも通常の for, foreach 構文と LINQ の ForEach を実験しているが、特に ForEach が遅かったのが記憶に残っていたので、改めて調べたキッカケだったりする(記事のバージョンが古いので)。だがしかし、現64bitバージョンでも値は2倍速くらいになっているが、結果(比率)は同じだった。例えば for に対してLNQ の ForEach はやはり2倍くらい遅い。こういったデータを公開してくれるのは非常に有り難い。

(参考)
【Unity】ループ構文の処理速度の検証結果


(※) Unity 2018.2.1f1(エディタ上).NET 3.5 / Windows10(x64), Intel Core i5 x64 2.9GHz で確認



■LINQ と for 構文で比較

 ここでは簡単な数値計算でのフィルタと、全要素の文字列を LINQ と単純な for で比較してみよう。データには List を使っているが、Array(配列)にした方が速くなるのはとりあえず置いておいて欲しい(笑)。

 実際の測定方法は以下のコードを1つのメソッドにして、11回実行し、はじめの1回の結果を捨て、残りの10回の平均を出している。なぜはじめの1回を捨ててるかというと、アプリを起動したときには色々な初期化処理が動くので重くなり、測定値が不安定になるからだ(だいたい通常より値が大きくなる)。Unity に限らず、他のプラットフォームでも同じことがよく起こるので、その辺は覚えておくと良いかも知れない。



●LINQ の ForEach と for の単純アクセス(値取り出し)比較

 例えば、順次アクセスしていって、値を取り出すだけのコードを比較してみた。通常はその値を何らかで利用すると考えて欲しい。

①LINQ で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
List<int> list = Enumerable.Range(0, num).ToList();

//以下を測定
float startTime = Time.realtimeSinceStartup;

list.ForEach(i => {
int value = list[i];
});

float elapsed = Time.realtimeSinceStartup - startTime;

Debug.Log("1 : " + elapsed + " [s]"); //0.002561998 [s]

②for で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
List<int> list = Enumerable.Range(0, num).ToList();

//以下を測定
float startTime = Time.realtimeSinceStartup;

for (int i = 0; i < num; i++)
{
int value = list[i];
}

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("2 : " + elapsed + " [s]"); //0.001707292 [s]

1 : 0.002561998 [s]
2 : 0.001707292 [s]

 ただ値を取り出しただけで何もしてないが、実行速度は LINQ に比べて for は約1.5倍くらい速かった。要素数が少ないとき(50以下とか)にはあまり気にすることはないとは思うが、要素数が大きくなるほど、その差は大きく出る。大量のデータをスキャン、または変換など処理を施すのには LINQ はとても遅いので気をつけよう(プロコン問題などに使うとタイムアウトすることが多い(笑))。
List を配列にした方がもっと速い(約3.1倍:0.0008201599 [s])。



●LINQ と for でフィルタ(抽出)の比較

 例えば、連続した値の要素から、偶数だけを抽出するコードを比較してみた。

①LINQ で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 10000;
List<int> list = Enumerable.Range(0, num).ToList();

//以下を測定
float startTime = Time.realtimeSinceStartup;

List<int> list2 = list.Where(e => e % 2 == 0).ToList();

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("1 : " + elapsed + " [s]"); //0.0007956981 [s]

②for で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 10000;
List<int> list = Enumerable.Range(0, num).ToList();

//以下を測定
float startTime = Time.realtimeSinceStartup;

List<int> list2 = new List<int>(num);
for (int i = 0; i < num; i++)
{
int value = list[i];
if (value % 2 == 0)
list2.Add(value);
}

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("2 : " + elapsed + " [s]"); //0.0002808333 [s]

1 : 0.0007956981 [s]
2 : 0.0002808333 [s]

 測定部分では、LINQ はとてもシンプルで、for 文は長々と書いている感じだが、実行速度は LINQ に比べて、単純な for の方が約2.8倍くらい速かった(笑)。特に要素数が大きいときにはこの傾向は強くなる。for 文は昔ながらのとてもダサい(笑)文に見えるが、実は高速なので、Unity のようなフレームアプリケーションには向いているかも知れない(笑)。



●LINQ と for で全要素文字列変換の比較

 例えば、一定の文字列があり、それらを全部小文字にするコードを比較してみた(100個書くのが面倒くさい(笑)ので 10個x10=100個にしている)。

①Linq で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
string[] words = {
"Apple", "Banana", "Candy", "Doughnut", "Egg",
"Fish", "Grape", "Honey", "IceCream", "Jelly"
};
List<string> list = Enumerable.Repeat(words, 10).SelectMany(a => a).ToList(); //10x10=100個
int num = 1000;

//以下を測定
float startTime = Time.realtimeSinceStartup;

for (int i = 0; i < num; i++)
{
List<string> list2 = list.Select(e => e.ToLower()).ToList();
}

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("1 : " + elapsed + " [s]"); //0.04769385 [s]

②for で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
string[] words = {
"Apple", "Banana", "Candy", "Doughnut", "Egg",
"Fish", "Grape", "Honey", "IceCream", "Jelly"
};
List<string> list = Enumerable.Repeat(words, 10).SelectMany(a => a).ToList(); //10x10=100個
int num = 1000;

//以下を測定
float startTime = Time.realtimeSinceStartup;

for (int i = 0; i < num; i++)
{
int length = list.Count;
List<string> list2 = new List<string>(length);
for (int j = 0; j < length; j++)
{
list2.Add(list[j].ToLower());
}
}

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("2 : " + elapsed + " [s]"); //0.03735647 [s]

1 : 0.04769385 [s]
2 : 0.03735647 [s]

 測定部分では、for 文の方では list.Count を length に入れて使っているが、こうすると、ループのたびにプロパティを読み出しに行かなくて済むため、ほんのわずかだが速くなる。生成された list2 の方は特に何も使ってないが、通常は何か処理が入ると考えて欲しい。

 結果はだいたい、LINQ より for で書いたほうが約1.3倍くらい速くなった。思ったより速度は変わらないね。要素数が少ないなら、どちらを使っても良さそう。

 ちなみに for の例で List を全て Array で書き直すとわずかに速くなる(約1.4倍:0.03489325 [s])



■List と Array(配列)で比較

 ここではデータ格納先となる List と Array(配列)で速度を比較してみよう。

 これも同じ様に、11回実行し、はじめの1回の結果を捨て、残りの10回の平均を出した測定値だ。単純なものしか書いてないが、実際には何らかの処理が他に入ると考えて欲しい。



●単純なアクセス(値取り出し)の比較

 例えば、順次アクセスしていって、値を取り出すだけのコードを比較してみた。通常はその値を何らかで利用すると考えて欲しい。

①List で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
List list = Enumerable.Range(0, num).ToList();

//以下を測定
float startTime = Time.realtimeSinceStartup;

for (int i = 0; i < list.Count; i++)
{
int value = list[i];
}

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("1 : " + elapsed + " [s]"); //0.00246439 [s]

②Array で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
int[] array = Enumerable.Range(0, num).ToArray();

//以下を測定
float startTime = Time.realtimeSinceStartup;

for (int i = 0; i < array.Length; i++)
{
int value = array[i];
}

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("2 : " + elapsed + " [s]"); //0.0008202791 [s]

1 : 0.00246439 [s]
2 : 0.0008202791 [s]

 ただ値を取り出しただけで何もしてないが、実行速度は List に比べて Array(配列)は約3.1倍くらい速かった(笑)。これも要素数が大きくなるほど、その傾向が強くなる。ライブラリ関数も配列を返すものが多いが、もしかしたらその利用の実行速度のためであるかも知れない(笑)。



●ソートの比較

 例えば、標準関数にあるソートで比較してみた。

①List で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
List<int> list = Enumerable.Range(0, num).ToList();
list.Reverse();

//以下を測定
float startTime = Time.realtimeSinceStartup;

list.Sort();

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("1 : " + elapsed + " [s]"); //0.00460794 [s]

②Array(配列)で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
int[] array = Enumerable.Range(0, num).ToArray();
Array.Reverse(array);

//以下を測定
float startTime = Time.realtimeSinceStartup;

Array.Sort(array);

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("2 : " + elapsed + " [s]"); //0.002492619 [s]

1 : 0.00460794 [s]
2 : 0.002492619 [s]

 結果は List に比べて Array(配列)でのソートの方が約1.8倍くらい速かった

 実際には C# のソートは要素数によってアルゴリズムが変わると聞いたことがあるので(例えばクイックソートは要素数が多いときはパフォーマンス良いが、要素数が少ないときは、それほど良くないと言われているので、アルゴリズムを変えるのは有効である)、利用する場合は一度実験しておく方が良いかも知れない。

50以下挿入ソート、5万以下マージソート、あとはクイックソート

 ちなみに、Reverse(要素の反転)の代わりに、後述するシャッフルを使った場合、以下のようになった。

1 : 0.007369685 [s]
2 : 0.003433919 [s]

 比率としては約2.1倍である。誤差を含めて約2倍と考えても良いだろう



●シャッフル(Fisher-Yates shuffle)の比較

 例えば、Fisher-Yates アルゴリズムを用いたシャッフル関数を定義し、それを利用して比較してみた。

①List で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
List<int> list = Enumerable.Range(0, num).ToList();

//以下を測定
float startTime = Time.realtimeSinceStartup;

list.Shuffle();

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("1 : " + elapsed + " [s]"); //0.00126977 [s]


//拡張メソッドを定義
public static class Extensions
{
//リストの要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this List<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
list.Swap(i, j);
}
}

//要素のスワップ
public static void Swap<T>(this List<T> list, int i, int j)
{
T tmp = list[i];
list[i] = list[j];
list[j] = tmp;
}
}

②Array(配列)で書いた場合
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

//前準備
int num = 100000;
int[] array = Enumerable.Range(0, num).ToArray();

//以下を測定
float startTime = Time.realtimeSinceStartup;

array.Shuffle();

float elapsed = Time.realtimeSinceStartup - startTime;
Debug.Log("2 : " + elapsed + " [s]"); //0.0006387234 [s]


//拡張メソッドを定義
public static class Extensions
{
//配列の要素をシャッフルする (Fisher-Yates shuffle)
public static void Shuffle<T>(this T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1); //[0]~[i]
arr.Swap(i, j);
}
}

//要素のスワップ
public static void Swap<T>(this T[] arr, int i, int j)
{
T tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}

1 : 0.00126977 [s]
2 : 0.0006387234 [s]

 このシャッフルは以前に書いたコードそのままである。

 結果は List に比べて Array(配列)でのソートの方が約1.9倍くらい速かった

 これも誤差を含めて約2倍と考えても良いだろう。



■結果一覧

 以上をまとめると表のようになる。

●LINQ と for 構文での比較
処理内容実装方法測定時間備考
単純なアクセスLINQ0.002561998 [s] 
for0.001707292 [s]LINQ より約1.5倍速い
フィルタ(抽出)LINQ0.0007956981 [s] 
for0.0002808333 [s]LINQ より約2.8倍速い
全要素文字列変換LINQ0.04769385 [s] 
for0.03735647 [s]LINQ より約1.3倍速い


●List と Array(配列)で比較
処理内容実装方法測定時間備考
単純なアクセスList0.00246439 [s] 
配列0.0008202791 [s]List より約3.1倍速い
ソートList0.00460794 [s] 
配列0.002492619 [s]List より約1.8倍速い
シャッフルList0.00126977 [s] 
配列0.0006387234 [s]List より約1.9倍速い


 今回は単純な反復での比較しかしてないが、他にもアルゴリズムも絡めると実行速度はまた変わる。しかしそれらは言語でも異なるので注意しよう。例えば C#, Java などは文字列操作は遅いが、Ruby, PHP, Perl などは文字列操作はとても速い(=変換も速い)。実際に「数値の桁数を求める」問題があったとして、C#, Java などでは「10で割って数をかぞえる」アルゴリズムが速いが、Ruby, PHP などは「文字列に変換して長さで数える」方が速かったりする。こういったものは測定してみないとわからない。

 プロコン問題(競技プログラミング:AtCoderとか)とか見てても、上のランクほど、for, while, 配列だけで解いている場合が多いのも納得がいく。やはり 0.1秒で合否が分かれる問題などでは(10万件のデータを2秒以内に処理するプログラムとか普通なので)、LINQ や List では遅すぎるというのは体験的に知っているのだろう(事実、大量のデータを処理するには LINQ は向いてないと思う※今後のバージョンアップで改善されるかも知れないが)。

 この結果だけ見ると「LINQ は使わないで単純な構文に」「List は使わないで配列に」のように思えてしまうが、要素数が少ないときはそれほど大差ないので(1000個を超えると結構差が出てくるが、50個以下で1回きりとかなら、それほど差は出ない)、実行速度が欲しいときに、改めて見直すのもヒントとして考えるのも良いだろう。

 またそのうち色々実験したら、追加しておく(笑)。





(関連記事)
【Unity】【C#】配列・リストのシャッフル
【Java】数値の桁数を調べる(べき乗の桁数・べき乗のべき乗の桁を調べる)
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【Java】2次元配列のソート
【Java】配列要素の反転(reverse)
【一覧】Java, C#, PHP, Ruby, Python, JavaScript での Math.round(四捨五入・五捨六入)比較


category: Unity

thread: プログラミング

janre: コンピュータ

tag: C#  実証実験 
tb: 0   cm: --

【Unity】【C#】UniRx で「1フレームごと待機して処理」してみる  


 今まで「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フレームごとに待機して処理」っぽくなりそうなものを、色々やってみる
 ●コルーチンで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フレームごとに待機して処理?
■とりあえず「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
}
}

(Start) frame : 1
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
}
}

(Start) frame : 1
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
}
}

(Start) frame : 1
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
}
}

(Start) frame : 1
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
}
}

(Start) frame : 1
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
}
}

(Start) frame : 1
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
}
}

(Start) frame : 1
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
}
}

(Start) frame : 1
frame : 1
frame : 1
frame : 2
frame : 3
frame : 4

 UniTask.DelayFrame() と同じ結果になった。これもはじめの1回が待機されてない…?ま、まぁ、今回はコルーチンでの表記をそのまま代替できる書き方を探していただけなので(パフォーマンスなども考慮に入れてない)、これで勘弁してやろう(笑)。誰かそのうち調べてくれるだろうと期待してる(←投げっぱなし(笑))。



■とりあえず「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);
}
}

(Start) frame : 1
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の完全でハイパフォーマンスな統合/a>
Unityにおけるコルーチンの省メモリと高速化について、或いはUniRx 5.3.0でのその反映





(関連記事)
【Unity】VRM(VRoid)をライブステージで踊らせるアプリを作ってみた


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityオープンソースライブラリ  Unityライブラリ  実証実験 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR

▲ Pagetop