e.blog

主にUnity/UE周りのことについてまとめていきます

実行時に使えるユニークな識別子を生成する

概要

Unityで開発していると、ときに、「固定の」ユニークなID(識別子)を割り振りたいときがあります。
Unityにはもともと、Object.GetInstanceIDがありますが、これはユニークではあるものの、実行時や保存時などに書き換わる可能性があります。
(どのタイミングで変わるかはちゃんと調べてませんが、同じオブジェクトのIDが変わることがあるのを確認しています)

今回書くことは、各オブジェクトの固定IDです。
利用シーンとしては、オブジェクトごとのデータを保存してあとで復元する、などを想定しています。

具体例で言うと、シーンをまたいだときに、そのオブジェクトの位置を保存しておく、と言った用途です。

解決策

まず最初に、どうやって固定のIDを割り振るかを書いてしまうと、単純に、そのオブジェクトの「階層」を利用します。

つまり、OSのファイル管理と同様のことをやるってことですね。
各オブジェクトは必ず階層構造を持ち、シーンファイルに紐付いています。
つまり、シーンファイル名をルートとした階層構造を文字列化すれば、それはユニークなIDとなります。

一点だけ注意点として、OSであれば同名ファイル名は許されていませんが、Unityの場合は同階層に同じ名前のオブジェクトを配置することができます。
なので、少しだけ工夫して、最後に、Siblingのindexを付与することでユニークなIDができあがります。

最終的にはこのパスを元にした文字列のハッシュ値を保持して比較することで、ユニークなIDとして利用することができるようになります。

コード

コードで示すと以下のようになります。

// 以下は、「GameObjectUtility」クラスの静的メソッドとして定義していると想定。

/// <summary>
/// ヒエラルキーに応じたパスを取得する
/// </summary>
static public string GetHierarchyPath(GameObject target)
{
    string path = "";
    Transform current = target.transform;
    while (current != null)
    {
        // 同じ階層に同名のオブジェクトがある場合があるので、それを回避する
        int index = current.GetSiblingIndex();
        path = "/" + current.name + index + path;
        current = current.parent;
    }

    Scene belongScene = target.GetBelongsScene();

    return "/" + belongScene.name + path;
}

※ 上のコードのGetBelongsSceneは拡張メソッドで、以下のように実装しています。

using UnityEngine.SceneManagement;

public static class GameObjectExtension
{
    public static Scene GetBelongsScene(this GameObject target)
    {
        for (int i = 0; i < SceneManager.sceneCount; i++)
        {
            Scene scene = SceneManager.GetSceneAt(i);
            if (!scene.IsValid())
            {
                continue;
            }

            if (!scene.isLoaded)
            {
                continue;
            }
            
            GameObject[] roots = scene.GetRootGameObjects();
            foreach (var root in roots)
            {
                if (root == target.transform.root.gameObject)
                {
                    return scene;
                }
            }
        }

        return default(Scene);
    }
}

実際に使う際は、取得したパス文字列のハッシュを保持しておきます。

string id = GameObjectUtility.GetHierarchyPath(gameObject);
int hash = id.GetHashCode();

// hashをなにかしらで保存する

階層構造の変更に対応する

さて、上記までである程度固定のIDを振ることができますが、動的に、階層構造が変更される、あるいは子要素が追加されるなどは当然ながら発生します。

すると問題になるのが、実行順によって階層構造が変更され、完全なユニーク性が失われる、ということです。

なので、シーンファイルの保存タイミングをフックして、その際にSerializeFieldに保持してしまう、という方法でこれを解決します。

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

/// <summary>
/// 保存時に、シーンに設定されているユニークID保持対象のPathを設定(ベイク)する
/// </summary>
public class SaveAllUniquePath : UnityEditor.AssetModificationProcessor
{
    /// <summary>
    /// アセットが保存される直前のイベント
    /// </summary>
    /// <param name="paths">保存される対象アセットのパス</param>
    static private string[] OnWillSaveAssets(string[] paths)
    {
        foreach (var path in paths)
        {
            Scene scene = SceneManager.GetSceneByPath(path);

            if (scene.IsValid())
            {
                GameObject[] roots = scene.GetRootGameObjects();
                foreach (var root in roots)
                {
                    RecursiveUpdateUniquePath(root.transform);
                }
            }
        }

        return paths;
    }

    /// <summary>
    /// 再帰的にUniquePathを更新する
    /// </summary>
    static void RecursiveUpdateUniquePath(Transform target)
    {
        UniquePathTarget upt = target.GetComponent<UniquePathTarget>();
        if (upt != null)
        {
            upt.SetUniquePathAndHash();
        }

        for (int i = 0; i < target.childCount; i++)
        {
            RecursiveUpdateUniquePath(target.GetChild(i));
        }
    }
}

以上です。
まぁぶっちゃけベイクしてしまうので、そもそもシーン名と連番やインスタンスIDでもそれなりに動くようにはできますが、実行順をちゃんと制御できるなら完全なランタイム時にIDを生成しても動くものが作れるのでメモとして書いてみました。