2013年1月23日水曜日

Unityで活かせるLINQの底力!! (前編)

最近、アセットストアでダウンロードしたコードに たまたま、LINQ を活用しているものがあって興味をもった。

そして「なるほどLINQ で書くと美しく見やすいコードを書ける場合があるのだ!」と素直に感じた。

調べてみたら、一般論としてUnity でどのような場合に活用すると良いか、Unity Gems に良い記事があったので2 回の投稿に分けて翻訳を紹介させてもらいたい。

長文ゴメン!!でもLINQをUnityでどう活用するか知らなかった人は覚えとくと、きっと役立つぜ!
http://unitygems.com/linq-1-time-linq/
Nov, 23, 2012

次の場合、この記事を読むとピッタリだ!

  • GameObject.FindGameObjectsWithTag() を使用して、ゲームオブジェクト自身でなく、それにアタッチされる全ての Transform を見つける方法がほしい場合
  • リストや配列の操作、ソート、及び変更を行いたいけれども、どうもコード量が余分に増えがちである場合
  • LINQ は聞いたことがあるけれど、仕組が良く分からない場合
  • 複雑な条件(あるターゲットに最も近い5つのタグつきオブジェクトのレンダラのマテリアルを全て)を戻り値とする条件を(あるいはもっと複雑な条件も)、一行のコードだけで出来ることを信じられない場合

上のトンチに対して、あなたが頭の中で「一行のコードでどうやって??」と悩んでる間に一緒に次の問題を考えることにしたい。「英語の文章で and が5連続で入って文法的に正しいものはどういうものだろう?」。想像つくだろうか。この答えは、文末にこの主題と完全に関連ない形で文末に載せてある(ズルして先に見ないでね)。 ※翻訳ブログでは次回記事


はじめに

LINQ とは Language Integrated Native Query の略称であり、.NET上でオブジェクトでも動作するデータベースのクエリの働きをするものである。強力であり、内容の理解が進むと非常に使いやすい。

LINQ は全ての for next ループを書き出すのに比べて遅い場合があります。半分ぐらいのスピードしか出ない場合があるため、毎フレーム行われる Update関数では慎重に使用されるべきだ。一方、キャッシング、プロセッシングでは優秀だ。

C# では実際に SQL 文に近い記述が出来る。
var transformsWithTag = from go in GameObject.FindGameObjectsWithTag("YourTag") 
     where Vector3.Distance(go.transform.position - transform.position) < 100
     select go.transform;
しかし関数ベースのアプローチがより強力かつ個人的に可読性が高いとおもわれるため、そちらを使用して行く。

列挙体の使用

Linq は列挙体を使用して既存の列挙体のコレクションを列挙しつつ、変更もかけていくものだ。このため Linq は配列、リスト、ディクショナリ、さらにあらゆるコレクションであるもので全て使用できる。Linq は列挙体を戻すために、これを繋ぎ合わせることができることは注目に値し、それゆえに強力だ。

Linq によりコレクションを編集する一連の関数を書くことが出来て、自身のコードを無名関数の形式で提供出来る。

早速始めてみよう!

LINQの有効化


Linq を使用するためには import または using ディレクティブをソースコードに追加する必要がある。
//C#
using System.Collections.Generic; //足しておくといつでも便利
Always a good idea
using System.Linq;

//JavaScript
import System.Linq;
有効化はこれだけで完了!

はじめてのLinqステートメント

まず簡単なことから始めよう - GameObject.FindGameObjectsWithTag で戻されるゲームオブジェクトと紐付けされた Transform の配列を作成してみる。

  • 全てのゲームオブジェクトを見つける
  • ゲームオブジェクトの Transform を選択
  • 結果を配列に変換

次のようなコードになる :
//C#

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(go => go.transform)
    .ToArray();

//Javascript

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(function (go) go.transform)
    .ToArray();
実現したいセレクション処理に無名関数を使用していることに注目してほしい。Select により列挙体(ここではゲームオブジェクト)の中身を取得し、適当な名前をつけて(ここでは go )、何らかの計算結果を戻して(ここでは transform )、ToArray が処理全体を実行したうえの結果を配列に格納します。

C# ではラムダ関数: go => go.transformを使用し、JavaScript では無名関数: function(go) go.transform を使用する。

さらに、、、この関数はひとつのステートメントに限定されない! { and }  をいれて好きなだけの量のコードを記述出来る。この点はまた後ほど説明する。

クエリ実行

次のトピックはクエリだ - 問い合わせを行うところから始めたい。現在の位置からワールド座標で 10 単位ほど離れたオブジェクトの Transform を全て取得して見よう。
//C#

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(go => go.transform)
    .Where(t => Vector3.Distance(t.position - transform.position) < 10
    .ToArray();

//Javascript

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(function (go) go.transform)
    .Where(function (t) Vector3.Distance(t.position - transform.position) < 10)
    .ToArray();
ここでは Where 関数を select の後に入れた - このため列挙体はすでに Transform の列挙体である - Where無名関数は真または偽を返す必要があるため、列挙体のTransform と現在のTransformの間の距離を計算する簡単なテストを行う。

ソート実行

Danger という名前のスクリプトが、オブジェクトにアタッチされているとして、このスクリプトには dangerLevel という float 変数を含むものとする。次に、Transform のリストをこの dangerLevel によってソートしていく(ここでは dangerLevel が範囲内にない場合は危険性がなく対象外という意味とする)。
//C#

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(go => go.transform)
    .Where(t => Vector3.Distance(t.position - transform.position) < 10
    .OrderByDescending(t => {
       var danger = t.GetComponent();
       return danger ? danger.dangerLevel : 0;
      })
    .ToArray();

//Javascript

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(function (go) go.transform)
    .Where(function (t) Vector3.Distance(t.position - transform.position) < 10)
    .OrderByDescending(function (t) {
          var danger = t.GetComponent(Danger);
          return danger ? danger.dangerLevel : 0;
         })
    .ToArray();
ここでは OrderByDescending を追加しました(降順に並べます)。最初にコンポーネントを取得して、次にその値を使用するために複数行に渡る関数を記述した。簡単なことであるのごお分かり頂けただろうか?通常の関数みたいに扱うことが出来る。

ここまでで十分イケてることは分かった - だけど、距離順 - 危険度順に並べて距離も重要なパラメータとして扱うにはどうすべきだろうか?
//C#

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(go => go.transform)
    .Where(t => Vector3.Distance(t.position - transform.position) < 10
    .OrderByDescending(t => {
       var danger = t.GetComponent();
       return danger ? danger.dangerLevel : 0;
      })
    .ThenBy(t => Vector3.Distance(t.position, transform.position)
    .ToArray();

//Javascript

var transformArray = GameObject.FindGameObjectsWithTag("MyTag")
    .Select(function (go) go.transform)
    .Where(function (t) Vector3.Distance(t.position - transform.position) < 10)
    .OrderByDescending(function (t) {
          var danger = t.GetComponent(Danger);
          return danger ? danger.dangerLevel : 0;
         })
    .ThenBy(function (t) Vector3.Distance(t.position, transform.position)
    .ToArray();
ThenBy および ThenByDescending により何個でも連結が出来る(ただし sqrMagnitude でも同じ結果が得られて、パフォーマンス観点からは平方根計算も省けるそちらを使用すべきであるが、分かり易くするためだけが理由で、ここでは Vector3.Distance とした)。
---

次回に続くぜ!

0 件のコメント:

コメントを投稿

ブックマークに追加

このエントリーをはてなブックマークに追加

自己紹介

自分の写真
Unity3D公式マニュアル翻訳やってる人がスマホ(iPhone, Android)のゲーム開発しています。気軽に面白く初心者が遊べる内容がモットー。Blogでは開発情報をひたすら、Twitterではゲーム作成の過程で参考にしている情報を中心につぶやきます

ページビューの合計

過去7日間の人気投稿