本ページにはプロモーションが含まれます
PlaymakerUnity

【Unity】GameObject.FindGameObjectsWithTagの配列の取得順序は保証されていない

この記事は約9分で読めます。

はじめに

PlaymakerのFindGameObjectsWithTagToArrayアクションを使って、指定したタグ名を持つ複数のオブジェクトを参照した際、順序よく取得されることを期待しましたが、実際にはそうではなかったため、その挙動と対処法についてまとめました。

PlaymakerのFindGameObjectsWithTagToArrayアクションは、内部的にGameObject.FindGameObjectsWithTagメソッドが使用されています。このGameObject.FindGameObjectsWithTagは、特定のタグを持つすべてのゲームオブジェクトを検索し、配列として返す静的メソッドです。しかし、このメソッドが返す配列の取得順序は、残念ながら保証されていません。Unity内部のGameObject管理状況に依存し、プロジェクトの状態やエディタの操作によって異なる順序で返されることがあります。この挙動は、直感的ではないかもしれません。

UnityのHierarchyビューに配置した「Slot1」「Slot2」「Slot3」といったGameObjectが、期待通りに配置順で取得されることはありませんでした。

実際の挙動

  • 取得順序は保証されず、ランダムに見えることがあります
    取得される順序は、Unityの内部でGameObjectが管理されている順序に依存します。これは、GameObjectがシーン内に配置された順序や、その生成・破棄のタイミングなど、Unity内部の管理状況に影響されるためです。
  • シーン内の階層構造やGameObject名には依存しません
    タグによる検索は、UnityのHierarchy上の階層構造やGameObjectの名前には依存せず、指定されたタグを持つ全てのオブジェクトをシーン全体からスキャンします。そのため、Hierarchyで目視できる視覚的な順序とも必ずしも一致するわけではありません。

順序を制御したい場合

  1. 取得した配列をソートする
    GameObjectを検索して取得した配列を、特定の基準で手動でソートすることで、順序を保証できます。

例: 名前でソート

GameObject[] objects = GameObject.FindGameObjectsWithTag(tag.Value);
System.Array.Sort(objects, (a, b) => string.Compare(a.name, b.name));

例: 位置でソート(例: x座標順)

GameObject[] objects = GameObject.FindGameObjectsWithTag(tag.Value);
System.Array.Sort(objects, (a, b) => a.transform.position.x.CompareTo(b.transform.position.x));
  1. 管理用スクリプトを利用する
    検索対象となるオブジェクトを、あらかじめ独自のリストやDictionaryなどに登録し、任意の順序や特定の基準でアクセスできるように一元的に管理する方法です。

例: スクリプトでオブジェクトを登録

using System.Collections.Generic;
using UnityEngine;
public class TagObjectManager : MonoBehaviour
{
    public static List<GameObject> taggedObjects = new List<GameObject>();
    public static void Register(GameObject obj)
    {
        if (!taggedObjects.Contains(obj))
        {
            taggedObjects.Add(obj);
        }
    }
    public static void Unregister(GameObject obj)
    {
        if (taggedObjects.Contains(obj))
        {
            taggedObjects.Remove(obj);
        }
    }
}

この例では、GameObjectが生成された際にTagObjectManager.Register(gameObject)を呼び出してリストに登録し、削除時にUnregisterを呼び出すことで管理します。このリストも、必要に応じてソートすることで任意の順序で操作することが可能です。

  1. Hierarchyの順序を考慮した管理(非推奨)
    UnityエディタのHierarchyビューで手動でGameObjectの配置順を調整し、その視覚的な順序を利用して制御する方法も考えられますが、これは非常に限定的なケースにのみ有効です。プロジェクトが複雑化するにつれて管理が困難になり、意図しない変更が発生しやすいため、一般的には推奨されません。

結論

GameObject.FindGameObjectsWithTagメソッドで取得されるGameObjectの順序は、Unityによって保証されていません。そのため、もし取得するオブジェクトの順序が重要であるならば、取得後に手動でソート処理を行うか、あるいは前述したような管理用スクリプトを利用して別の方法で管理することが確実です。

参考先

おわりに

GameObject.FindGameObjectsWithTagで取得した結果は、配列として手動でソートすれば良いだけ、と思われる方もいるかもしれません。

実際、PlaymakerのFindGameObjectsWithTagToArrayアクションの次に、Array Sortアクションを追加して対応しようとしました。Playmakerでゲームロジックを構築している以上、できる限りPlaymaker内で完結させたいという思いがあるかと思います。
ArgumentException: At least one object must implement IComparable.というエラーが発生しました。

これは、PlaymakerのArray Sortアクションが、配列内の要素を比較してソートするために、その要素がIComparableインターフェースを実装しているか、あるいはカスタムの比較メソッド(コンパレータ)が提供されている必要があるためです。コンパレータとは、Array.SortList.Sortなどを用いて配列やリストをソートする際に、要素同士をどのように比較するかを定義するためのロジックのことです。

なぜカスタムコンパレータが必要なのでしょうか。Array.SortList.Sortは、intfloatstringといった基本的なデータ型については、IComparableインターフェースがデフォルトで実装されているため、自動的に比較方法が定義されています。しかし、GameObjectや独自のカスタムクラスのような複雑なオブジェクトの場合、UnityやC#の標準では、どのプロパティを基準に比較すべきかが一意に定まりません。そのため、開発者自身が「このオブジェクトとこのオブジェクトは、この基準(例えば名前や位置など)で比較してください」と、具体的な比較方法を定義する必要があるのです。

初めてこのようなエラーに遭遇すると、戸惑ってしまうかもしれません。PlaymakerのFindGameObjectsWithTagToArrayアクションに、最初からソート機能が組み込まれていれば、と感じることも少なくないでしょう。

using HutongGames.PlayMaker;
using System.Collections.Generic;
using UnityEngine;
namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("General")]
    [Tooltip("GameObjectの配列を名前順に並べ替えます。")]  
    public class SortGameObjectsByName : FsmStateAction
    {
        [RequiredField] 
        [UIHint(UIHint.Variable)]  
        [Tooltip("並べ替えるGameObjectの配列です。")]  
        public FsmArray gameObjectArray;  // GameObjectの配列を受け取る変数
        // リセット時の初期設定
        public override void Reset()
        {
            gameObjectArray = null;  // 配列をnullで初期化
        }
        // アクションが開始されたときに呼び出される
        public override void OnEnter()
        {
            // 配列がnullまたは空の場合は終了
            if (gameObjectArray == null || gameObjectArray.Length == 0)
            {
                Finish();
                return;
            }
            // FsmArrayからGameObjectリストを作成
            List<GameObject> objects = new List<GameObject>();
            foreach (var obj in gameObjectArray.Values)
            {
                if (obj is GameObject gameObject)
                {
                    objects.Add(gameObject);  // 配列の各要素をリストに追加
                }
            }
            // リストを名前順に並べ替え
            objects.Sort((a, b) => string.Compare(a.name, b.name, System.StringComparison.Ordinal));
            // 並べ替えたリストでFsmArrayを更新
            gameObjectArray.Resize(objects.Count);  // 配列のサイズをリストに合わせて変更
            for (int i = 0; i < objects.Count; i++)
            {
                gameObjectArray.Set(i, objects[i]);  // 配列の各要素にソートされたGameObjectを設定
            }
            // アクションの終了
            Finish();
        }
    }
}

そこで、GameObject配列を名前順にソートするためのカスタムアクションを作成しました。これをベースに、昇順・降順の選択機能や、位置、その他のプロパティをキーとしたソート機能などを実装することで、より汎用的に活用できるようになります。

このカスタムアクションを使用することで、期待通りの順序でGameObjectをソートし、処理を進めることができるようになりました。