FC2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »C#
このページの記事一覧

【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#】RectTransform の矩形の実座標を取得する  


 ちょっとパネルのドラッグ機能を付けたときに必要だったので、備忘録的にメモ。


 特にスマホなどは解像度によって見た目の大きさと実際のピクセルサイズが違うので、動的に取得する必要があった。調べたらすぐにわかったが、視覚的な解説が無かったので、簡単に書いてみた。

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


●RectTransform の矩形の実際の座標を取得する
using UnityEngine;

public class Sample : MonoBehaviour {

public RectTransform target;

//RectTransform の矩形の実座標を Rect にして返す
public Rect GetRect(RectTransform rt)
{
Vector3[] corners = new Vector3[4];
rt.GetWorldCorners(corners);
return new Rect(corners[0], corners[2] - corners[0]);
}

//Rect 型にして、左下-右上座標を表示
void DisplayRect()
{
Rect rect = GetRect(target);
Debug.Log("(" + rect.xMin + ", " + rect.yMin + ") - ("
+ rect.xMax + ", " + rect.yMax + ")");
}

//4コーナー(左下, 左上, 右上, 右下)座標を表示
void DisplayCorners()
{
Vector3[] corners = new Vector3[4];
target.GetWorldCorners(corners);

for (var i = 0; i < 4; i++)
{
Debug.Log("corners[" + i + "] : " + corners[i]);
}
}

// Use this for initialization
private void Start () {
if (target == null)
target = GetComponent();

DisplayCorners();
DisplayRect();
}
}

corners[0] : (460.0, 240.0, 0.0)
corners[1] : (460.0, 480.0, 0.0)
corners[2] : (820.0, 480.0, 0.0)
corners[3] : (820.0, 240.0, 0.0)
(460, 240) - (820, 480)

 RectTransform.GetWorldCorners() は画面の左下を(0, 0)、右上が解像度の最大座標として、[0]:左下, [1]:左上, [2]:右上, [3]:右下 の座標を返す。


 この例では Canvas の設定が「Canvas Scaler」で「UI Scale Mode>Scale With Screen Size」, 「Reference Resolution>800x600 となっていて、パネルのサイズは 300x200 となっている。また、画面解像度は 1280x720 になっているときの値だ。


 ちなみに画面解像度を 2560x1440 にすると以下の値となる。
corners[0] : (920.0, 480.0, 0.0)
corners[1] : (920.0, 960.0, 0.0)
corners[2] : (1640.0, 960.0, 0.0)
corners[3] : (1640.0, 480.0, 0.0)
(920, 480) - (1640, 960)

 また、DisplayCorners() は公式のサンプルそのものだが、ちょっと手を加えて Rect 型で返す関数を GetRect() としても定義しておいた。矩形領域だけなら [0]:左下 - [2]:右上 の座標だけでも十分なことも多いからだ。


 冒頭に書いたパネル上での使用方法は、パネルをロックしたときに、パネル外ではカメラ回転、パネル内では無視するために座標を取っている(パネル上でマウスでカーソル合わせたりして、動かすたびにカメラが回転するのは鬱陶しいので(笑))。

 私はエクスプローラからのファイルドラッグ&ドロップにも対応しているが、UIの矩形座標を取れば、ドロップした先で処理を分岐するのも可能だろう。色々使える気がする(笑)。






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


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityリファレンス  C# 
tb: 0   cm: --

【Unity】【C#】Android で VRM(VRoid)を動的に読み込む  


 VRM Live Viewer にも利用しているが、元々は「プラグインを使って VRM を Android で読み込めるか?」という実験をしてみたら、スンナリと行けてしまったので次々とアイデアが浮かび、VRM Live Viewer をリリースするまでに至ってしまったという…(タイムスタンプを見ると、試しにライブステージ導入してから、アプリリリースまで4日しかかかってない←夢中になるといつの間にかアプリを完成させてしまうことも多い(笑))。


 まぁせっかくなので、VRMVRoid を Android でも読み込み、利用する方法を書いておこう。ちなみに VRM は VRChat やバーチャルキャストで使われるアバターフォーマットではあるが、リアルタイムで読み込むことができるので、あらかじめモデルをアプリに入れてビルドする必要もなく、読み込みもそれほど時間はかからないので、応用範囲は広いと思う。

 私は Unity4 の時代から MMD を Unity で動かしたり、一般公開されているモデルを実験で使ってたりしてたが、Unity ではいつもキャラのバリエーションが少ないな~と感じていたので、VRM で動的に読み込めるのは画期的だとさえ思う。例えば RPG でもアクションでも、好きなキャラで遊べるゲームとかも作れそうだしね(もちろん、大きさやコライダの判定などの問題もあるが、あくまで可能性として(笑))。アイデアは常に新しい発想から生まれるので、既成概念に捕らわれずに色々やってみると良いと思う。それがいつか新たな作品に繋がる。

 今回はあくまで Android で VRMVRoid を読み込む方法だけだが(どちらも "~.vrm" で扱うとして)、私が試したところ、一度 Unity 内に読み込んでしまえば、プラットフォームに関係なく扱えると思うので(見た目はシェーダなどのせいで多少変わることもあるが)、ひとつの方法として覚えておけば色々活用できるだろう。ちなみに VRM Live Viewer は Android版と Windows版を出しているが、ファイル読み込みやダイアログなどプラットフォーム固有のもの以外は全て同じだ。実際にシーン1つだけでビルドしている。つまり複数のプラットフォーム対応も簡単にできることがわかる。


(※) Unity 5.6.3p1 - 2018.2.1f1 / UniVRM 0.40 - 0.43 / VRoid Studio 0.1.1 - 0.2.8 / Windows10(x64) / Galaxy S7 Edge (Android 7.0) で確認



■UniVRM をインポートする

 Unity で VRM を読み込むには UniVRM というオープンソースが必要となる。ライセンスは「MIT License」となるので、その辺りは各自で確認して欲しい。ちなみにライセンス形態にも色々あるが、MIT License は比較的緩いライセンスだ。ついでに参考資料も載せておこう。

(参考)
GPL, LGPL, BSD などのOSSライセンスの違いと注意点まとめ
知らないと損をする6つのライセンスまとめ



 なお、新規プロジェクトで Android プラットフォームでビルドして試すなら、パッケージをインポートする前に「File>Build Settings...」であらかじめ「Switch Platform」で Android プラットフォームに切り替えておいた方が良いかも知れない。UniVRM に内包されているシェーダ(MToon 等)を再コンパイルしたりするのに結構時間がかかる(笑)。


 プロジェクトの準備ができたら、まずは UniVRM をダウンロードしよう。今回はアプリに動的に VRM を読み込むので UniVRM の本体「UniVRM-x.xx_xxx.unitypackage」(xxx はバージョンなど)の他に「UniVRM-RuntimeLoaderSample-x.xx_xx.unitypackage」のインポートも必要になる。本体「UniVRM-x.xx_xxx.unitypackage」を先にインポートしてから、ランタイムローダ「UniVRM-RuntimeLoaderSample-x.xx_xx.unitypackage」をインポートしよう。とりあえず VRM の動的読み込みに必要なものはこれだけで良い。






●API のアップデートが促されたら、「Go Ahead!」する




■VRM を動的に読み込んでみる

 UniVRM のインポートが終わったら、次にプロジェクトビューで「Assets/VRM.Samples/Scenes」で、シーン「VRMViewer」を開いてみよう。ビューワ自体は PC 用なのだが、これを改造することにより、Android 等他のプラットフォームの読み込み方法もわかると思う。



 ちなみに「VRM Live Viewer」はこのシーンを元ベースとしている(見た目もたいして変わってないのでわかると思うが(笑))。他の VRM 利用アプリを見てみると、たぶん同じようにこれを改造してるものが多い気がする。エクスポートできるアプリを作るなら、シーン「VRMRuntimeExporterSample」あたりを見てみると良いと思う。せっかくのオープンソースなのだから、遠慮なく使わせて頂こう(笑)。


 このシーンでは左上部にある「Open」ボタンを押すことにより、VRM を動的に読み込んで、シーン上にモデル(アバター)をロードすることができる。ただ、Windows 上なら 「PC, Mac & Linux Standalone」プラットフォームになってればそのまま使えるが、Android では無視される。この辺りから少し改造していこう。


1.スクリプトとしてはヒエラルキーで「Canvas」をクリックして、インスペクタで表示される「Viewer UI」にそのコードが書かれている。これを編集しよう。グレーアウトしてる「Script>ViewerUI」をダブルクリックすれば、Visual Studio で開かれる(シングルクリックなら、プロジェクトビューで移動できる)。



2.「ViewerUI.cs」を開いたら、検索で「OnOpenClicked」を探してみよう。これが前述した「Open」ボタンのイベントハンドラとなっている。ここのコードを見てみるとプリプロセッサディレクティブ(#if~文)でプラットフォームが分けられている。とりあえず Unity エディタ上でもテストできるようにディレクティブ(UNITY_EDITOR_WIN)を付け加えておこう。

void OnOpenClicked()
{
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
var path = FileDialogForWindows.FileDialog("open VRM", "vrm", "glb", "bvh");
#else
var path = Application.dataPath + "/default.vrm";
#endif
・・・(略)・・・
}

 ちなみに「UNITY_EDITOR_WIN」とは「Unityエディタ上でかつ Windows である場合の条件」である。プラットフォーム依存コンパイルを上手く使えば、複数のプラットフォームを分別することも可能だ。まぁしかし、コードは見づらくなるので、機能まるごとみたいな場合は、クラスごとに用意するという手もある。今回は一部を改造して使うので、この方法でやっていこう。

プラットフォーム依存コンパイル


3.「UNITY_EDITOR_WIN」を入れたら、グレーアウトしていた文字が見えるようになったと思う。しかし「FileDialogForWindows.FileDialog」の方にエラーが出たかも知れない。まぁ、これも同じプラットフォーム依存なので、「FileDialogForWindows」部分にカーソルを合わせ、「F12」を押せば、クラスがまるごとグレーアウトしてるのがわかる。手順2と同じように「UNITY_EDITOR_WIN」を #if~文に追加しよう。

#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
using System;
・・・(略)・・・
#endif

namespace VRM
{
public static class FileDialogForWindows
{
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
#region GetOpenFileName
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class OpenFileName
{
・・・(略)・・・
}
・・・(略)・・・
#endif
}
}


 これでコンパイルが通るようになったと思う。一旦、Unityエディタに戻ってプレイしてみよう。「Open」ボタンを押して適当な「~.vrm」を読み込んで見ると良い。VRMニコニ立体で多く配布されているので、いくつかダウンロードしておくと良いだろう。「ニコニ立体ちゃん」ことアリシア・ソリッドはとても軽いのでテストするにはもってこいだ。自分で作った VRoid でも可能だが、髪の毛などメッシュが多いものほど、生成に時間がかかるようだ(なので「VRM Live Viewer」では非同期読み込みの方を利用している。非同期読み込みを使うには「.NET4.x」にする必要があるので、ここでは割愛)。

ニコニ立体ちゃん (VRM)





■VRM の動的に読み込みを Android に対応させる

 VRM を動的に読み込みに成功したなら、後は Android に対応させるだけだ。ファイル選択などはプラットフォームに依存するので、先に出てきた「FileDialogForWindows」のようなものが必要になるが、Unity の標準機能には無いので、ここではプラグインを使うことにする(自分で作ったものがあれば、それでも良い)。



 ここで紹介するプラグインは元々私がブログで公開していたものだが、様々なアプリで利用して貰えてるようなのでアセットストアにも提出したというものだ(既に GooglePlay 等で公開されてるアプリなどにも利用されている。「〇〇というアプリを作ってるんですが、使わせて貰って良いですか?」と聞かれるようになったので、気兼ねなしに使えるようにアセットストアにも出したという経緯もある)。AssetStore版GoogleDrive版に機能的な違いはないので(AssetStore版 はアセットストアの規約に合わせただけ)、どちらを利用しても構わない(※ここでは AssetStore版を例にしている)。



 セットアップは以前の記事にあるので、そちらを参照して欲しい。AssetStore版GoogleDrive版では一部ファイル名やパス、素材が違うくらいで、内容的には同じだ。注意点は「Plugins」フォルダを「Assets」直下に移動し、「Plugins/Android」フォルダにあるサンプルのマニフェストファイル(AndroidManifest.xml)を用意しておくということだ(テストだけなら、"AndroidManifest_demo.xml"[AssetStore版]、または"AndroidManifest_test.xml"[GoogleDrive版]を複製してリネームすれば良い)。

AssetStore版のセットアップ
GoogleDrive版のセットアップ


1.プラグインのインポートとセットアップの準備が済んだら、プロジェクトビューの検索で「StorageOpenFileController」のプレファブを見つけよう。見つけたら、これをヒエラルキーに置き、後述のコードを書くことにより、Android でもファイルの情報を受け取れるようになる。本来なら Android でストレージの読み取りなどにはパーミッションなども必要になるが、前述のデモのマニフェスト("AndroidManifest_demo.xml"など)を使ってる分には既に含まれている(「READ_EXTERNAL_STORAGE」または「WRITE_EXTERNAL_STORAGE」が必要。デモにはそれ以外の権限も含まれているが、通常は不要な権限は削除した方が良い→ユーザーにインストを拒否られる確率が高くなるため)。

(パーミッション)
READ_EXTERNAL_STORAGE(ファイル読み取り権限)
WRITE_EXTERNAL_STORAGE(ファイル読み書き権限)



2.次に「StorageOpenFileController」で取得したファイル名を受け取るハンドラを、元のコード「ViewerUI.cs」の「OnOpenClicked」に追加しよう。書き方は前述のコードに追加する形となる。Android の場合ディレクティブは「UNITY_ANDROID」となるので、それを追加し、「StorageOpenFileController」でストレージを開くコードを Android プラットフォーム用に書いておこう

プラットフォーム依存コンパイル

using FantomLib;

・・・(略)・・・

void OnOpenClicked()
{
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
var path = FileDialogForWindows.FileDialog("open VRM", "vrm", "glb", "bvh");
#elif UNITY_ANDROID
var path = "";
StorageOpenFileController storageOpenFileController = FindObjectOfType<StorageOpenFileController>(); //ここはインスペクタで登録できるようにしても良い
storageOpenFileController.Show(); //実機ではエクスプローラのようなもので、ファイル選択ができるようになる
#else
var path = Application.dataPath + "/default.vrm";
#endif
if (string.IsNullOrEmpty(path))
{
return;
}
・・・(略)・・・
}

※この例はやっつけ的なコードなので、「StorageOpenFileController」をインスペクタで登録できるようにしたり、任意にまとめたりして使って下さい(笑)。


3.ランタイム時では「StorageOpenFileController」は閉じられてから、コールバックで結果(選択されたファイルパス名)が返ってくるので、「UNITY_ANDROID」ディレクティブ内ではパスを空(path = "")にしていることに注意して欲しい。これはすぐ下にある「string.IsNullOrEmpty(path)」で一旦終了することを意味する。

 なので、取得したパスを受け取るハンドラを作成する必要がある。ここでは簡略のため、元の「OnOpenClicked()」内のコードを一部まるっとコピーして、もう1つ「OnStorageOpenFile()」というメソッドを定義した(メソッド名は任意)。

public void OnStorageOpenFile(string path)
{
if (string.IsNullOrEmpty(path))
{
return;
}

var ext = Path.GetExtension(path).ToLower();
switch (ext)
{
case ".gltf":
case ".glb":
case ".vrm":
LoadModel(path);
break;

case ".bvh":
LoadMotion(path);
break;
}
}

※UniVRM v0.40 以前は拡張子分岐は無いが、同じように「LoadModel(path)」を呼べば良い。

 実際には「StorageOpenFileController」をインスペクタで登録できるようにしたり、拡張子による分岐などは重複してるので「OnOpenClicked() → OnStorageOpenFile(path)」へ行くように書き換えても良いだろう。その辺りはお任せする(笑)。とりあえずはコード自体はこれで良い。


4.後はヒエラルキーに戻って「StorageOpenFileController」のコールバック「OnResult」に先程の「OnStorageOpenFile(String)」に登録しよう。これで一応完成である。ただし、実機でしか確認できないので、Android ビルドして動作確認してみよう。



5.「File>Build Settings...」を開いてシーン「VRMViewer」を追加してビルドしよう。ビルドに関してはいくつか注意点があるので、以下を参照して欲しい。

「要求 API Level」の設定
シーンを追加してビルドする
Unity 2018.1.0~1.6 での Gradle ビルドにおいて、「Cannot read packageName from~(パス)\AndroidManifest.xml」と出る。




 ここまでできれば、例えば以前の「VRoid(VRM)を動かす」のようにして、ゲームに使うことも可能だろう。1つ1つの技術は結構手間のかかるものだと思うが、プラグインも含め、全て無料でできるので、これを使わない手はない(笑)。今までにない新たな利用法を考えてみるのも良いだろう。

●実機(Android)で「ニコニ立体ちゃん (VRM)」を読み込んでみた所

ニコニ立体ちゃん (VRM)
(c) DWANGO Co.,Ltd. ニコニ立体ちゃんライセンス


 今回はただ VRM を読み込んで動的にアバターを召喚(笑)しただけだが、実際にスマートフォンで利用するには画面解像度・回転の対応やピンチなど、使い勝手を良くした方が良いだろう。プラグインにはそういったスマホらしい操作(ピンチ・スワイプ・ロングタップ等)の例も入っている。VRM Live Viewer の Android 版はまさにその使用例なので、動作確認にインストして動かしてみるのも良いだろう(またはプラグインのデモもQRコードからインストできるようにしてあるので参考に)。











(関連記事)
【Unity】VRM(VRoid)をライブステージで踊らせるアプリを作ってみた
【Unity】VRoid(VRM)をインポートして動かす
【Unity】AssetStore版 FantomPlugin のセットアップ
【Unity】Androidのトーストやダイアログ、通知、音声認識、ハード音量操作など基本的な機能を使えるプラグインを作ってみた


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: VRM  VRoid  Unityプラグイン  C# 
tb: 0   cm: --

【Unity】【C#】UI-Text 版 FPS(フレームレート)をリアルタイムに測定して表示する  


 以前に作ったレガシーGUI を uGUI(UI.Text)に移植しただけのもの。

 StandardAssets には「FPSCounter」という FPS測定ライブラリも入っているが、ソースを見てみると、一定間隔の平均を求めてるようなので、できればリアルな(平均でない)値を見たいと思って、わざわざ移植した(と言ってもほとんどまるっとコピーだが(笑))。

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



●UI-Text 版 リアルタイムにFPSを測定して表示する
using System.Text;
using UnityEngine;
using UnityEngine.UI;

// FPS 測定値を UI-Text に表示する
// http://fantom1x.blog130.fc2.com/blog-entry-307.html
public class FpsText : MonoBehaviour {

public Text targetText; //表示する UI-Text
public string displayFormat = "{0:F1} FPS"; //表示フォーマット(F0 とすれば小数点以下は無くなる)

//測定用
int tick = 0; //フレーム数
float elapsed = 0; //経過時間
float fps = 0; //フレームレート

StringBuilder sb = new StringBuilder(16);

// Use this for initialization
private void Start () {
if (targetText == null)
targetText = GetComponentInChildren<Text>();
}

// Update is called once per frame
private void Update () {
tick++;
elapsed += Time.deltaTime;
if (elapsed >= 1f) {
fps = tick / elapsed;
tick = 0;
elapsed = 0;

if (targetText != null)
{
sb.Length = 0;
sb.AppendFormat(displayFormat, fps);
targetText.text = sb.ToString();
}
}
}
}

 使い方は UI の Text などにアタッチし、targetText に表示する Text をセットすれば完了だ(未セットでも子要素までは自動で探す)。

 測定(計算)方法は Update() による Time.deltaTime で集計した単位時間あたりのフレーム数である。あくまでもフレーム更新での測定なので、現実時間に近い測定(連続した時間)にしたいなら Time.realtimeSinceStartup で測定するのも良いだろう(StandardAssets の FPSCounter は Time.realtimeSinceStartup での測定)。

 実は色々な FPSカウンターを見てみると、測定(計算)方法はまちまちなので、用途に合わせた選択をした方が良いだろう。例えば StandardAssets の「FPSCounter」では連続的な時間での平均的な fps 測定に、今回の「FpsText」では一定更新毎(連続的というより、更新約1秒ごとにフレーム数を測定という感じ)の測定には向いてるかも知れない。具体的に言えば、平均化されてる場合は、もたったりして、ある程度バラついてもフラットな値になるが、平均化されてない場合は、もたったりしてる瞬間は値が激変する。自分がどちらの値を欲してるかで決めれば良い。

 以前の記事にも書いたが完全にフレームごとの超詳細な測定をしたいなら、毎フレーム「1f / Time.deltaTime」で計算する方法もある。ただしこの場合、ほんの僅かなもたりでも(人間の感覚ではわからないくらい小さいものでも)値として出るので、通常使う分には向いてないかも知れない(また、物凄く更新が速いため、見づらいという欠点もある)。仕様はケースバイケースで合わせた方が良いだろう。


※この記事のスクリプトはプラグインのライブラリにも同梱されています。


※とりあえず試してみたい方には、最新版をビルドした apk デモをダウンロードできます。動作確認にもどうぞ。

プラグインデモをダウンロード
(Google Drive を利用)


Android 4.2以上
※「提供元不明アプリのインストール」許可が必要です。


(関連記事)
【Unity】【C#】FPS(フレームレート)をリアルタイムに測定して表示する
【Unity】【C#】FPS(フレームレート)をリアルタイムに測定して表示するv2(4隅選択可能で、画面サイズの変更にも対応版)


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityライブラリ  C# 
tb: 0   cm: --

【Unity】【C#】テキストファイルの読み書きをする(リソース/ストレージ)  


 Unity では内部リソースからテキストファイルを読み込む方法が非常に簡単に用意されているが、それとは別に外部のリソース(ストレージ等)でテキストファイルを読み書きする方法も書いておこう。

 またついでに Android でプラグインを利用して読み書きする方法も紹介しておく。ちなみに Unity から C# で直接 SDカードに書き込もうとすると、アクセス拒否で失敗するが、プラグインを利用する場合は、SDカードにも書き込むことが可能だ。エクスプローラみたいなファイル選択も使えるので、ユーザーが自由にファイルの読み書きを行えるようなシステムを作るには役に立つだろう。

 ここでは C# スクリプト例はインスタンスメソッドとして書いておくが、static なライブラリとして利用したいなら、例外処理を throw などすれば良いと思う。とりあえず簡単な実装を書いておこう。自由に改造でもして使って欲しい。


(※) Unity 5.6.3p1 - 2018.1.8f1 / Windows10(x64) で確認



■リソースからテキストアセットを読み込む(Unity ビルトイン機能)

 ここでは Unity の機能を使って、アプリに内包したテキストファイルを読み込んでみよう。

 これは Unity のエディタ上であらかじめリソースとして持っておき、ランタイム時にロードする方法だ。Unity では「Resources」というフォルダを作っておくと、起動後にリソースとしてテクスチャやデータなどを読み込むことができる。ここにテキストファイルを置いておけば、「TextAsset」として簡単に扱える。それを利用して読み込んでみよう。

●リソースからテキストアセットを読み込む(メソッド定義)
using UnityEngine;
using UnityEngine.UI;

public string textAssetName = "Texts/テスト"; //拡張子はいらない
public Text displayText; //表示するUI-Text

//リソースからテキストアセットを読み込む
void LoadTextAsset(string name)
{
if (string.IsNullOrEmpty(name) || displayText == null)
return;

TextAsset textAsset = (TextAsset)Resources.Load(name, typeof(TextAsset));
if (textAsset != null)
displayText.text = textAsset.text;
else
Debug.Log("Not found TextAsset : " + name);
}

●使用例(メインでのコードなど)
using UnityEngine;

//ロードボタンのコールバックハンドラ
public void OnLoadTextAsset()
{
LoadTextAsset(textAssetName);
}

 読み込み先テキストには UI-Text(displayText)を用いているが、もちろん string 型の引数のメソッドを作って直接読み込んでも良い。これはあくまでも例なので、その辺りは用途に応じて書き換えて欲しい。

●「Resources」フォルダを作り、その配下にテキストファイル(UTF-8)をインポートしておく。

 テストするには UI-Button などを適当に配置し、「Button」の「OnClick()」にスクリプトの「OnLoadTextAsset()」を登録すれば良い。





●「テスト用スクリプトのアタッチ例


●ファイルの内容は任意(UTF-8)

※ランタイム時にログを見るにはプラグインライブラリのプレファブ「DebugConsole」をシーンに置き、コード中の Debug.Log() を XDebug.Log() に置き換えて下さい。また、インスペクタで「displayText」に「DebugConsole」以下の「Text」を登録します。



■ローカルストレージなどでテキストファイルの読み書きをする(C# スクリプト)

 次にパス(ファイル名)を指定してテキストファイル(UTF-8)を読み書きすることをやってみよう。

 例ではフォルダ(ディレクトリ)に「Application.persistentDataPath」を使っているが、これはプラットフォームによって変わるデフォルトの永続的な保存先となる。また、会社名(company name)やアプリ名(product name)でもフォルダ分けされる。詳しくは公式マニュアルで確認して欲しい。

(参考)Application.persistentDataPath

●ローカルストレージなどでテキストファイルの読み書きをする(メソッド定義)
using System;
using System.IO;
using System.Text;

//テキストをファイルから読み込む(行読み)
//※Android で SD カードから読み込みをするには、「AndroidManifest.xml」にパーミッション("READ_EXTERNAL_STORAGE" または "WRITE_EXTERNAL_STORAGE")が必要。
string LoadText(string path)
{
StringBuilder sb = new StringBuilder(1024); //※capacity は任意

try
{
using (StreamReader reader = new StreamReader(path))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
sb.Append(line).Append("\n");
}
}
}
catch (Exception e)
{
Debug.Log(e.Message);
return null;
}

return sb.ToString();
}

//テキストをファイルに保存
//※Android で External Storage に書き込みをするには、「AndroidManifest.xml」にパーミッション("WRITE_EXTERNAL_STORAGE")が必要。
//※セキュリティ上、Unity から直接 SD カードには保存できない。
bool SaveText(string text, string path)
{
try
{
using (StreamWriter writer = new StreamWriter(path))
{
writer.Write(text);
writer.Flush();
writer.Close();
}
}
catch (Exception e)
{
Debug.Log(e.Message); //Access to the path "filename" is denied. → パーミッションが無い, 書き込みアクセス不可(SDカードなど)
return false;
}
return true;
}

●使用例(メインでのコードなど)
using UnityEngine;
using UnityEngine.UI;

public string filename = "test.txt";
public Text displayText; //表示するUI-Text

//ロードボタンのコールバックハンドラ
public void OnLoadClick()
{
if (string.IsNullOrEmpty(filename) || displayText == null)
return;

string path = Path.Combine(Application.persistentDataPath, filename); //プラットフォームによってパスは異なる

if (!File.Exists(path))
{
Debug.Log("Not found : " + path);
return;
}

string text = LoadText(path);
if (!string.IsNullOrEmpty(text))
displayText.text = text;
}

//セーブボタンのコールバックハンドラ
public void OnSaveClick()
{
if (string.IsNullOrEmpty(filename) || displayText == null)
return;

string path = Path.Combine(Application.persistentDataPath, filename); //プラットフォームによってパスは異なる

if (SaveText(displayText.text, path))
Debug.Log("Save to : " + path);
}

 テストするには UI-Button などを適当に配置し、「Button」の「OnClick()」にスクリプトの「OnLoadClick()」「OnSaveClick()」をそれぞれ登録すれば良い。
●UI-Button 配置とテスト用スクリプトのアタッチ例

●UI-Button のコールバック登録例

 読み書きするテキストには UI-Text(displayText)を用いているが、もちろん string 型の引数のメソッドを作って直接読み書きしても良い。これはあくまでも例なので、その辺りは用途に応じて書き換えて欲しい。

 なお、Android でSDカードに読み書きする場合は以下のパーミッションが必要になる。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />'

 パーミッションについての説明が必要なら以前の記事を参照して欲しい。


※ランタイム時にログを見るにはプラグインライブラリのプレファブ「DebugConsole」をシーンに置き、コード中の Debug.Log() を XDebug.Log() に置き換えて下さい。また、インスペクタで「displayText」に「DebugConsole」以下の「Text」を登録します。



■Android でファイル選択+テキストファイルの読み書きをする(プラグイン利用)

 ついでにプラグインに入っている「StorageLoadTextController」と「StorageSaveTextController」(~/FantomLib/Prefabs/System/ 以下。検索で探すと簡単)というテキストファイル読み書きの機能を使ってみよう。これら「~Controller」は Android 標準のファイル選択機能を使い、テキストファイルを読み込み・書き込みをしてくれるものだ。これら機能はプラグイン内部に実装されているので、自分でコードを書く部分は、機能呼び出しと結果受け取りだけで良い。

 ではまず、その機能呼び出しと結果受け取りのコードだけ書いてしまおう。名前は任意で良い。これらは各「~Controller」や UI-Button のコールバックハンドラとなる。

●コールバックハンドラ例(メインでのコードなど)
using UnityEngine;
using UnityEngine.UI;
using FantomLib;

public Text displayText; //表示するUI-Text

//読み込んだテキストを UI-Text に表示する
public void OnStorageLoad(string text)
{
if (string.IsNullOrEmpty(text) || displayText == null)
return;

displayText.text = text;
}

public StorageSaveTextController storageSaveTextController; //※インスペクタで登録

//UI-Text のテキストをファイルに保存する
public void OnStorageSave()
{
if (displayText == null || string.IsNullOrEmpty(displayText.text)) //※空は保存しない場合
return;

if (storageSaveTextController != null)
storageSaveTextController.Show(displayText.text);
}

//エラーステータス用コールバックハンドラ
public void OnError(string message)
{
Debug.Log(message);
}

 あとはヒエラルキーに「StorageLoadTextController」と「StorageSaveTextController」(~/FantomLib/Prefabs/System/ 以下。検索で探すと簡単)を置き、UI-Button など(※名前は任意)を Canvas に配置したら、インスペクタでコールバックを設定するだけだ。

●「StorageLoadTextController」と「StorageSaveTextController」を置く。テスト用スクリプトのアタッチ例。

●スクリプト(TextFileTest)へコールバック登録する
※または、StorageLoadTextController.OnResult に直接 UI.Text.text を登録しても良い。

●UI-Button から機能呼び出しをする


 あとはビルドするだけだが、プラグインを含む場合のビルドは「AndroidManifest.xml」が必要になる。その辺りは以前の記事にまとめてあるので参照して欲しい。もちろん「Build Settings...」にシーンを追加するのを忘れずに。

●ファイルの内容は任意(UTF-8)

※ランタイム時にログを見るにはプラグインライブラリのプレファブ「DebugConsole」をシーンに置き、コード中の Debug.Log() を XDebug.Log() に置き換えて下さい。また、インスペクタで「displayText」に「DebugConsole」以下の「Text」を登録します。


※この記事のUnityアセットはプラグインとして配布されています。


※とりあえず試してみたい方は、最新版をビルドした apk デモをダウンロードできます。動作確認にもどうぞ。

プラグインデモをダウンロード
(Google Drive を利用)


Android 4.2以上
※「提供元不明アプリのインストール」許可が必要です。


(関連記事)
【Unity】AssetStore版 FantomPlugin のセットアップ
【Unity】Androidのトーストやダイアログ、通知、音声認識、ハード音量操作など基本的な機能を使えるプラグインを作ってみた
【Unity】【C#】UnityEvent, Action, delegate, interface でのコールバック実装方法とインスペクタでの登録


category: Unity

thread: ゲーム開発

janre: コンピュータ

tag: Unityリファレンス  Unityプラグイン  C# 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR

▲ Pagetop