LINQ to GameObjectによるUnityでのLINQの活用

Unityで、LINQは活用されているようでされていないようで、基本的にはあまりされていない気配を非常に感じます。もったいない!というわけじゃないんですが、以前に私の勤務先と別の会社さんとで勉強会/交流会したのですが、そこで作ったスライドがあるので(若干手直ししたものを)公開します。LINQについて全くの初心者の人向けにLINQの良さを説明しようー、みたいな感じです、でもちょびっとだけ踏み込んだ内容もね、みたいな。勉強会自体は5月ぐらいにやったので、ずいぶんと公開まで開いてしまった……。

LINQ in Unity from Yoshifumi Kawai

その私の勤務先(まどろっこしい言い方だ……)グラニでは会社間での勉強会は大歓迎なので、もし、やりたい!という人がいらっしゃいましたら是非是非私のほうまでー。オフィスは六本木にあるのでその周囲ほげkmぐらいまでなら出張りますです(他のオフィスを見てみたい野次馬根性)。私の持ちネタとしてはC#, LINQ, Rxぐらいならなんでもどこまでもいけます。

さて、そんなわけでLINQは有益です、という話なんですが(AOT問題はスライドに書きましたが頑張れば解決できる!)、LINQを活用するためにはデータソースを見つけなきゃいけません。逆にデータソースさえ見つかれば幾らでも使えます。今回(本題)着目したのはGameObjectのヒエラルキーで、これを丸々とLINQと親和性の高い形で探索/操作できるアセット「LINQ to GameObject」を作りました。GitHubでソースコード、Unity Asset StoreではFREEで公開しています。

実際のトコロ、多分、みんな絶対に作って手元に持ってるちょっとしたユーティリティ、です。これもまた一つの俺々ユーティリティ。ちょっと違うところがあるとしたら、このライブラリは、ツリー階層構造へのLINQスタイルの操作ということで、LINQ to XMLからインスパイアされていて、API体系を寄せています。既に実績があり、そして実際に良さが保証されている(LINQ to XMLは非常に良いのです!問題はXMLを操作する機会がJSONに奪われてしまって近年少なくなってしまっただけでLINQ to XML自体は非常に良い!)APIとほぼ同一なので、ある程度のクオリティが担保されている、ということでしょうか。

ツリー探索とLINQ

探索のAPIは図にすると、以下の様な形になっています。

起点を元に親方向(Parent/Ancestors)か、子孫方向(Child/Children/Descendants)か、兄弟方向(ObjectsBeforeSelf/ObjectsAfterSelf)かに並んでいるGameObjectをIEnumerable<GameObject>として列挙します。

// 以下の様な形に抽出される
origin.Ancestors();   // Container, Root
origin.Children();    // Sphere_A, Sphere_B, Group, Sphere_A, Sphere_B
origin.Descendants(); // Sphere_A, Sphere_B, Group, P1, Group, Sphere_B, P2, Sphere_A, Sphere_B
origin.ObjectsBeforeSelf(); // C1, C2
origin.ObjectsAfterSelf();  // C3, C4

これはXPath(今となっては懐かしい響き!)の「軸」と同じもので、考えられる全ての列挙方向/方法を満たしています。特徴的なのは全てがIEnumerable<GameObject>になることで、LINQ to Objectsとシームレスに繋がり、フィルタやコレクションの変形を連続して行うことができます。

// 子孫方向のゲームオブジェクトの近いものトップ5を配列に固める(ただforeachするだけなら配列にしなくていい/しないほうがいいよ!)
var nearObjects = origin.Descendants()
    .OrderBy(x => (x.transform.position - this.transform.position).sqrMagnitude)
    .Take(5)
    .ToArray();

// 子孫方向の全ゲームオブジェクトのうちtagが"foobar"のオブジェクトを破壊
origin.Descendants().Where(x => x.tag == "foobar").Destroy();

// 自分を含む子ノードからBoxCollider2Dを抽出
var colliders = origin.ChildrenAndSelf().OfComponent<BoxCollider2D>();

// 全ての方法を組み合わせで満たせる、例えば兄弟方向に下のノードの全子孫はObjectsAfterSelf + Descendants
// これは ObjectsAfterSelf().SelectMany(x => x.Descendants()) のシンタックスシュガー
origin.ObjectsAfterSelf().Descendants();

SelectしてOrderByして、あれやこれや、なども自由に幾らでも行えます。また、繋がった状態での定形操作ということで、LINQ to GameObjectでは更にIEnumerable<GameObject>に対してDestoryとOfComponentという拡張メソッドを用意しています。それとちなみに性能面でも余計な中間コレクションを作らないため、(理屈上は)優位です。この理屈上ってのが、まぁ、あんまり踏み込みません:)

階層へのオブジェクトの追加

ところで利用法ですが、全てのメソッドはGameObjectへの拡張メソッドとして実装しています!暴力的に大雑把に!なので

using Unity.Linq;

としてもらえれば、全部のメソッドがにょきにょきっと生えます。メソッド一覧はリファレンスにあります。

オブジェクトを追加する、というのは階層を意識して追加していくわけですが、素でやるとparentにアタッチしてsiblingを弄って、というのは非常にカッタルイ話で絶対にみんな何とかゆーてぃりてぃを持っているとは思うのですが、LINQ to GameObjectにもあります。これも同様にLINQ to XMLと同じAPIを採用し、全ての方向/方法を網羅しています。

var root = GameObject.Find("root"); 
var cube = Resources.Load("Prefabs/PrefabCube") as GameObject; 

// Addは子の末尾に追加
// Parentの設定の他にレイヤーの統一とlocalPosition/Scale/Rotationを調整します
// 追加された子はCloneされていて、戻り値はそのCloneされたものを返します
var clone = root.Add(cube);

// 兄弟方向、自分の下に追加
// オブジェクトを追加する際は配列で渡せば複数一気に追加され、クローンされたオブジェクトをListで受け取れます
var clones = root.AddAfterSelf(new[] { cube, cube, cube });  

// 他にAddFirst(子の先頭に追加)とAddBeforeSelf(兄弟方向、自分の上)がある
// 追加の向きとしてはこれで全パターンでしょう!

// ついでに(?)Destoryの拡張メソッドもあり
// nullかどうかのチェック + 一旦階層から外してDestoryします
root.Destroy();

Addなんていう超汎用的くさい名前をGameObjectへの拡張メソッドにするってのがすっごく極悪なんですが、まぁいっか、みたいな。いいんですかね、いや、いいでしょう、はい、多分、うん。

LINQ to XMLはツリーへのLINQ的操作のリファレンスデザイン

LINQ to BigQuery - C#による型付きDSLとLINQPadによるDumpと可視化で、LINQの定義を

LINQがLINQであるためにはクエリ構文はいらない。Query Providerもいらない。LINQ to XMLがLINQなのは何故?Parallel LINQがLINQであるのは何故?Reactive ExtensionsがLINQであるのは何故?linq.jsがLINQであるのは何故?そこにあるのは……、空気と文化。

LINQと名乗ること自体はマーケティングのようなもので、形はない。使う人が納得さえすれば、LINQでしょう。そこにルールを求めたがる人がいても、ないものはないのだから規定しようがないよ?LINQらしく感じさせる要素をある程度満たしてればいい。FuncもしくはExpressionを使ってWhereでフィルタしSelectで射影する(そうすればクエリ構文もある程度は使えるしね)。OrderBy系の構文はOrderBy/OrderByDescending/ThenBy/ThenByDescendingで適用される。基本的な戻り値がシーケンスっぽい何かである。うん、だんだん満たせてくる。別に100%満たさなくても、70%ぐらい満たせばLINQらしいんだよ。

極論言えば私がLINQだって言ってるんだからLINQなのですが(何か文句ある?)、多くの人には十分納得してもらえると考えています

と、かなり乱暴な感じに「勝手に」定義しましたが(つまり今回のLINQ to GameObjectも私がLINQだって言ってるんだからLINQなのだ!)、実際、LINQ to GameObjectからはLINQらしさを感じ取れるんじゃないかと思います。何故か?当然理由はあるし、そうなるように意識してデザインしてます。

LINQ to XMLとは何であるのか。ツリー構造に対するLINQ的操作のリファレンスデザインだと捉えることができます。ツリー構造はLINQになる、そのガイドライン。LINQ to Objectsと非常に相性の良い探索の抽象化。もちろん、それ自体はXML向けだけど、「軸」を意識すればJSONにも適用できるし、そして、LINQ to GameObjectにも適用できた。

もしツリーを見かけたら、そこにLINQがなかったら、同じように作ることができるし、そうすればLINQの全てのメリットを甘受できる。データソースを発見していくこと。これは視点の問題で、そう捉えれば見えるようになる。それがLINQをただ漠然と使うことから一歩踏み出せるんじゃないのかな。

ユニットテスト

今回はじめてUnity Test Toolsをちょろっとだけ使ってみました。UniRxではファイルをリンクとしてコピーして普通の.NET上、Visual Studio上のMSTestで動かすという荒っぽいことをしてて、それはそれで楽ちんでいいんですけど、GameObjectへの操作とかUnityEngineに依存するものはさすがに無理で、今回のライブラリは100%それなので困ったなー、と。で、そこで、Unity Test Toolsの出番だったわけですね。

うん、まあ、普通ですね!いや、普通で、普通に悪くないです。ロジックのテストには全然いい。便利だよ。で、アサーションは普通にNUnitなんですが、私はAssert.AreEqualとかAssert.Thatとか嫌いなのです!嫌いなのでふつーのC#用にはChaining Assertionというライブラリを作って/公開しているんですが、それのUnity版を用意しました。

AssetStoreに投稿するほどのものかなーってことで、とりあえずLINQ to GameObjectのリポジトリ内にあるだけなんですが、気になる人はEditor/ChainingAssertion.csをどうぞ。この.csファイル一個だけです。これで

[Test]
public void Children()
{
    Origin.Children().Select(x => x.name)
        .IsCollection("Sphere_A", "Sphere_B", "Group", "Sphere_A", "Sphere_B");

    Origin.Children("Sphere_B").Select(x => x.name)
        .IsCollection("Sphere_B", "Sphere_B");

    Origin.ChildrenAndSelf().Select(x => x.name)
        .IsCollection("Origin", "Sphere_A", "Sphere_B", "Group", "Sphere_A", "Sphere_B");

    Origin.ChildrenAndSelf("Sphere_A").Select(x => x.name)
        .IsCollection("Sphere_A", "Sphere_A");
}

みたいな感じにテスト書けます/書きました。

まとめ

LINQは良い。LINQを使うにはIEnumerableが必要。LINQ to GameObjectはそのIEnumerableを作り出すので、UnityでよりLINQを活用できる!ので使いましょう。

(ところでObjectsAfterSelfはAfterSelfのほうが良いですね……すっごく失敗した……次のバージョンで変更するかも、というかします、はい……)。

あとLINQ to XMLの大きな要素として関数型構築があるんですが、勿論LINQ to GameObjectにも用意しました!ただ、実用性は(LINQ to XMLと違ってUnityの性質上)ビミョーなので、ここでは紹介しません。とりあえずとにかく、「ツリーを上下左右に探索できて」「ツリーの上下左右に追加できて」「関数型構築できれば」ツリーへのLINQ。と言えます、きっと。

それとUniRxなんですが、スライドで少し触れましたが、uFrameというUnity用のフレームワークに採用されて同梱されるようになりました。結構いい具合に躍進してきてるんで、UniRxも是非是非チェックを。ブログは全然書いてないんですが(!)機能拡充はずっと続けているんで、また近いうちに何か書きませう。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive