Unity開発:ヒエラルキーの状態抽出スクリプト作成について

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

個人も法人もゲーム開発を行う際に生成AIを使用することがメジャーとなってきています。
確かに、最新の生成AIをUnity開発に組み込む際のコードの生成能力は飛躍的に向上しています。一方で、長時間のチャットでコンテキスト(文脈)を維持することが難しくなっています。

皆さんもチャットが長くなってくると以前言った事を覚えていない、今までと一貫性のない回答をするなどやきもきした経験はありませんか?

私も個人でゲーム開発を行っているのですが、どの生成AIを利用してもこの問題にぶち当たり、
AIに悪態をついてしまうこともあります。
ただ、新たなチャットを開始すると今までの引継ぎに何ラリーも必要となり、
非常に効率が悪いです。

それを解消するためにヒエラルキーとインスペクターの情報もAIに引き継げる
コードの作り方をご紹介できればと思います。

本記事では、AIとの連携時における「文脈喪失」の課題と、それを解決するために作成したエディタ拡張スクリプトについて解説します。

スクリプト作成の経緯:最新AIのアーキテクチャ変化とUnityの相性

以前のAIモデルでは、1つの長いチャットスレッドで「少しずつ機能を足していく」という対話型の開発が可能でした。
しかし、推論能力が強化された最新モデルでは、長大なチャット履歴を保持したまま複雑な指示を出すと、内部の推論プロセスが衝突し、指示の無視や堂々巡りのループといった破綻を引き起こしやすくなっています。

これを防ぐための最も確実な運用方法は、「タスクごとに新規チャットを立ち上げる(ステートレスな開発)」ことです。

しかし、Unity開発においてこのアプローチをとる場合、致命的な問題が発生します。Unityのゲームロジックはスクリプトだけでなく、ヒエラルキーの親子関係や、インスペクター上で設定されたコンポーネントのパラメータに強く依存しています。
新規チャットを立ち上げるたびに、「現在のオブジェクトにはこのコンポーネントがついていて、パラメータはこれで…」と手動でテキスト化してAIに伝えるのは、非効率の極みです。

何とか作成したコード(.cs)やヒエラルキーとインスペクターも全て引き継いで
次のチャットに以降できないかを考えていました。

ContextExporter.cs の機能

そういったなかで作成したのがこのスクリプトです。Unityエディタ上で選択したゲームオブジェクトの「現在の状態」をAIが読み取りやすいテキストデータとして一括出力し、クリップボードにコピーする機能を提供します。

主な仕様:

階層構造の再帰的取得:選択したルートオブジェクトだけでなく、その下にある子オブジェクトの階層ツリーをインデント(字下げ)付きで正確に出力します。

コンポーネントとプロパティの抽出:各オブジェクトにアタッチされているコンポーネントをスキャンし、インスペクター上で可視化されている主要なパラメータ(数値、文字列、フラグ、参照など)を抽出します。

ノイズの排除:スクリプトの内部参照(m_Script)など、AIの推論にとってノイズとなる不要なメタデータは自動で除外されるよう設計されています。

使い方はシンプルで、ヒエラルキー上で対象のオブジェクトを複数選択し、右クリックメニューから「Copy Context for AI」を実行するだけです。

導入手順

  • Unityエディタの Assets フォルダ配下に、新しく Editor という名前のフォルダを作成します(既に存在する場合はそれを使用します)。
  • その中に ContextExporter.cs という名前でC#スクリプトを作成し、以下のコードを記述します。(スクリプト名はお好きなお名前で)
  • 本ページ最下部の「using UnityEngine; 」から始まるコードをそのままコピー

使用方法

  • Unityのヒエラルキービューで、情報を抽出したいオブジェクトを右クリックします。
  • メニューから Copy Context for AI を選択します。
  • クリップボードにテキストとして情報がコピーされますので、新規チャットのプロンプトに貼り付け、「この状態を前提に、〇〇の機能を追加するスクリプトを作成せよ」といった形式で指示を出します。

これを作る(導入する)メリット

このスクリプトを導入することで、以下のメリットが得られます。

AI開発における「ステートレス化」の実現

チャットが破綻しそうになったり、新しい機能を実装したくなった際、いつでもためらいなく新規チャットに移行できます。クリップボードのテキストを貼り付けるだけで、AIは現在のUnityシーンの状態を完全に把握した状態から推論をスタートできます。

複雑な仕様伝達の自動化

例えば、レーシングゲームの車両に設定された細かな物理挙動パラメータや、3D脱出ゲームの複雑なギミックの階層構造などを、手作業で書き起こす必要がなくなります。出力されたテキストと「この状態をベースに〇〇の機能を追加するコードを書いて」と指示するだけで済みます。

トークン消費の最適化とハルシネーションの抑制

プロジェクト全体を読み込ませるのではなく、現在作業しているスコープ(特定のオブジェクトツリー)のみを正確にテキスト化するため、AIに対して余計な情報を与えず、精度の高いコード生成を引き出すことが可能になります。

まとめ

生成AIのモデルが進化し、推論プロセスが複雑化するにつれて、人間側が「AIが処理しやすい形で正確なコンテキスト(前提条件)を渡す」ことの重要性が増しています。

「ContextExporter.cs」は、UnityのGUIデータとテキストベースのAIの間の強力な橋渡しとなります。AIを使った開発プロセスで行き詰まりを感じている場合は、ぜひこのようなアプローチによるコンテキスト管理を試してみてください。

以下コードと生成イメージ↓↓↓↓↓↓

using UnityEngine;
using UnityEditor;
using System.Text;

public class ContextExporter : EditorWindow
{
    [MenuItem("GameObject/Copy Context for AI", false, 10)]
    private static void CopyContextToClipboard()
    {
        // 選択されたオブジェクトのうち、最上位のものだけを取得(親子の重複選択を排除)
        Transform[] selectedTransforms = Selection.GetTransforms(SelectionMode.TopLevel);
        
        if (selectedTransforms.Length == 0)
        {
            Debug.LogWarning("対象のオブジェクトが選択されていません。");
            return;
        }

        StringBuilder sb = new StringBuilder();
        sb.AppendLine("【シーン階層およびコンテキスト情報】");

        // 選択された各ルートオブジェクトに対して再帰処理を実行
        foreach (Transform t in selectedTransforms)
        {
            ExportGameObjectRecursive(t, 0, sb);
        }

        GUIUtility.systemCopyBuffer = sb.ToString();
        Debug.Log($"選択された {selectedTransforms.Length} 個の階層ツリーをクリップボードにコピーしました。");
    }

    // 再帰的に子オブジェクトを走査するメソッド
    private static void ExportGameObjectRecursive(Transform t, int depth, StringBuilder sb)
    {
        // 階層の深さに応じてインデント(空白)を追加
        string indent = new string(' ', depth * 4); 
        GameObject obj = t.gameObject;

        // オブジェクトの基本情報
        sb.AppendLine($"{indent}■ [{obj.name}] (Tag: {obj.tag}, Layer: {LayerMask.LayerToName(obj.layer)}, Active: {obj.activeSelf})");

        // コンポーネント情報の取得
        Component[] components = obj.GetComponents<Component>();
        foreach (Component comp in components)
        {
            if (comp == null) continue;

            sb.AppendLine($"{indent}  - Component: {comp.GetType().Name}");
            
            SerializedObject so = new SerializedObject(comp);
            SerializedProperty prop = so.GetIterator();
            
            bool enterChildren = true;
            while (prop.NextVisible(enterChildren))
            {
                enterChildren = false; // ネストの深掘りを防ぎ、トップレベルのプロパティのみ取得
                if (prop.name == "m_Script") continue; // スクリプトの参照情報は除外

                string valueStr = GetPropertyValueAsString(prop);
                sb.AppendLine($"{indent}      {prop.name}: {valueStr}");
            }
        }
        sb.AppendLine(); // オブジェクト間の視認性を高めるための空行

        // 子オブジェクトに対して同じ処理を繰り返す
        for (int i = 0; i < t.childCount; i++)
        {
            ExportGameObjectRecursive(t.GetChild(i), depth + 1, sb);
        }
    }

    private static string GetPropertyValueAsString(SerializedProperty prop)
    {
        switch (prop.propertyType)
        {
            case SerializedPropertyType.Integer: return prop.intValue.ToString();
            case SerializedPropertyType.Boolean: return prop.boolValue.ToString();
            case SerializedPropertyType.Float: return prop.floatValue.ToString();
            case SerializedPropertyType.String: return prop.stringValue;
            case SerializedPropertyType.Color: return prop.colorValue.ToString();
            case SerializedPropertyType.ObjectReference: 
                return prop.objectReferenceValue != null ? prop.objectReferenceValue.name : "null";
            case SerializedPropertyType.Enum: 
                return prop.enumNames.Length > prop.enumValueIndex && prop.enumValueIndex >= 0 
                       ? prop.enumNames[prop.enumValueIndex] : prop.enumValueIndex.ToString();
            case SerializedPropertyType.Vector2: return prop.vector2Value.ToString();
            case SerializedPropertyType.Vector3: return prop.vector3Value.ToString();
            default: return $"[{prop.propertyType}]";
        }
    }
}Code language: HTML, XML (xml)

【シーン階層およびコンテキスト情報の出力イメージ】
■ [Player] (Tag: Player, Layer: Default, Active: True)

  • Component: Transform
    m_LocalPosition: (0.0, 1.0, 0.0)
    m_LocalRotation: (0.0, 0.0, 0.0, 1.0)
  • Component: Rigidbody m_Mass: 1 m_UseGravity: True ■ [CameraPivot] (Tag: Untagged, Layer: Default, Active: True)
    • Component: Transform
      m_LocalPosition: (0.0, 2.5, -4.0)

コメント

タイトルとURLをコピーしました