読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

Unity(C#) 便利なEnumerable

今回のエンジニアブログを担当する加賀です。

Enumerableクラスを使用して、いくつかの場面を簡潔に記述していきます。
今回のコードはUnity 4.5.3f3、Mono / .NET 3.5で確認しています。

System.Linq.Enumerableクラスは、.NET Framework 3.5以降で使用できるクラスで、
主にIEnumerableに対する拡張機能を提供しているクラスです。
IEnumerableインタフェースを実装しているもの(コレクション)は、配列、ListDictionary<TKey, TValue>などがあります。

自分以外の全ての子のGameObjectのリストを取得

GetComponentsInChildren()を使用すれば自分自身を含む全ての子のTransformが取得できます。
しかし、自分自身を省いたり、GameObjectに変換したりするコードが手間だったりします。

using UnityEngine;  
using System.Collections.Generic;  
using System.Linq;  
  
public class Example1 : MonoBehavior {  
  /// <summary>  
  /// 自分以外の全ての子のGameObjectのリストを取得する  
  /// </summary>  
  /// <param name="obj">子を取得したいGameObject</param>  
  /// <return>自分以外の全ての子のGameObjectのリスト</return>  
  public static List<GameObjects> GetAllChildGameObjects(GameObject obj) {  
    // objがnullなら、空のリストを返す  
    if (!obj)  
      return new List<GameObject> ();  
  
    // 自分自身を含む全ての子のTransformを取得  
    Transform[] allTransform = obj.GetComponentsInChildren<Transform> ();  
  
    // 自分自身を除いたTransformをGameObjectに変換する  
    List<GameObject> gameObjects = new List<GameObject> ();  
    foreach(Tranform t in allTransform) {  
      // 自分自身でないなら  
      if (t != obj.transform) {  
        // GameObjectに変換し、リストに追加する  
        gameObjects.Add (t.gameObject);  
      }  
    }  
  
    // GameObjectのリストを返す  
    return gameObjects;  
  }  
}  

このように、結構長いコードを書く必要があります。
しかし、Enumerableクラスの提供する拡張メソッドを使用すれば、こんなに長く記述する必要はありません。

  /// <summary>  
  /// 自分以外の全ての子のGameObjectのリストを取得する  
  /// </summary>  
  /// <param name="obj">子を取得したいGameObject</param>  
  /// <return>自分以外の全ての子のGameObjectのリスト</return>  
  public static List<GameObject> GetAllChildGameObjects(GameObject obj) {  
    // objがnullなら、空のリストを返す  
    if (!obj)  
      return new List<GameObject> ();  
  
    // 自分自身を含む全ての子のTransformを取得  
    return obj.GetComponentsInChildren<Transform> ()  
    // 自分自身以外の全ての子のTransformを取得  
      .Where (delegate (Transform t) {  
        if (t != obj.transform)  
          return true;  
        return false;  
      })  
    // TransformをGameObjectに変換  
      .Select (delegate (Transform t) {  
        return t.gameObject;  
      })  
    // リスト化  
      .ToList ();  
  }  

これは匿名メソッドを使用しましたが、1文程度ならラムダ式を使用することもできます。
ラムダ式を使用するなら、横に長くなりますが1行で書くことも可能です。
ラムダ式を使わない上と、一時変数名は同じにしておきます。

  /// <summary>  
  /// 自分以外の全ての子のGameObjectのリストを取得する  
  /// </summary>  
  /// <param name="obj">子を取得したいGameObject</param>  
  /// <return>自分以外の全ての子のGameObjectのリスト</return>  
  public static List<GameObject> GetAllChildGameObjects(GameObject obj) {  
    // objがnullなら、空のリストを返す  
    if (!obj)  
      return new List<GameObject> ();  
  
    // 自分自身を含む全ての子のTransformを取得  
    return obj.GetComponentsInChildren<Transform> ()  
    // 自分自身以外の全ての子のTransformを取得  
      .Where (t => t != obj.transform)  
    // TransformをGameObjectに変換  
      .Select (t => t.gameObject)  
    // リスト化  
      .ToList ();  
  }  

上のラムダ式は、引数を1つ取り、値を返すタイプのデリゲートのようなものです。
=>の左側が引数を受け取る変数名で、=>の右側にその値を代入するというイメージです。

Enumerableクラスの提供する拡張メソッドの説明は以下です。

関数シグネチャ 説明
IEnumerable Where (     System.Func predicate ) コレクションの要素1つごとに、predicateが呼び出されます。 Whereメソッドの戻り値は、predicateがtrueを返した要素の集合です。 通常、predicateには、この要素が条件に合うかどうかの判定を記述します。 必ずbool値を返却する必要があります。
IEnumerable Select (     System.Func selector ) コレクションの要素1つごとに、selectorが呼び出されます。 Selectメソッドの戻り値は、selectorが返した値の集合です。 通常、selectorには、型の変換処理や、この要素への値の更新などを記述します。 必ずなにか値を返却する必要があります。

関数シグネチャを見ればわかるように型指定が必要ですが、今回のような書き方をすれば、コンパイラが自動で型を推定してくれます。

Enumerableクラスの提供する拡張メソッドとラムダ式を使うことで、こんなにも短くする事が出来ました。

foreach文でもループカウンタを使用する

配列内のGameObjectを一列に並べるときに、for文でループカウンタなどを使用して実装しますが、
条件文を間違えたり、カウンタ増加を忘れたりとミスも起こりがちです。
foreach文ではループカウンタがないので、今何番目の要素なのかを知ることが簡単にはできません。
しかし、Enumerable.Rangeメソッドを使用すれば、foreach文でもループカウンタを使用できます。
(配列にはインデックスアクセスになってしまいますが・・・)

関数シグネチャ 説明
IEnumerable Range (     int start,     int count ) startの値からcount個分、1ずつ増える値を返却するオブジェクトを作成する。 主に、ループカウンタが欲しい場合に、foreach文のコレクション記述位置(inの後ろ)に記述する。

using UnityEngine;  
using System.Collections.Generic;  
using System.Linq;  
  
public class Example2 : MonoBehavior {  
  /// <summary>  
  /// オブジェクト配列  
  /// </summary>  
  [SerializeField] GameObject[] _gameObjectArray = new GameObject[0];  
  
  /// <summary>  
  /// オブジェクト配列のオブジェクトを一列に並べる  
  /// </summary>  
  public void Start() {  
    // _gameObjectArrayの要素の個数回ループする  
    foreach(int i in Enumerable.Range (0, _gameObjectArray.Length)) {  
      // X+方向に並べる  
      _gameObjectArray[i].transform.localPosition = new Vector3 (5f * i, 0f, 0f);  
    }  
  }  
}  
終わり

今回はこの2つだけですが、匿名メソッド、ラムダ式を使えばさらに応用は広がります。
Listには、値を返さないメソッドをコレクションの各要素に対して実行するForEach()メソッドもあります。
配列では、静的メソッドとしてForEach()が用意されています。
これらを使用して、短く、それでいて分かりやすいコードを書いていこうと思います。