Unityプロジェクトを通してジェネリックを学習しましょう

2021年4月5日

ゲームオブジェクトにアタッチされている特定のコンポーネントをまとめて処理する目的のために、ジェネリックを活用してみましょう。

プロジェクトの作成とマネージャの基本動作

3Dプロジェクトを作成してください。

オブジェクトの配置

4つのCubeオブジェクトと処理をするための1つの空のゲームオブジェクト作成します。
なお、オブジェクトの親子、またアクティブ・非アクティブの関係についても一緒に見ていきましょう。

次のシーンを参考にしてください。

Cube

4つのCubeの違いは、Positionだけです。

各CubeのPosition

  • Cube (0, 0, 0)
  • Cube(1) (2, 0, 0)
  • Cube(2) (2, 0, 0)
  • Cube(3) (6, 0, 0)

Cube(1)とCube(2)のPositionが同じに見えますが、親子関係の場合は親の位置からの相違(差)が表示されます。なので、これで、4つの Cubeは位置2ずつx軸に並んでいることなります。なお、Cube(1)とCube(2)は非アクティブになっていますので、ここでは表示されていないことに注意しましょう。

GameObject

テストコードを記述するために空のゲームオブジェクトを作成します。
基本として、Positionはリセット(0, 0, 0)しておきます。

スクリプト

いきなり複雑なテストも難しく感じますので、まず小さなものから始めましょう。
・・・ただし、あまり実用性が無いことに注意してください。

GenericsSample.cs (コンポーネントの名前を取得する)

自作のジェネリックメソッドを作成し、呼び出してみます。

using UnityEngine;

public class GenericsSample : MonoBehaviour
{
    void Start()
    {
        string componentString = Utility.GetComponentName<Rigidbody>();

        Debug.Log($"コンポーネント名を文字列にしました {componentString}");
    }
}

コードを分析してみましょう。
Utilityの部分はクラス名です。
Utility + . + GetComponentName<Rigidbody>()
クラス名 + .(ピリオド)+ メソッド名となっていますので、これはstaticメソッドと判断できます。(staticメソッドは、インスタンスを作らないため)
ネーミングは、Get(得る)Component(コンポーネント)Name(名前)<Ridigbody>(取得したいコンポーネント)にしました。
(取得したい)コンポーネントの名前を取得すると呼んでもらえればいいと思います。
なお、戻り値の型は、stringにします。

Utility.GetComponentName<Rigidbody>();

次は、結果の表示です。得られた文字列を表示しています。

Debug.Log($"コンポーネント名を文字列にしました {componentString}");

Utility.cs (型の名前を文字列で返すジェネリックメソッドが実装されているクラス)

では、実装をみていきましょう。
このクラスは、インスタンスを作らないので、static修飾子をつけます。
クラスをstaticにした場合、全てのメンバーにstaticが必要になります。

using UnityEngine;

static class Utility
{
    public static string GetComponentName<T>()
    {
        return typeof(T).Name;
    }
}

メソッドのシグネチャをみてみます。
public + static + string + GetComponentName()
の形式になっています。
アクセス修飾子 + static修飾子 + 戻り値の型 + メソッド名()
ですね。
<T>の部分がジェネリックになります。List<string>などで見かけましたよね。
このTは型引数(タイプパラメータ)と呼ばれ、メソッドの引数の考え方に近いです。
Tには、クラス名を含む型を記述して呼び出します。

public static string GetComponentName<T>()

コードブロックをみていきます。
typeof(T)は、型Tの宣言を取得するメソッドになります。
Nameプロパティで取得することで、string型での取得にします。

return typeof(T).Name;

実行結果

ちなみに、ゲームオブジェクトを配置しましたが、このテストでは影響しません。

ジェネリックメソッドの効果をみるために、Ridigbodyの代わりにCollider型に置き換えてテストしてみましょう。

うまくいきましたか?

スクリプト

GenericsSample.cs (親オブジェクトにアタッチされている特定のコンポーネント一覧を取得する)

全ての親オブジェクトにそれぞれアタッチされているコンポーネントで、欲しいものだけを配列で取得できるスクリプトを作成してみます。

using UnityEngine;

public class GenericsSample : MonoBehaviour
{
    void Start()
    {
        Collider[] colliders = Utility.GetAllRootObjectsComponents<Collider>();

        foreach (var collider in colliders)
        {
            Debug.Log($"名前 { collider.name}  コンポーネント {collider.GetType()}");
        }
    }
}

コードを分析します。
型引数をColliderとしています。これで、親オブジェクトにアタッチされているColliderコンポーネントを配列にしたのを取得しようとしています。

Collider[] colliders = Utility.GetAllRootObjectsComponents<Collider>();

コードを分析します。
型引数をColliderとしています。これで、親オブジェクトにアタッチされているColliderコンポーネントを配列にしたのを取得しようとしています。

結果は、配列に入っていますので、foreachで取り出してみます。(for文でも構いません。その場合、colliders.Lengthまでループすればいいでしょう)

foreach (var collider in colliders)
{
    Debug.Log($"名前 { collider.name}  コンポーネント {collider.GetType()}");
}

Utility.cs (親オブジェクトにアタッチされている特定のコンポーネント一覧を取得するメソッドを追加)

では、実装をみていきましょう。
このメソッドも同様にstaticにします。

public static T[] GetAllRootObjectsComponents<T>() where T : Component
{
    List<T> components = new List<T>();

    Scene scene = SceneManager.GetActiveScene();

    GameObject[] sceneGameObjects = scene.GetRootGameObjects();

    foreach (GameObject obj in sceneGameObjects)
    {
        T component = obj.GetComponent<T>();

        if (component != null)
        {
            components.Add(component);
        }
    }

    return components.ToArray();
}

分析してみます。
戻り値が、T[]になっていますね。
指定された型引数の配列タイプが戻り値の型なのがわかります。
where T : Component句で、型パラメータの制約をComponent または、Componentを継承した型にしています。
これは、UnityのGetComponent<>の型パラメータの Component制約が必要なためです。

public static T[] GetAllRootObjectsComponents<T>() where T : Component

components変数に指定された型Tのリストを使います。
これは、ゲームオブジェクトごとのコンポーネント一覧を保存しておく一時置き場になります。

List<T> components = new List<T>();

components変数に指定された型Tのリストを使います。
これは、ゲームオブジェクトごとのコンポーネント一覧を保存しておく一時置き場になります。

次のコードは、SceneManagerクラスのstaticメソッドGetActiveSceneの参照を取得しているものになります。
戻り値の型は、Scene型になります。現在のシーンを取得しています。

Scene scene = SceneManager.GetActiveScene();

次のコードで、シーンの中の親オブジェクト(ルートオブジェクト)の一覧を取得しています。
戻り値は、GameObject型の配列になります。

GameObject[] sceneGameObjects = scene.GetRootGameObjects();

次のコードは、1つのゲームオブジェクトのコンポーネント一覧を配列に代入するためのものです。
1つのゲームオブジェクトには、複数のコンポーネントがアタッチされています。
今回は、指定した特定のコンポーネント(指定は型Tでしたよね)だけを取得して、配列に代入します。もちろん、型はTになります。

T component = obj.GetComponent<T>();

次のコードは、SceneManagerクラスのstaticメソッドGetActiveSceneの参照を取得しているものになります。
戻り値の型は、Scene型になります。現在のシーンを取得しています。

一時的に使うためのListに先程の指定コンポーネントを追加します。
if文は、そのコンポーネントが存在しなかったときの処理で、nullでない(存在する)場合だけ追加するようにしています。
たとえば、カメラなどは、Colliderコンポーネントがアタッチされていないですよね。

このクラスにComponent制約(where T : Component)がない場合、ここでエラーになりますので注意してください。

if (component != null)
{
    components.Add(component);
}

結果をリターンしています。
List型のため、Array(配列)に変換して戻しています。

return components.ToArray();

実行結果

CubeとCube(3)オブジェクトにアタッチされているBoxColliderが取得できています。
指定した型は、Colliderなのですが、BoxColliderはCollider型を継承していますので、取得することができています。
残りのCube(1)とCube(2)は、非アクティブのため、未取得となっています。

考えてみましょう

呼び出しを次のように変更してみましょう。

int[] colliders = Utility.GetAllRootObjectsComponents<int>();

エラーにならないですね。
intも型なので、適用されます。
でも、int型のコンポーネントなんてアタッチされている可能性があるでしょうか?
・・・ないですよね。
そこで、コンポーネント、または、コンポーネントを継承しているクラスしか型引数に指定してはならないような制約を設けましょう。
次のシグネチャを見てください。

 public static T[] GetAllRootObjectsComponents<T>() where T : Component

そうです。where T : Componentが追加されています。
これは、型Tは、Component型または、それを継承している型であることを強制するものになります。

変換ができない旨、表示されて実行すらできないようにすることができました。これだと間違っておかしな動作をしてしまうことを防げますね。

スクリプト

GenericsSample.cs (非アクティブを含む全てのオブジェクトにアタッチされている特定のコンポーネント一覧を取得する)

全てのオブジェクト(子オブジェクト含む)にそれぞれアタッチされているコンポーネントで、欲しいものだけを配列で取得できるスクリプトを作成してみます。
前回との違いは、呼び出すメソッド名だけです。

using UnityEngine;

public class GenericsSample : MonoBehaviour
{
    void Start()
    {
        Collider[] colliders = Utility.GetComponentsAll1<Collider>();

        foreach (var collider in colliders)
        {
            Debug.Log($"名前 { collider.name}  コンポーネント {collider.GetType()}");
        }
    }
}

Utility.cs (親オブジェクトにアタッチされている特定のコンポーネント一覧を取得するメソッドを追加)

では、実装をみていきましょう。
このメソッドも同様にstaticにします。

public static T[] GetComponentsAll1<T>() where T : Component
{
    List<T> components = new List<T>();

    Scene scene = SceneManager.GetActiveScene();

    GameObject[] sceneGameObjects = scene.GetRootGameObjects();

    foreach (GameObject obj in sceneGameObjects)
    {
        T[] allComponents = obj.GetComponentsInChildren<T>(true);

        components.AddRange(allComponents);
    }

    return components.ToArray();
}

次のコードは、1つのゲームオブジェクトで子オブジェクトを含むコンポーネント一覧を配列に代入するためのものです。
引数をtrueにすると、非アクティブオブジェクトも含んで取得します。
falseの場合または、引数なしで非アクティブオブジェクトを除外した取得になります。

T[] allComponents = obj.GetComponentsInChildren<T>(true);

一時的に使うためのListに先程の指定コンポーネントの配列を追加します。その場合、Addメソッドではなく、AddRangeメソッドを使います。
if文でのnull判断は今回不要です。存在しない場合、配列の要素数が0になるため、追加がされないためです。
たとえば、カメラなどは、Colliderコンポーネントがアタッチされていないですよね。

components.AddRange(allComponents);

実行結果

子オブジェクトと非アクティブオブジェクトを含む全てのゲームオブジェクトにアタッチされているBoxColliderが取得できています。

別のコード

GetComponentsAll2という名前のメソッドを作りました。
GetComponentsAll1との違いは、取得の方法になります。
これは、Objectクラスのメソッドを使って、あらゆるものから型Tのインスタンスを取得するものになります。
処理の違いから、こちらの方が遅くなります。

public static T[] GetComponentsAll2<T>() where T : Component
{
    List<T> components = new List<T>();

    T[] gameObjects = Object.FindObjectsOfType<T>(true);

    components.AddRange(gameObjects);

    return components.ToArray();
}

ジェネリックメソッドの全てのコード

Utility.cs

これまでのコードをまとめたものになります。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

static class Utility
{
    public static string GetComponentName<T>()
    {
        return typeof(T).Name;
    }

    public static T[] GetAllRootObjectsComponents<T>() where T : Component
    {
        List<T> components = new List<T>();

        Scene scene = SceneManager.GetActiveScene();

        GameObject[] sceneGameObjects = scene.GetRootGameObjects();

        foreach (GameObject obj in sceneGameObjects)
        {
            T component = obj.GetComponent<T>();

            if (component != null)
            {
                components.Add(component);
            }
        }

        return components.ToArray();
    }

    public static T[] GetComponentsAll1<T>() where T : Component
    {
        List<T> components = new List<T>();

        Scene scene = SceneManager.GetActiveScene();

        GameObject[] sceneGameObjects = scene.GetRootGameObjects();

        foreach (GameObject obj in sceneGameObjects)
        {
            T[] allComponents = obj.GetComponentsInChildren<T>(true);

            components.AddRange(allComponents);
        }

        return components.ToArray();
    }

    public static T[] GetComponentsAll2<T>() where T : Component
    {
        List<T> components = new List<T>();

        T[] gameObjects = Object.FindObjectsOfType<T>(true);

        components.AddRange(gameObjects);

        return components.ToArray();
    }
}

Utility.cs(リファクタリングしたコード)

ポイントは次のところになります。

  • ジェネリックのメソッドからクラスへ変更(これにより、ジェネリッククラスになります)
  • 静的コンストラクタの追加(初期値を代入するためです。外部クラスでこのクラスのインスタンスは作成できません。自身でコンストラクタを実行します)
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

static class Utility<T> where T : Component
{
    static List<T> components = new List<T>();

    static Scene scene;

    static GameObject[] sceneGameObjects;

    static Utility()
    {
        scene = SceneManager.GetActiveScene();
        sceneGameObjects = scene.GetRootGameObjects();
    }

    public static string GetComponentName()
    {
        return typeof(T).Name;
    }

    public static T[] GetAllRootObjectsComponents()
    {
        foreach (GameObject obj in sceneGameObjects)
        {
            T component = obj.GetComponent<T>();

            if (component != null)
            {
                components.Add(component);
            }
        }

        return components.ToArray();
    }

    public static T[] GetComponentsAll1()
    {
        foreach (GameObject obj in sceneGameObjects)
        {
            T[] allComponents = obj.GetComponentsInChildren<T>(true);

            components.AddRange(allComponents);
        }

        return components.ToArray();
    }

    public static T[] GetComponentsAll2()
    {
        T[] gameObjects = Object.FindObjectsOfType<T>(true);

        components.AddRange(gameObjects);

        return components.ToArray();
    }
}

GenericsSample.cs (リファクタリングしたコード)

最後のテストに絞っていますが、次のようになります。
ジェネリックメソッドからジェネリッククラスにアクセスが変更になりますので、それに合わせて呼び出し元のクラスも変更する必要があります。

using UnityEngine;

public class GenericsSample : MonoBehaviour
{
    void Start()
    {
        Collider[] colliders = Utility<Collider>.GetComponentsAll2();

        foreach (var collider in colliders)
        {
            Debug.Log($"名前 { collider.name}  コンポーネント {collider.GetType()}");
        }
    }
}

クラス図

まとめ

Unityのコンポーネントを題材に実際のジェネリックについて扱ってみました。
今回はColliderでしたが、RigidbodyやRendererなど多くのコンポーネント、また、スクリプト名(=クラス名)もコンポーネントなので型引数にすることができます。(スクリプト全てを取得したければ、MonoBehaviourもあります)
このように、ジェネリックを使わないとほぼ無限にメソッドが必要となるところが1つで済むところにジェネリックの利点があるわけです。

ソースコード

Unity バージョン 2021.1.1f1

C#,Unity

Posted by hidepon