前回の「Unityで活かせる LINQの底力!!」ではLINQの基本的な使い方をみていった。
関連記事
Unityで活かせる LINQの底力!!(前編)
http://gamesonytablet.blogspot.com/2013/01/unitylinq.html
今回はUnityでLINQを使用すると、どんな風に便利であるか、Unity Gemsより翻訳を紹介したい:
http://unitygems.com/linq-1-time-linq/
Nov, 23, 2012
ドリルダウン
LINQ を使用して、リスト項目のメンバに当たるコレクションのコンテンツにドリルダウンすることが出来る。
前述の例ではタグが一致した全てのアイテムの transform を戻していた - もし全てのレンダラを下の階層分も全てほしい場合はどうすれば良いだろう?ここで SelectMany が役立つ:
//C#
var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
.SelectMany(go => go.GetComponentsInChildren())
.ToArray();
//Javascript
var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
.SelectMany(function (go) go.GetComponentsInChildren(Renderer))
.ToArray();
SelectMany により、戻り値のコレクションの集合を連結してひとつのコレクションに入れるため、SelectMany を実行した後は全てのアイテムの全てのレンダラが含まれる。
Linq の中にLinq
ここで仮に前述の例で期待どおりでなくて、レンダラはあくまでオブジェクト自身と直接の子オブジェクトのみが本来は取得したかったとする - これも簡単なことだが、Linq の中にLinqを使用することになる。
//C#
var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
.SelectMany(go => go.transform.Cast()
.Select(t=>t.renderer)
.Concat(new [] { go.renderer })
.Where(r=>r!=null)
)
.ToArray();
//Javascript
var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
.SelectMany(function (go) go.transform.Cast.()
.Select(function (t) t.renderer)
.Concat([go.renderer])
.Where(function (r) r != null)
)
.ToArray();
Transform を取得して Transform のコレクションとなるように 型キャストする(Unity で変な仕様と感じるところだが、全ての子オブジェクトも取得出来るためにデフォルトでは型が異なる)。次に各々の子オブジェクトのレンダラを選択し、親オブジェクトのレンダラを格納する配列に列挙体を連結していき、次の行でnull でないものだけにこの処理を限定している。
この作業もこれで完了だ。
最も近いオブジェクトを見つける
最も近いオブジェクトを見つける方法はいくつかあるが、まずは効率悪い方法からお見せする:
//C#
var closestGameObject = GameObject.FindGameObjectsWithTag("MyTag")
.OrderBy(go => Vector3.Distance(go.transform.position, transform.position)
.FirstOrDefault();
//Javascript
var closestGameObject = GameObject.FindGameObjectsWithTag("MyTag")
.OrderBy(function (go) Vector3.Distance(go.transform.position, transform.position))
.FirstOrDefault();
次にコレクションを距離の順に並べて FirstOrDefault を取得する。次にオブジェクトが一覧にない場合、FirstOrDefault はnull を戻す - First を使用することも出来るが、アイテムがない場合は例外を投げることになる。
ここで問題は、リストのソートが必要だということだ - 最も近いオブジェクトを取得するだけのことに時間が多くかかってしまう。
ジェネリック関数の Aggregate を使用して、それを回避するように指示できる。
var closestGameObject = GameObject.FindGameObjectsWithTag("MyTag")
.Aggregate((current, next)=> Vector3.Distance(current.transform.position, transform.position) < Vector3.Distance(next.transform.position, transform.position)
? current : next);
//Javascript
var closestGameObject = GameObject.FindGameObjectsWithTag("MyTag")
.Aggregate(function(current, next) Vector3.Distance(current.transform.position, transform.position) < Vector3.Distance(next.transform.position, transform.position)
? current : next);
これはリストを一回だけ実行し、各ステップ毎に二つの候補のアイテムのうち近い方を戻す。リストは一回だけ渡すため何回も処理は行われなくて済む。C# では無名関数を使用して最適化出来る(JavaScript では上手く動作しないのでここでは割愛する)。
//最適化した C#
var currentPos = transform.position; //高価なのでキャッシュして節約
var closestGameObject = GameObject.FindGameObjectsWithTag("MyTag")
.Select( go => new { go = go, position = go.transform.position })
.Aggregate((current, next)=>
(current.position - currentPosition).sqrMagnitude <
(next.position - currentPosition).sqrMagnitude
? current : next).go;
最初のSelectでは二つのメンバつきの新しいクラスを作成した - その時点で列挙体に入っているのはそれだけだ。次にキャッシュされた位置を使って近い方を見つける(高価な平方根演算を回避して sqrMagnitude を使用)。最後に近いオブジェクトが得られるが、無名クラスになっているので、go 変数を使用してゲームオブジェクトに変換する。
リストおよびディクショナリ
ところで作成が出来るのは配列だけでなく、.ToList() を使用してリストを作成できるし、ディクショナリだって出来る!!
ディクショナリは明確にキーも持っているのでどうやって作成するか見ていくとする。では、シーンにある全てのタグつきのゲームオブジェクトを見つけて、そのタグに基づいてディクショナリに格納する :
//C#
var lookupByTag = GameObject.FindObjectsOfType(typeof(GameObject))
.Cast()
.Where(go=>!string.IsNullOrEmpty(go.tag))
.ToLookup(go => go.tag);
//JavaScript
var lookupByTag = GameObject.FindObjectsOfType(GameObject)
.Cast.()
.Where(function (go) go.tag != "")
.ToLookup( function (go) go.tag );
これにより特殊なディクショナリが作成され、そこで lookupByTag["タグ名"] によりゲームオブジェクトの一覧にアクセス出来るようになる。例えばゲームオブジェクトまでの距離を示すか通常のディクショナリを作成することが出来る(ゲームオブジェクトを関数の引数にするとターゲット地点までの距離を戻す)。いくつものゲームオブジェクトで頻繁に計算が実行される場合にも十分に速い処理だ。
//C#
var objectToDistance = GameObject.FindObjectsOfType(typeof(GameObject))
.Cast()
.ToDictionary(go=>go, go=>Vector3.Distance(go.transform.position, transform.position));
// ゲームオブジェクトの距離を後に取得する時:
var distance = objectToDistance[someGameObject];
ToDictionary の最初の引数がキーであり、二つ目が必要とするディクショナリの値であることに注目してほしい。これはDictionary<GameObject, float> を戻す。
結論
望むらくはこの紹介記事により Linq を使用する 際のテクニックが身に付き、リスト編集のコツが掴めてもらっていれば何よりだ。知っておくと便利なことは他にもあるので、それは次の機会に検証することとしたい!!
回答
- 一行のコードで、ターゲットに最も近い準備で、5つのタグつきオブジェクトのレンダラのマテリアルを取得するのが信じられないひとつは次のコードをとくとご覧あれ!!
var materials = GameObject.FindGameObjectsWithTag("sometag").Select(r=>r.renderer).Where(r=>r!=null).OrderBy(r=>(r.transform.position - transform.position).sqrMagnitude).Take(5).SelectMany(r=>r.materials).ToList();
foreach(var m in materials) m.color = Color.red;
- 最後に: 英語の文章で5 連続でand使っていみが通じるのは?
"Pig and Whistle" という飲み屋の看板をペンキ屋が描いてるところへ、地主が一瞥して一言:
"You need to leave more space between the Pig and 'and' and 'and' and Whistle."
「Pig と and、andとWhistleの間の隙間が足りないな」
翻訳注: 5つもandが並んでるのが言いにくくてたまらない言葉遊び。まあ英語版「寿限無」の呪文とおもって笑えたらあなたもネイティブだ!! 「いや、意味分からん」とおもった方・・・まあ、外国のジョークはそんなもんだ(笑)
-------
筆者は正直、自分でLINQを使ったことはなかったのだが、Asset Storeのサンプルプロジェクトに入っていて、どんなものであるか調べていくうちに本記事をみて、なかなか便利なモノだと感じたが、皆さんはどうだろう。
いろんな見解を共有しあって、理解をどんどん深めていこうぜ!