Skip to content

Instantly share code, notes, and snippets.

@supertask
Last active December 9, 2025 07:15
Show Gist options
  • Select an option

  • Save supertask/86dcfc865e96d125d8cece1fa73148e9 to your computer and use it in GitHub Desktop.

Select an option

Save supertask/86dcfc865e96d125d8cece1fa73148e9 to your computer and use it in GitHub Desktop.
Unity Hierarchy Web Server
// Assets/Editor/HierarchyWebServer.cs
#if UNITY_EDITOR
using System;
using System.Net;
using System.Text;
using System.Threading;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class HierarchyWebServer : ScriptableObject
{
// ====== 設定 ======
const string HOST = "http://localhost:8777/"; // ポート競合時は変更(末尾スラッシュ必須)
const int MAX_DEPTH = 64;
const bool DEBUG_MODE = false; // デバッグ時のみtrueに設定(頻繁なログ出力を制御)
static HttpListener _listener;
static Thread _thread;
static volatile bool _running;
// サーバスレッドから読むだけ(Unity APIは触らない)
static volatile string _latestText = "waiting…";
static volatile string _latestJson = "{\"status\":\"waiting\"}";
static volatile string _latestSceneViewBase64 = "";
static volatile string _latestGameViewBase64 = "";
// ウィンドウからアクセス可能にするためのプロパティ
public static string LatestText => _latestText;
public static bool IsRunning => _running;
public static void StartServer()
{
if (_running) { Debug.Log("[HierarchyWebServer] already running."); return; }
// 選択変更イベントを登録(エディタの描画中に呼ばれない安全な方法)
Selection.selectionChanged -= OnSelectionChanged;
Selection.selectionChanged += OnSelectionChanged;
// プレイモード変更時のクリーンアップを登録
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
try
{
// HOSTの検証
if (string.IsNullOrWhiteSpace(HOST))
{
Debug.LogWarning("[HierarchyWebServer] HOST is not configured. Web server will not start.");
Selection.selectionChanged -= OnSelectionChanged;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
return;
}
_listener = new HttpListener();
// HttpListenerのPrefixは末尾にスラッシュが必要
string prefix = HOST.EndsWith("/") ? HOST : HOST + "/";
_listener.Prefixes.Add(prefix);
_listener.Start();
_running = true;
_thread = new Thread(HandleLoop) { IsBackground = true, Name = "HierarchyWebServer" };
_thread.Start();
// 初回スナップショット(エディタの描画が完了した後に実行)
EditorApplication.delayCall += RebuildSnapshotFromSelection;
Debug.Log($"[HierarchyWebServer] listening on {prefix}");
}
catch (ArgumentException e)
{
// 無効なHOST設定の場合は静かに失敗(ブラウザで何も表示しない)
_running = false;
_listener = null;
Selection.selectionChanged -= OnSelectionChanged;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
Debug.LogWarning($"[HierarchyWebServer] Invalid HOST configuration: {HOST}. Web server will not start. ({e.Message})");
}
catch (Exception e)
{
_running = false;
_listener = null;
Selection.selectionChanged -= OnSelectionChanged;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
Debug.LogWarning($"[HierarchyWebServer] failed to start: {e.Message}. Web server will not start.");
}
}
public static void StopServer()
{
try
{
_running = false;
if (_listener != null)
{
_listener.Close();
_listener = null;
}
if (_thread != null)
{
_thread.Join(500);
_thread = null;
}
Selection.selectionChanged -= OnSelectionChanged;
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
EditorApplication.delayCall -= RebuildSnapshotFromSelection;
Debug.Log("[HierarchyWebServer] stopped.");
}
catch (Exception e)
{
Debug.LogError("[HierarchyWebServer] stop error: " + e);
}
}
// エディタ停止時に後片付け
static void OnPlayModeStateChanged(PlayModeStateChange s)
{
if (s == PlayModeStateChange.ExitingEditMode || s == PlayModeStateChange.ExitingPlayMode)
StopServer();
}
// --- 選択監視(メインスレッド / Unity API OK) ---
// Selection.selectionChangedイベントを使用することで、エディタの描画中に呼ばれることを防ぐ
static void OnSelectionChanged()
{
if (!_running) return;
// エディタの描画が完了した後に処理を実行(安全)
// 重複登録を防ぐために事前に削除
EditorApplication.delayCall -= RebuildSnapshotFromSelection;
EditorApplication.delayCall += RebuildSnapshotFromSelection;
}
static void RebuildSnapshotFromSelection()
{
try
{
// 複数選択に対応
var selectedObjects = Selection.gameObjects;
if (selectedObjects == null || selectedObjects.Length == 0)
{
_latestText = "No GameObject selected.";
_latestJson = "{\"status\":\"no_selection\"}";
_latestSceneViewBase64 = "";
_latestGameViewBase64 = "";
return;
}
// スクリーンショットを取得
CaptureScreenshots();
// 複数オブジェクトのメタ情報を収集
var objectsList = new List<(string name, string path, string metadata, string time, string playMode)>();
var allTextSb = new StringBuilder();
var validObjects = new List<GameObject>();
foreach (var go in selectedObjects)
{
if (go != null && go.transform != null)
{
validObjects.Add(go);
}
}
int index = 1;
foreach (var go in validObjects)
{
// メタ情報を取得(ヘッダーなし)
var metadataSb = new StringBuilder();
Traverse(go.transform, 0, metadataSb);
string metadata = metadataSb.ToString();
// テキスト出力用に追加
allTextSb.AppendLine($"o{index}:{go.name}");
allTextSb.AppendLine();
allTextSb.AppendLine($"Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
allTextSb.AppendLine($"PlayMode: {EditorApplication.isPlaying}");
allTextSb.AppendLine();
allTextSb.AppendLine(metadata);
// 最後のオブジェクト以外は区切りを追加
if (index < validObjects.Count)
{
allTextSb.AppendLine();
allTextSb.AppendLine("===");
allTextSb.AppendLine();
}
// JSON用にオブジェクト情報を追加(ヘッダーを含む完全なメタ情報)
var currentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var playMode = EditorApplication.isPlaying.ToString();
var fullMetadataSb = new StringBuilder();
fullMetadataSb.AppendLine($"=== Hierarchy Metadata (Unity {Application.unityVersion}) ===");
fullMetadataSb.AppendLine($"Root: {GetPath(go.transform)}");
fullMetadataSb.AppendLine($"Time: {currentTime}");
fullMetadataSb.AppendLine($"PlayMode: {playMode}");
fullMetadataSb.AppendLine();
fullMetadataSb.Append(metadata);
var jsonSafe = EscapeJson(fullMetadataSb.ToString());
objectsList.Add((go.name, GetPath(go.transform), jsonSafe, currentTime, playMode));
index++;
}
_latestText = allTextSb.ToString();
// JSONに画像データを含める
var sceneViewBase64 = string.IsNullOrEmpty(_latestSceneViewBase64) ? "" : _latestSceneViewBase64;
var gameViewBase64 = string.IsNullOrEmpty(_latestGameViewBase64) ? "" : _latestGameViewBase64;
// デバッグ用ログ
if (DEBUG_MODE)
{
Debug.Log($"[HierarchyWebServer] Selected objects: {objectsList.Count}, SceneView base64 length: {sceneViewBase64.Length}, GameView base64 length: {gameViewBase64.Length}");
}
// JSON配列を構築
var objectsJson = new StringBuilder();
objectsJson.Append("[");
for (int i = 0; i < objectsList.Count; i++)
{
if (i > 0) objectsJson.Append(",");
var obj = objectsList[i];
var name = EscapeJson(obj.name);
var path = EscapeJson(obj.path);
var metadata = obj.metadata;
var time = EscapeJson(obj.time);
var playMode = EscapeJson(obj.playMode);
objectsJson.Append($"{{\"name\":\"{name}\",\"path\":\"{path}\",\"metadata\":\"{metadata}\",\"time\":\"{time}\",\"playMode\":\"{playMode}\"}}");
}
objectsJson.Append("]");
_latestJson = $"{{\"ok\":true,\"updated\":\"{DateTime.Now:O}\",\"objects\":{objectsJson},\"sceneView\":\"{sceneViewBase64}\",\"gameView\":\"{gameViewBase64}\"}}";
}
catch (NullReferenceException)
{
// NullReferenceExceptionは無視(エディタの状態が不安定な時に発生する可能性がある)
_latestText = "Error: Editor state is unstable. Please try again.";
_latestJson = "{\"ok\":false,\"error\":\"Editor state unstable\"}";
_latestSceneViewBase64 = "";
_latestGameViewBase64 = "";
}
catch (Exception e)
{
_latestText = "Error while building snapshot:\n" + e;
_latestJson = $"{{\"ok\":false,\"error\":\"{EscapeJson(e.ToString())}\"}}";
_latestSceneViewBase64 = "";
_latestGameViewBase64 = "";
}
}
static string EscapeJson(string s)
=> s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
// スクリーンショット取得用の設定
const int SCREENSHOT_MAX_WIDTH = 4096; // 大きなGame View用に拡張
const int SCREENSHOT_MAX_HEIGHT = 2160; // 4K対応
const int JPEG_QUALITY = 85; // JPG品質(0-100、85が推奨)
// 縦横比を維持しながら最大サイズ内に収めるヘルパーメソッド
static void CalculateScreenshotSize(int originalWidth, int originalHeight,
int maxWidth, int maxHeight,
out int screenshotWidth, out int screenshotHeight)
{
if (originalWidth <= 0 || originalHeight <= 0)
{
screenshotWidth = maxWidth;
screenshotHeight = maxHeight;
return;
}
float aspectRatio = (float)originalWidth / originalHeight;
// まず幅で制限した場合のサイズを計算
int widthBasedWidth = Mathf.Min(originalWidth, maxWidth);
int widthBasedHeight = Mathf.RoundToInt(widthBasedWidth / aspectRatio);
// 次に高さで制限した場合のサイズを計算
int heightBasedHeight = Mathf.Min(originalHeight, maxHeight);
int heightBasedWidth = Mathf.RoundToInt(heightBasedHeight * aspectRatio);
// 両方の制限を満たす方を選択
if (widthBasedWidth <= maxWidth && widthBasedHeight <= maxHeight &&
heightBasedWidth <= maxWidth && heightBasedHeight <= maxHeight)
{
// 両方とも制限内なら、より大きなサイズを選択(画質優先)
if (widthBasedWidth * widthBasedHeight >= heightBasedWidth * heightBasedHeight)
{
screenshotWidth = widthBasedWidth;
screenshotHeight = widthBasedHeight;
}
else
{
screenshotWidth = heightBasedWidth;
screenshotHeight = heightBasedHeight;
}
}
else if (widthBasedWidth <= maxWidth && widthBasedHeight <= maxHeight)
{
// 幅ベースが制限内
screenshotWidth = widthBasedWidth;
screenshotHeight = widthBasedHeight;
}
else if (heightBasedWidth <= maxWidth && heightBasedHeight <= maxHeight)
{
// 高さベースが制限内
screenshotWidth = heightBasedWidth;
screenshotHeight = heightBasedHeight;
}
else
{
// どちらも制限を超える場合は、より厳しい制限に合わせる
float widthScale = (float)maxWidth / originalWidth;
float heightScale = (float)maxHeight / originalHeight;
float scale = Mathf.Min(widthScale, heightScale);
screenshotWidth = Mathf.RoundToInt(originalWidth * scale);
screenshotHeight = Mathf.RoundToInt(originalHeight * scale);
}
// 最小サイズを確保
screenshotWidth = Mathf.Max(screenshotWidth, 256);
screenshotHeight = Mathf.Max(screenshotHeight, 256);
}
static void CaptureScreenshots()
{
try
{
// Scene Viewのスクリーンショット
_latestSceneViewBase64 = CaptureSceneView();
// Game Viewのスクリーンショット(プレイモード中のみ)
if (EditorApplication.isPlaying)
{
_latestGameViewBase64 = CaptureGameView();
}
else
{
_latestGameViewBase64 = "";
}
}
catch (Exception e)
{
Debug.LogWarning($"[HierarchyWebServer] Failed to capture screenshots: {e.Message}");
_latestSceneViewBase64 = "";
_latestGameViewBase64 = "";
}
}
static string CaptureSceneView()
{
try
{
var sceneView = SceneView.lastActiveSceneView;
if (sceneView == null)
{
Debug.LogWarning("[HierarchyWebServer] SceneView.lastActiveSceneView is null");
return "";
}
// Scene Viewを強制的に再描画して最新の状態にする
sceneView.Repaint();
// Scene Viewのカメラを取得
var sceneCamera = sceneView.camera;
if (sceneCamera == null)
{
Debug.LogWarning("[HierarchyWebServer] SceneView camera is null");
return "";
}
// Scene Viewの実際のサイズを取得して縦横比を維持
var sceneViewRect = sceneView.position;
int sceneViewWidth = (int)sceneViewRect.width;
int sceneViewHeight = (int)sceneViewRect.height;
// 縦横比を維持しながら最大サイズ内に収める
float aspectRatio = sceneViewWidth > 0 && sceneViewHeight > 0
? (float)sceneViewWidth / sceneViewHeight
: 1.0f;
int screenshotWidth, screenshotHeight;
CalculateScreenshotSize(sceneViewWidth, sceneViewHeight,
SCREENSHOT_MAX_WIDTH, SCREENSHOT_MAX_HEIGHT,
out screenshotWidth, out screenshotHeight);
if (DEBUG_MODE)
{
Debug.Log($"[HierarchyWebServer] Scene View size: {sceneViewWidth}x{sceneViewHeight}, Screenshot size: {screenshotWidth}x{screenshotHeight}, Aspect: {aspectRatio:F2}");
}
// RenderTextureを作成(縦横比を維持)
var rt = RenderTexture.GetTemporary(screenshotWidth, screenshotHeight, 24);
// Scene Viewのカメラを直接使用してレンダリング
// これにより、Scene Viewの実際の描画内容を取得できる
var prevSceneCameraTarget = sceneCamera.targetTexture;
sceneCamera.targetTexture = rt;
sceneCamera.Render();
sceneCamera.targetTexture = prevSceneCameraTarget;
// Texture2Dに変換
RenderTexture.active = rt;
var tex = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGB24, false);
tex.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0);
tex.Apply();
// JPGにエンコードしてbase64に変換
byte[] jpgData = tex.EncodeToJPG(JPEG_QUALITY);
if (jpgData == null || jpgData.Length == 0)
{
Debug.LogWarning("[HierarchyWebServer] Failed to encode Scene View to JPG");
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
UnityEngine.Object.DestroyImmediate(tex);
return "";
}
string base64 = Convert.ToBase64String(jpgData);
if (DEBUG_MODE)
{
Debug.Log($"[HierarchyWebServer] Scene View captured successfully, JPG size: {jpgData.Length} bytes, base64 length: {base64.Length}");
}
// クリーンアップ
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
UnityEngine.Object.DestroyImmediate(tex);
return base64;
}
catch (Exception e)
{
Debug.LogWarning($"[HierarchyWebServer] Failed to capture Scene View: {e.Message}\n{e.StackTrace}");
return "";
}
}
static Bounds GetBounds(GameObject go)
{
var renderers = go.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0)
{
// Rendererがない場合は、Transformの位置を中心とした小さなバウンディングボックスを返す
return new Bounds(go.transform.position, Vector3.one);
}
var bounds = renderers[0].bounds;
foreach (var renderer in renderers)
{
if (renderer != null)
{
bounds.Encapsulate(renderer.bounds);
}
}
return bounds;
}
static string CaptureGameView()
{
try
{
var mainCamera = Camera.main;
if (mainCamera == null)
{
// メインカメラがない場合は、最初に見つかったカメラを使用
var cameras = UnityEngine.Object.FindObjectsOfType<Camera>();
if (cameras.Length == 0) return "";
mainCamera = cameras[0];
}
// Game Viewのサイズを取得(Handles.GetMainGameViewSize()を使用)
var gameViewSize = Handles.GetMainGameViewSize();
int gameViewWidth = (int)gameViewSize.x;
int gameViewHeight = (int)gameViewSize.y;
// Game Viewが取得できない場合は、Screenサイズを使用
if (gameViewWidth <= 0 || gameViewHeight <= 0)
{
gameViewWidth = Screen.width;
gameViewHeight = Screen.height;
}
// 縦横比を維持しながら最大サイズ内に収める
float aspectRatio = gameViewWidth > 0 && gameViewHeight > 0
? (float)gameViewWidth / gameViewHeight
: 16f / 9f; // デフォルトは16:9
int screenshotWidth, screenshotHeight;
CalculateScreenshotSize(gameViewWidth, gameViewHeight,
SCREENSHOT_MAX_WIDTH, SCREENSHOT_MAX_HEIGHT,
out screenshotWidth, out screenshotHeight);
if (DEBUG_MODE)
{
Debug.Log($"[HierarchyWebServer] Game View size: {gameViewWidth}x{gameViewHeight}, Screenshot size: {screenshotWidth}x{screenshotHeight}, Aspect: {aspectRatio:F2}");
}
// RenderTextureを作成
var rt = RenderTexture.GetTemporary(screenshotWidth, screenshotHeight, 24);
var prev = mainCamera.targetTexture;
mainCamera.targetTexture = rt;
mainCamera.Render();
mainCamera.targetTexture = prev;
// Texture2Dに変換
RenderTexture.active = rt;
var tex = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGB24, false);
tex.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0);
tex.Apply();
// JPGにエンコードしてbase64に変換
byte[] jpgData = tex.EncodeToJPG(JPEG_QUALITY);
if (jpgData == null || jpgData.Length == 0)
{
Debug.LogWarning("[HierarchyWebServer] Failed to encode Game View to JPG");
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
UnityEngine.Object.DestroyImmediate(tex);
return "";
}
string base64 = Convert.ToBase64String(jpgData);
if (DEBUG_MODE)
{
Debug.Log($"[HierarchyWebServer] Game View captured successfully, JPG size: {jpgData.Length} bytes, base64 length: {base64.Length}");
}
// クリーンアップ
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
UnityEngine.Object.DestroyImmediate(tex);
return base64;
}
catch (Exception e)
{
Debug.LogWarning($"[HierarchyWebServer] Failed to capture Game View: {e.Message}");
return "";
}
}
static void Traverse(Transform t, int depth, StringBuilder sb)
{
if (depth > MAX_DEPTH) return;
if (t == null) return;
try
{
string indent = new string(' ', depth * 2);
var go = t.gameObject;
if (go == null) return;
sb.AppendLine($"{indent}- GameObject: {go.name}");
sb.AppendLine($"{indent} Path: {GetPath(t)}");
sb.AppendLine($"{indent} ActiveInHierarchy: {go.activeInHierarchy}, ActiveSelf: {go.activeSelf}");
sb.AppendLine($"{indent} Tag: {go.tag}, Layer: {LayerMask.LayerToName(go.layer)} ({go.layer})");
sb.AppendLine($"{indent} HideFlags: {go.hideFlags}");
try
{
var prefabStatus = PrefabUtility.GetPrefabInstanceStatus(go);
if (prefabStatus != PrefabInstanceStatus.NotAPrefab)
{
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(go);
if (prefab != null)
{
var prefabPath = AssetDatabase.GetAssetPath(prefab);
var prefabGuid = string.IsNullOrEmpty(prefabPath) ? "" : AssetDatabase.AssetPathToGUID(prefabPath);
sb.AppendLine($"{indent} Prefab: {prefabStatus} Path: {prefabPath} GUID: {prefabGuid}");
}
}
}
catch
{
// PrefabUtility関連のエラーは無視
}
try
{
var comps = go.GetComponents<Component>();
foreach (var c in comps)
{
if (c == null) { sb.AppendLine($"{indent} * Missing Component"); continue; }
sb.AppendLine($"{indent} * Component: {c.GetType().FullName}");
if (c is MonoBehaviour mb)
{
try
{
var ms = MonoScript.FromMonoBehaviour(mb);
if (ms != null)
{
var scriptPath = AssetDatabase.GetAssetPath(ms);
var scriptGuid = string.IsNullOrEmpty(scriptPath) ? "" : AssetDatabase.AssetPathToGUID(scriptPath);
sb.AppendLine($"{indent} Script: {scriptPath} GUID: {scriptGuid}");
}
}
catch
{
// MonoScript関連のエラーは無視
}
}
// コンポーネントのシリアライズされたフィールドを取得
try
{
SerializedObject so = new SerializedObject(c);
SerializedProperty prop = so.GetIterator();
bool enterChildren = true;
while (prop.NextVisible(enterChildren))
{
enterChildren = false;
// m_Scriptプロパティはスキップ(既に表示済み)
if (prop.name == "m_Script") continue;
SerializeProperty(prop, indent + " ", sb, 0);
}
}
catch (Exception e)
{
sb.AppendLine($"{indent} [Error reading properties: {e.Message}]");
}
}
}
catch
{
// GetComponents関連のエラーは無視
}
for (int i = 0; i < t.childCount; i++)
{
var child = t.GetChild(i);
if (child != null)
Traverse(child, depth + 1, sb);
}
}
catch
{
// トラバース中のエラーは無視(エディタの状態が不安定な時に発生する可能性がある)
}
}
static string GetPath(Transform t)
{
var list = new List<string>();
while (t != null) { list.Add(t.name); t = t.parent; }
list.Reverse(); return string.Join("/", list);
}
static void SerializeProperty(SerializedProperty prop, string indent, StringBuilder sb, int depth)
{
const int MAX_PROPERTY_DEPTH = 8;
if (depth > MAX_PROPERTY_DEPTH) return;
try
{
string displayName = prop.displayName;
string propertyType = prop.propertyType.ToString();
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
sb.AppendLine($"{indent}{displayName}: {prop.intValue}");
break;
case SerializedPropertyType.Boolean:
sb.AppendLine($"{indent}{displayName}: {prop.boolValue}");
break;
case SerializedPropertyType.Float:
sb.AppendLine($"{indent}{displayName}: {prop.floatValue}");
break;
case SerializedPropertyType.String:
sb.AppendLine($"{indent}{displayName}: \"{prop.stringValue}\"");
break;
case SerializedPropertyType.Color:
sb.AppendLine($"{indent}{displayName}: {prop.colorValue}");
break;
case SerializedPropertyType.ObjectReference:
if (prop.objectReferenceValue != null)
{
var obj = prop.objectReferenceValue;
var assetPath = AssetDatabase.GetAssetPath(obj);
var guid = string.IsNullOrEmpty(assetPath) ? "" : AssetDatabase.AssetPathToGUID(assetPath);
sb.AppendLine($"{indent}{displayName}: {obj.name} ({obj.GetType().Name})");
if (!string.IsNullOrEmpty(assetPath))
{
sb.AppendLine($"{indent} Path: {assetPath} GUID: {guid}");
}
}
else
{
sb.AppendLine($"{indent}{displayName}: None");
}
break;
case SerializedPropertyType.LayerMask:
sb.AppendLine($"{indent}{displayName}: {prop.intValue}");
break;
case SerializedPropertyType.Enum:
sb.AppendLine($"{indent}{displayName}: {prop.enumNames[prop.enumValueIndex]}");
break;
case SerializedPropertyType.Vector2:
sb.AppendLine($"{indent}{displayName}: {prop.vector2Value}");
break;
case SerializedPropertyType.Vector3:
sb.AppendLine($"{indent}{displayName}: {prop.vector3Value}");
break;
case SerializedPropertyType.Vector4:
sb.AppendLine($"{indent}{displayName}: {prop.vector4Value}");
break;
case SerializedPropertyType.Rect:
sb.AppendLine($"{indent}{displayName}: {prop.rectValue}");
break;
case SerializedPropertyType.ArraySize:
sb.AppendLine($"{indent}{displayName}: {prop.intValue}");
break;
case SerializedPropertyType.Character:
sb.AppendLine($"{indent}{displayName}: '{(char)prop.intValue}'");
break;
case SerializedPropertyType.AnimationCurve:
sb.AppendLine($"{indent}{displayName}: AnimationCurve");
break;
case SerializedPropertyType.Bounds:
sb.AppendLine($"{indent}{displayName}: {prop.boundsValue}");
break;
case SerializedPropertyType.Quaternion:
sb.AppendLine($"{indent}{displayName}: {prop.quaternionValue}");
break;
case SerializedPropertyType.ExposedReference:
sb.AppendLine($"{indent}{displayName}: {prop.exposedReferenceValue}");
break;
case SerializedPropertyType.Vector2Int:
sb.AppendLine($"{indent}{displayName}: {prop.vector2IntValue}");
break;
case SerializedPropertyType.Vector3Int:
sb.AppendLine($"{indent}{displayName}: {prop.vector3IntValue}");
break;
case SerializedPropertyType.RectInt:
sb.AppendLine($"{indent}{displayName}: {prop.rectIntValue}");
break;
case SerializedPropertyType.BoundsInt:
sb.AppendLine($"{indent}{displayName}: {prop.boundsIntValue}");
break;
case SerializedPropertyType.Generic:
// 配列またはネストされたオブジェクト
if (prop.isArray && prop.propertyType != SerializedPropertyType.String)
{
int arraySize = prop.arraySize;
sb.AppendLine($"{indent}{displayName}: Array[{arraySize}]");
for (int i = 0; i < arraySize && i < 100; i++) // 最大100要素まで
{
var element = prop.GetArrayElementAtIndex(i);
SerializeProperty(element, indent + " ", sb, depth + 1);
}
if (arraySize > 100)
{
sb.AppendLine($"{indent} ... ({arraySize - 100} more elements)");
}
}
else
{
// ネストされたオブジェクト
sb.AppendLine($"{indent}{displayName}:");
var copy = prop.Copy();
var endProperty = copy.GetEndProperty();
copy.NextVisible(true);
int childCount = 0;
while (!SerializedProperty.EqualContents(copy, endProperty) && childCount < 50)
{
SerializeProperty(copy, indent + " ", sb, depth + 1);
childCount++;
if (!copy.NextVisible(false))
break;
}
}
break;
default:
sb.AppendLine($"{indent}{displayName}: ({propertyType})");
break;
}
}
catch (Exception e)
{
sb.AppendLine($"{indent}[Error: {e.Message}]");
}
}
// --- サーバ本体(別スレッド / Unity APIは使わない) ---
static void HandleLoop()
{
while (_running && _listener != null)
{
HttpListenerContext ctx = null;
try { ctx = _listener.GetContext(); }
catch { if (!_running) break; else continue; }
try
{
var req = ctx.Request;
var res = ctx.Response;
if (req.Url.AbsolutePath == "/" || req.Url.AbsolutePath == "/index.html")
{
var html = GetIndexHtml();
WriteUtf8(res, "text/html; charset=utf-8", html);
}
else if (req.Url.AbsolutePath == "/text")
{
WriteUtf8(res, "text/plain; charset=utf-8", _latestText);
}
else if (req.Url.AbsolutePath == "/json")
{
WriteUtf8(res, "application/json; charset=utf-8", _latestJson);
}
else if (req.Url.AbsolutePath == "/health")
{
WriteUtf8(res, "application/json; charset=utf-8", "{\"ok\":true}");
}
else
{
res.StatusCode = 404;
WriteUtf8(res, "text/plain; charset=utf-8", "404");
}
}
catch (Exception e)
{
try
{
ctx.Response.StatusCode = 500;
WriteUtf8(ctx.Response, "text/plain; charset=utf-8", "500\n" + e);
}
catch { /* ignore */ }
}
}
}
static string GetIndexHtml() => @"<!doctype html>
<html lang=""ja""><head>
<meta charset=""utf-8"">
<meta name=""viewport"" content=""width=device-width,initial-scale=1"">
<title>Unity Hierarchy Metadata</title>
<style>
body { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; margin: 0; background:#0f1214; color:#e5e7eb;}
header { padding:12px 16px; background:#111827; position:sticky; top:0; display:flex; gap:12px; align-items:center; z-index:100;}
#time { opacity:.7;}
.images { display:flex; flex-direction:column; gap:16px; padding:16px; align-items:flex-start; }
.image-container { width:100%; }
.image-container h3 { margin:0 0 8px 0; font-size:14px; color:#9ca3af; }
.image-container img { max-width:100%; height:auto; border:1px solid #374151; border-radius:6px; background:#1f2937; }
.image-container .no-image { padding:40px; text-align:center; color:#6b7280; border:1px solid #374151; border-radius:6px; background:#1f2937; }
.metadata-wrapper { width:100%; padding:16px; margin:0 16px 16px 16px; border:2px solid #374151; border-radius:8px; background:#111827; }
.copy-section { width:100%; margin-bottom:16px; }
.copy-section h3 { margin:0 0 12px 0; font-size:14px; color:#9ca3af; }
.copy-button { width:100%; padding:12px 16px; background:#2563eb; color:#ffffff; border:none; border-radius:8px; cursor:pointer; font-size:14px; font-weight:600; transition:all 0.2s; box-shadow:0 2px 4px rgba(0,0,0,0.2); }
.copy-button:hover { background:#1d4ed8; transform:translateY(-1px); box-shadow:0 4px 8px rgba(0,0,0,0.3); }
.copy-button:active { transform:translateY(0); box-shadow:0 1px 2px rgba(0,0,0,0.2); background:#1e40af; }
.copy-button:disabled { background:#374151; cursor:not-allowed; opacity:0.6; }
.copy-success { background:#10b981 !important; }
.objects-container { padding:0; }
.object-item { margin-bottom:24px; }
.object-item h2 { margin:0 0 8px 0; font-size:16px; color:#e5e7eb; padding:8px 12px; background:#1f2937; border:1px solid #374151; border-radius:6px; }
.object-item textarea { width:100%; min-height:200px; padding:12px; background:#1f2937; color:#e5e7eb; border:1px solid #374151; border-radius:6px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:12px; white-space:pre; overflow:auto; resize:vertical; }
</style>
</head><body>
<header>
<strong>Hierarchy Metadata</strong>
<span id=""time""></span>
</header>
<div class=""images"">
<div class=""image-container"">
<h3>Scene View</h3>
<div id=""sceneView""><div class=""no-image"">loading…</div></div>
</div>
<div class=""image-container"">
<h3>Game View</h3>
<div id=""gameView""><div class=""no-image"">loading…</div></div>
</div>
</div>
<div class=""metadata-wrapper"">
<div class=""copy-section"">
<h3>Copy All Metadata</h3>
<button id=""copy"" class=""copy-button"">すべてのメタ情報をクリップボードにコピー</button>
</div>
<div class=""objects-container"" id=""objectsContainer"">loading…</div>
</div>
<script>
let timer=null;
let lastDataHash = null;
let lastUpdated = null;
const DEBUG_MODE = false; // デバッグ時のみtrueに設定
// textareaがフォーカスされているか、選択中かをチェック
function isTextareaActive(){
const activeElement = document.activeElement;
if(activeElement && activeElement.tagName === 'TEXTAREA'){
return true;
}
// 選択範囲があるかチェック
const selection = window.getSelection();
if(selection && selection.rangeCount > 0){
const range = selection.getRangeAt(0);
const container = range.commonAncestorContainer;
// textarea内の選択かチェック
let node = container.nodeType === Node.TEXT_NODE ? container.parentNode : container;
while(node){
if(node.tagName === 'TEXTAREA'){
return true;
}
node = node.parentNode;
}
}
return false;
}
// データのハッシュを計算(簡易版)
function calculateDataHash(data){
if(!data || !data.ok) return null;
const objects = data.objects || [];
let hash = objects.length + '|';
for(let i = 0; i < objects.length; i++){
const obj = objects[i];
hash += (obj.name || '') + '|' + (obj.metadata ? obj.metadata.length : 0) + '|';
}
hash += (data.updated || '');
return hash;
}
async function tick(){
try{
const r = await fetch('/json?ts='+Date.now(), {cache:'no-store'});
if(!r.ok){
throw new Error('HTTP error: '+r.status);
}
const data = await r.json();
if(DEBUG_MODE){
console.log('JSON data received:', {
ok: data.ok,
objectsCount: data.objects ? data.objects.length : 0,
sceneViewLength: data.sceneView ? data.sceneView.length : 0,
gameViewLength: data.gameView ? data.gameView.length : 0
});
}
// データが変更されたかチェック
const currentDataHash = calculateDataHash(data);
const dataChanged = currentDataHash !== lastDataHash;
// textareaがアクティブな場合は、データが変更されていても更新をスキップ
const textareaActive = isTextareaActive();
if(data.ok){
// Scene View画像を更新(常に更新)
const sceneViewDiv = document.getElementById('sceneView');
if(data.sceneView && data.sceneView.length > 0){
const img = document.createElement('img');
img.src = 'data:image/jpeg;base64,'+data.sceneView;
img.alt = 'Scene View';
img.onerror = function(e){
console.error('Failed to load Scene View image:', e);
sceneViewDiv.innerHTML = '<div class=""no-image"">Failed to load Scene View image</div>';
};
img.onload = function(){
if(DEBUG_MODE){
console.log('Scene View image loaded successfully');
}
};
sceneViewDiv.innerHTML = '';
sceneViewDiv.appendChild(img);
}else{
sceneViewDiv.innerHTML = '<div class=""no-image"">No Scene View available</div>';
}
// Game View画像を更新(常に更新)
const gameViewDiv = document.getElementById('gameView');
if(data.gameView && data.gameView.length > 0){
const img = document.createElement('img');
img.src = 'data:image/jpeg;base64,'+data.gameView;
img.alt = 'Game View';
img.onerror = function(e){
console.error('Failed to load Game View image:', e);
gameViewDiv.innerHTML = '<div class=""no-image"">Failed to load Game View image</div>';
};
img.onload = function(){
if(DEBUG_MODE){
console.log('Game View image loaded successfully');
}
};
gameViewDiv.innerHTML = '';
gameViewDiv.appendChild(img);
}else{
gameViewDiv.innerHTML = '<div class=""no-image"">No Game View (not in Play Mode)</div>';
}
// オブジェクトリストを更新(データが変更された場合のみ、かつtextareaがアクティブでない場合)
const objectsContainer = document.getElementById('objectsContainer');
if(dataChanged && !textareaActive){
if(data.objects && data.objects.length > 0){
let html = '';
for(let i = 0; i < data.objects.length; i++){
const obj = data.objects[i];
const name = obj.name || 'Unknown';
const metadata = obj.metadata || '';
html += '<div class=""object-item"">';
html += '<h2>o' + (i + 1) + ': ' + escapeHtml(name) + '</h2>';
html += '<textarea readonly>' + escapeHtml(metadata) + '</textarea>';
html += '</div>';
}
objectsContainer.innerHTML = html;
}else{
objectsContainer.innerHTML = '<div class=""no-image"">No objects selected</div>';
}
lastDataHash = currentDataHash;
}else if(!dataChanged && !textareaActive){
// 初回ロード時は必ず更新
if(lastDataHash === null){
if(data.objects && data.objects.length > 0){
let html = '';
for(let i = 0; i < data.objects.length; i++){
const obj = data.objects[i];
const name = obj.name || 'Unknown';
const metadata = obj.metadata || '';
html += '<div class=""object-item"">';
html += '<h2>o' + (i + 1) + ': ' + escapeHtml(name) + '</h2>';
html += '<textarea readonly>' + escapeHtml(metadata) + '</textarea>';
html += '</div>';
}
objectsContainer.innerHTML = html;
}else{
objectsContainer.innerHTML = '<div class=""no-image"">No objects selected</div>';
}
lastDataHash = currentDataHash;
}
}
}else{
// エラー時は常に更新
document.getElementById('objectsContainer').innerHTML = '<div class=""no-image"">Error: '+(data.error || 'Unknown error')+'</div>';
lastDataHash = null;
}
// 最終更新日時を表示(常に更新)
if(data.ok && data.updated){
try {
const updatedDate = new Date(data.updated);
const formattedDate = updatedDate.toLocaleString('ja-JP', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
document.getElementById('time').textContent = formattedDate;
lastUpdated = data.updated;
} catch(e) {
document.getElementById('time').textContent = data.updated || '-';
lastUpdated = data.updated;
}
} else {
document.getElementById('time').textContent = '-';
lastUpdated = null;
}
}catch(e){
console.error('Fetch error:', e);
document.getElementById('objectsContainer').innerHTML = '<div class=""no-image"">fetch error: '+escapeHtml(e.toString())+'</div>';
lastDataHash = null;
}
}
function escapeHtml(text){
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
timer = setInterval(tick, 1000);
tick();
document.getElementById('copy').onclick = async () => {
const copyButton = document.getElementById('copy');
const objectsContainer = document.getElementById('objectsContainer');
const objectItems = objectsContainer.querySelectorAll('.object-item');
if(objectItems.length === 0){
copyButton.textContent = 'コピーするオブジェクトがありません';
copyButton.disabled = true;
setTimeout(() => {
copyButton.textContent = 'すべてのメタ情報をクリップボードにコピー';
copyButton.disabled = false;
}, 2000);
return;
}
let allText = '';
for(let i = 0; i < objectItems.length; i++){
const item = objectItems[i];
const h2 = item.querySelector('h2');
const textarea = item.querySelector('textarea');
if(h2 && textarea){
const objName = h2.textContent;
const metadata = textarea.value;
// メタ情報からTimeとPlayModeを抽出(metadataにはヘッダーが含まれている)
const timeMatch = metadata.match(/Time: ([^\\n]+)/);
const playModeMatch = metadata.match(/PlayMode: ([^\\n]+)/);
const time = timeMatch ? timeMatch[1] : '';
const playMode = playModeMatch ? playModeMatch[1] : '';
// メタ情報からヘッダー部分を削除(=== Hierarchy Metadata...から始まる部分とRoot:、Time:、PlayMode:の行を削除)
let cleanMetadata = metadata.replace(/=== Hierarchy Metadata[^\\n]*\\n/g, '');
cleanMetadata = cleanMetadata.replace(/Root: [^\\n]*\\n/g, '');
cleanMetadata = cleanMetadata.replace(/Time: [^\\n]*\\n/g, '');
cleanMetadata = cleanMetadata.replace(/PlayMode: [^\\n]*\\n/g, '');
cleanMetadata = cleanMetadata.replace(/^\\n+/g, ''); // 先頭の空行を削除
allText += 'o' + (i + 1) + ':' + objName + '\\n';
allText += '\\n';
allText += 'Time: ' + time + '\\n';
allText += 'PlayMode: ' + playMode + '\\n';
allText += '\\n';
allText += cleanMetadata;
if(i < objectItems.length - 1){
allText += '\\n\\n';
allText += '===\\n';
allText += '\\n';
}
}
}
try {
copyButton.disabled = true;
copyButton.textContent = 'コピー中...';
await navigator.clipboard.writeText(allText);
copyButton.classList.add('copy-success');
copyButton.textContent = '✓ コピー完了!';
setTimeout(() => {
copyButton.classList.remove('copy-success');
copyButton.textContent = 'すべてのメタ情報をクリップボードにコピー';
copyButton.disabled = false;
}, 2000);
} catch(e){
copyButton.textContent = 'コピーに失敗しました';
copyButton.disabled = true;
setTimeout(() => {
copyButton.textContent = 'すべてのメタ情報をクリップボードにコピー';
copyButton.disabled = false;
}, 2000);
}
};
</script>
</body></html>";
static void WriteUtf8(HttpListenerResponse res, string contentType, string body)
{
var bytes = Encoding.UTF8.GetBytes(body);
res.Headers["Access-Control-Allow-Origin"] = "*"; // 必要に応じて絞る
res.ContentType = contentType;
res.ContentLength64 = bytes.Length;
using (var s = res.OutputStream)
s.Write(bytes, 0, bytes.Length);
}
}
// --- エディタウィンドウ ---
public class HierarchyMetadataWindow : EditorWindow
{
private Vector2 _scrollPosition;
private string _displayText = "No GameObject selected.";
private bool _autoRefresh = true;
[MenuItem("Window/Hierarchy Metadata Viewer")]
public static void ShowWindow()
{
var window = GetWindow<HierarchyMetadataWindow>("Hierarchy Metadata");
window.minSize = new Vector2(400, 300);
window.Show();
}
void OnEnable()
{
// 選択変更イベントを登録
Selection.selectionChanged += OnSelectionChanged;
// 初回更新(エディタの描画が完了した後に実行)
EditorApplication.delayCall += RefreshData;
}
void OnDisable()
{
Selection.selectionChanged -= OnSelectionChanged;
EditorApplication.delayCall -= RefreshData;
}
void OnSelectionChanged()
{
if (_autoRefresh)
{
// エディタの描画が完了した後に処理を実行(安全)
EditorApplication.delayCall -= RefreshData;
EditorApplication.delayCall += RefreshData;
}
}
void RefreshData()
{
// 複数選択に対応
var selectedObjects = Selection.gameObjects;
if (selectedObjects == null || selectedObjects.Length == 0)
{
_displayText = "No GameObject selected.";
}
else
{
try
{
var sb = new StringBuilder();
var validObjects = new List<GameObject>();
foreach (var go in selectedObjects)
{
if (go != null && go.transform != null)
{
validObjects.Add(go);
}
}
int index = 1;
foreach (var go in validObjects)
{
// オブジェクト番号と名前
sb.AppendLine($"o{index}:{go.name}");
sb.AppendLine();
// TimeとPlayMode
sb.AppendLine($"Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"PlayMode: {EditorApplication.isPlaying}");
sb.AppendLine();
// メタ情報
TraverseForWindow(go.transform, 0, sb);
// 最後のオブジェクト以外は区切りを追加
if (index < validObjects.Count)
{
sb.AppendLine();
sb.AppendLine("===");
sb.AppendLine();
}
index++;
}
_displayText = sb.ToString();
}
catch (Exception e)
{
_displayText = $"Error: {e.Message}";
}
}
Repaint();
}
void OnGUI()
{
// ツールバー
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
_autoRefresh = GUILayout.Toggle(_autoRefresh, "Auto Refresh", EditorStyles.toolbarButton, GUILayout.Width(100));
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
RefreshData();
}
GUILayout.FlexibleSpace();
// Web Server Start/Stopボタン
if (HierarchyWebServer.IsRunning)
{
GUI.color = Color.green;
GUILayout.Label("Web Server: Running", EditorStyles.toolbarButton, GUILayout.Width(120));
GUI.color = Color.white;
if (GUILayout.Button("Stop", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
HierarchyWebServer.StopServer();
RefreshData();
}
}
else
{
GUI.color = Color.gray;
GUILayout.Label("Web Server: Stopped", EditorStyles.toolbarButton, GUILayout.Width(120));
GUI.color = Color.white;
if (GUILayout.Button("Start", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
HierarchyWebServer.StartServer();
RefreshData();
}
}
EditorGUILayout.EndHorizontal();
// コピーボタン
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Copy to Clipboard", GUILayout.Height(25)))
{
EditorGUIUtility.systemCopyBuffer = _displayText;
Debug.Log("[HierarchyMetadataWindow] Copied to clipboard.");
}
EditorGUILayout.EndHorizontal();
// スクロール可能なテキスト表示
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
EditorGUILayout.TextArea(_displayText, EditorStyles.textArea, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
static string GetPath(Transform t)
{
var list = new List<string>();
while (t != null) { list.Add(t.name); t = t.parent; }
list.Reverse();
return string.Join("/", list);
}
static void TraverseForWindow(Transform t, int depth, StringBuilder sb)
{
if (depth > 64) return;
if (t == null) return;
try
{
string indent = new string(' ', depth * 2);
var go = t.gameObject;
if (go == null) return;
sb.AppendLine($"{indent}- GameObject: {go.name}");
sb.AppendLine($"{indent} Path: {GetPath(t)}");
sb.AppendLine($"{indent} ActiveInHierarchy: {go.activeInHierarchy}, ActiveSelf: {go.activeSelf}");
sb.AppendLine($"{indent} Tag: {go.tag}, Layer: {LayerMask.LayerToName(go.layer)} ({go.layer})");
sb.AppendLine($"{indent} HideFlags: {go.hideFlags}");
try
{
var prefabStatus = PrefabUtility.GetPrefabInstanceStatus(go);
if (prefabStatus != PrefabInstanceStatus.NotAPrefab)
{
var prefab = PrefabUtility.GetCorrespondingObjectFromSource(go);
if (prefab != null)
{
var prefabPath = AssetDatabase.GetAssetPath(prefab);
var prefabGuid = string.IsNullOrEmpty(prefabPath) ? "" : AssetDatabase.AssetPathToGUID(prefabPath);
sb.AppendLine($"{indent} Prefab: {prefabStatus} Path: {prefabPath} GUID: {prefabGuid}");
}
}
}
catch { }
try
{
var comps = go.GetComponents<Component>();
foreach (var c in comps)
{
if (c == null) { sb.AppendLine($"{indent} * Missing Component"); continue; }
sb.AppendLine($"{indent} * Component: {c.GetType().FullName}");
if (c is MonoBehaviour mb)
{
try
{
var ms = MonoScript.FromMonoBehaviour(mb);
if (ms != null)
{
var scriptPath = AssetDatabase.GetAssetPath(ms);
var scriptGuid = string.IsNullOrEmpty(scriptPath) ? "" : AssetDatabase.AssetPathToGUID(scriptPath);
sb.AppendLine($"{indent} Script: {scriptPath} GUID: {scriptGuid}");
}
}
catch { }
}
// コンポーネントのシリアライズされたフィールドを取得
try
{
SerializedObject so = new SerializedObject(c);
SerializedProperty prop = so.GetIterator();
bool enterChildren = true;
while (prop.NextVisible(enterChildren))
{
enterChildren = false;
// m_Scriptプロパティはスキップ(既に表示済み)
if (prop.name == "m_Script") continue;
SerializeProperty(prop, indent + " ", sb, 0);
}
}
catch (Exception e)
{
sb.AppendLine($"{indent} [Error reading properties: {e.Message}]");
}
}
}
catch { }
for (int i = 0; i < t.childCount; i++)
{
var child = t.GetChild(i);
if (child != null)
TraverseForWindow(child, depth + 1, sb);
}
}
catch { }
}
static void SerializeProperty(SerializedProperty prop, string indent, StringBuilder sb, int depth)
{
const int MAX_PROPERTY_DEPTH = 8;
if (depth > MAX_PROPERTY_DEPTH) return;
try
{
string displayName = prop.displayName;
string propertyType = prop.propertyType.ToString();
switch (prop.propertyType)
{
case SerializedPropertyType.Integer:
sb.AppendLine($"{indent}{displayName}: {prop.intValue}");
break;
case SerializedPropertyType.Boolean:
sb.AppendLine($"{indent}{displayName}: {prop.boolValue}");
break;
case SerializedPropertyType.Float:
sb.AppendLine($"{indent}{displayName}: {prop.floatValue}");
break;
case SerializedPropertyType.String:
sb.AppendLine($"{indent}{displayName}: \"{prop.stringValue}\"");
break;
case SerializedPropertyType.Color:
sb.AppendLine($"{indent}{displayName}: {prop.colorValue}");
break;
case SerializedPropertyType.ObjectReference:
if (prop.objectReferenceValue != null)
{
var obj = prop.objectReferenceValue;
var assetPath = AssetDatabase.GetAssetPath(obj);
var guid = string.IsNullOrEmpty(assetPath) ? "" : AssetDatabase.AssetPathToGUID(assetPath);
sb.AppendLine($"{indent}{displayName}: {obj.name} ({obj.GetType().Name})");
if (!string.IsNullOrEmpty(assetPath))
{
sb.AppendLine($"{indent} Path: {assetPath} GUID: {guid}");
}
}
else
{
sb.AppendLine($"{indent}{displayName}: None");
}
break;
case SerializedPropertyType.LayerMask:
sb.AppendLine($"{indent}{displayName}: {prop.intValue}");
break;
case SerializedPropertyType.Enum:
sb.AppendLine($"{indent}{displayName}: {prop.enumNames[prop.enumValueIndex]}");
break;
case SerializedPropertyType.Vector2:
sb.AppendLine($"{indent}{displayName}: {prop.vector2Value}");
break;
case SerializedPropertyType.Vector3:
sb.AppendLine($"{indent}{displayName}: {prop.vector3Value}");
break;
case SerializedPropertyType.Vector4:
sb.AppendLine($"{indent}{displayName}: {prop.vector4Value}");
break;
case SerializedPropertyType.Rect:
sb.AppendLine($"{indent}{displayName}: {prop.rectValue}");
break;
case SerializedPropertyType.ArraySize:
sb.AppendLine($"{indent}{displayName}: {prop.intValue}");
break;
case SerializedPropertyType.Character:
sb.AppendLine($"{indent}{displayName}: '{(char)prop.intValue}'");
break;
case SerializedPropertyType.AnimationCurve:
sb.AppendLine($"{indent}{displayName}: AnimationCurve");
break;
case SerializedPropertyType.Bounds:
sb.AppendLine($"{indent}{displayName}: {prop.boundsValue}");
break;
case SerializedPropertyType.Quaternion:
sb.AppendLine($"{indent}{displayName}: {prop.quaternionValue}");
break;
case SerializedPropertyType.ExposedReference:
sb.AppendLine($"{indent}{displayName}: {prop.exposedReferenceValue}");
break;
case SerializedPropertyType.Vector2Int:
sb.AppendLine($"{indent}{displayName}: {prop.vector2IntValue}");
break;
case SerializedPropertyType.Vector3Int:
sb.AppendLine($"{indent}{displayName}: {prop.vector3IntValue}");
break;
case SerializedPropertyType.RectInt:
sb.AppendLine($"{indent}{displayName}: {prop.rectIntValue}");
break;
case SerializedPropertyType.BoundsInt:
sb.AppendLine($"{indent}{displayName}: {prop.boundsIntValue}");
break;
case SerializedPropertyType.Generic:
// 配列またはネストされたオブジェクト
if (prop.isArray && prop.propertyType != SerializedPropertyType.String)
{
int arraySize = prop.arraySize;
sb.AppendLine($"{indent}{displayName}: Array[{arraySize}]");
for (int i = 0; i < arraySize && i < 100; i++) // 最大100要素まで
{
var element = prop.GetArrayElementAtIndex(i);
SerializeProperty(element, indent + " ", sb, depth + 1);
}
if (arraySize > 100)
{
sb.AppendLine($"{indent} ... ({arraySize - 100} more elements)");
}
}
else
{
// ネストされたオブジェクト
sb.AppendLine($"{indent}{displayName}:");
var copy = prop.Copy();
var endProperty = copy.GetEndProperty();
copy.NextVisible(true);
int childCount = 0;
while (!SerializedProperty.EqualContents(copy, endProperty) && childCount < 50)
{
SerializeProperty(copy, indent + " ", sb, depth + 1);
childCount++;
if (!copy.NextVisible(false))
break;
}
}
break;
default:
sb.AppendLine($"{indent}{displayName}: ({propertyType})");
break;
}
}
catch (Exception e)
{
sb.AppendLine($"{indent}[Error: {e.Message}]");
}
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment