Initial commit
This commit is contained in:
111
skills/assets/unity-package/Runtime/GameObjectGuid.cs
Normal file
111
skills/assets/unity-package/Runtime/GameObjectGuid.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace UnityEditorToolkit.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// GameObject에 영구적인 GUID를 부여하는 컴포넌트
|
||||
/// instance_id는 세션마다 변경되므로 GUID를 사용하여 영구 식별
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[ExecuteAlways]
|
||||
public class GameObjectGuid : MonoBehaviour
|
||||
{
|
||||
[SerializeField, HideInInspector]
|
||||
private string guid = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// GameObject의 고유 GUID
|
||||
/// </summary>
|
||||
public string Guid
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
GenerateGuid();
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// GUID가 없으면 생성
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
GenerateGuid();
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateGuid()
|
||||
{
|
||||
guid = System.Guid.NewGuid().ToString();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// EditorOnly: 변경사항을 씬에 저장
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
EditorUtility.SetDirty(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
Debug.Log($"[GameObjectGuid] Generated GUID for '{gameObject.name}': {guid}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GUID 재생성 (에디터 전용)
|
||||
/// </summary>
|
||||
[ContextMenu("Regenerate GUID")]
|
||||
public void RegenerateGuid()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"Regenerate GUID",
|
||||
$"Are you sure you want to regenerate GUID for '{gameObject.name}'?\n\nThis will break database references!",
|
||||
"Yes", "Cancel"))
|
||||
{
|
||||
guid = string.Empty;
|
||||
GenerateGuid();
|
||||
Debug.LogWarning($"[GameObjectGuid] GUID regenerated for '{gameObject.name}'");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Inspector에서 GUID 표시 (읽기 전용)
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(GameObjectGuid))]
|
||||
public class GameObjectGuidEditor : Editor
|
||||
{
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
var guidComp = (GameObjectGuid)target;
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"This component assigns a persistent GUID to the GameObject.\n" +
|
||||
"The GUID is used for database synchronization and remains constant across Unity sessions.",
|
||||
MessageType.Info);
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
GUI.enabled = false;
|
||||
EditorGUILayout.TextField("GUID", guidComp.Guid);
|
||||
GUI.enabled = true;
|
||||
|
||||
EditorGUILayout.Space();
|
||||
|
||||
if (GUILayout.Button("Regenerate GUID (⚠️ WARNING)"))
|
||||
{
|
||||
guidComp.RegenerateGuid();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22dd8e320f3862f488490d73a4f1263e
|
||||
8
skills/assets/unity-package/Runtime/Handlers.meta
Normal file
8
skills/assets/unity-package/Runtime/Handlers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbd0d9514ed911a4ea200e084ce4317c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
209
skills/assets/unity-package/Runtime/Handlers/BaseHandler.cs
Normal file
209
skills/assets/unity-package/Runtime/Handlers/BaseHandler.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditorToolkit.Protocol;
|
||||
|
||||
namespace UnityEditorToolkit.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Base handler for JSON-RPC commands
|
||||
/// </summary>
|
||||
public abstract class BaseHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// GameObject 캐시 (WeakReference 사용하여 메모리 누수 방지)
|
||||
/// </summary>
|
||||
private static Dictionary<string, System.WeakReference> gameObjectCache = new Dictionary<string, System.WeakReference>();
|
||||
private static readonly object cacheLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Handler category (e.g., "GameObject", "Transform")
|
||||
/// </summary>
|
||||
public abstract string Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Handle JSON-RPC request
|
||||
/// </summary>
|
||||
/// <param name="request">JSON-RPC request</param>
|
||||
/// <returns>Response object or null for error</returns>
|
||||
public object Handle(JsonRpcRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate request
|
||||
if (request == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request), "Request cannot be null");
|
||||
}
|
||||
|
||||
// Validate method name
|
||||
string fullMethod = request.Method;
|
||||
if (string.IsNullOrWhiteSpace(fullMethod))
|
||||
{
|
||||
throw new ArgumentException("Method name cannot be null or empty", nameof(request.Method));
|
||||
}
|
||||
|
||||
// Validate method belongs to this handler category
|
||||
if (!fullMethod.StartsWith(Category + "."))
|
||||
{
|
||||
throw new ArgumentException($"Invalid method for {Category} handler: {fullMethod}");
|
||||
}
|
||||
|
||||
string methodName = fullMethod.Substring(Category.Length + 1);
|
||||
|
||||
// Validate extracted method name
|
||||
if (string.IsNullOrWhiteSpace(methodName))
|
||||
{
|
||||
throw new ArgumentException($"Method name is empty after removing category prefix: {fullMethod}");
|
||||
}
|
||||
|
||||
// Route to specific handler method
|
||||
return HandleMethod(methodName, request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogError($"[{Category}] Handler error: {ex.Message}\n{ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle specific method (must be implemented by subclass)
|
||||
/// </summary>
|
||||
protected abstract object HandleMethod(string method, JsonRpcRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Validate required parameter
|
||||
/// </summary>
|
||||
protected T ValidateParam<T>(JsonRpcRequest request, string paramName) where T : class
|
||||
{
|
||||
var paramsObj = request.GetParams<T>();
|
||||
if (paramsObj == null)
|
||||
{
|
||||
throw new Exception($"Missing or invalid parameter: {paramName}");
|
||||
}
|
||||
return paramsObj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find GameObject by name or path (캐싱 적용)
|
||||
/// </summary>
|
||||
public UnityEngine.GameObject FindGameObject(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 캐시 확인
|
||||
lock (cacheLock)
|
||||
{
|
||||
if (gameObjectCache.TryGetValue(name, out var weakRef) && weakRef.IsAlive)
|
||||
{
|
||||
var cachedObj = weakRef.Target as UnityEngine.GameObject;
|
||||
if (cachedObj != null && cachedObj.scene.IsValid())
|
||||
{
|
||||
return cachedObj;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 캐시 무효화 (객체가 파괴됨)
|
||||
gameObjectCache.Remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try direct find first (빠른 검색)
|
||||
var obj = UnityEngine.GameObject.Find(name);
|
||||
if (obj != null)
|
||||
{
|
||||
CacheGameObject(name, obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Try finding in all objects (including inactive) - 비용이 큼
|
||||
var allObjects = UnityEngine.Resources.FindObjectsOfTypeAll<UnityEngine.GameObject>();
|
||||
foreach (var go in allObjects)
|
||||
{
|
||||
if (go.name == name || GetGameObjectPath(go) == name)
|
||||
{
|
||||
// Make sure it's a scene object, not asset
|
||||
if (go.scene.IsValid())
|
||||
{
|
||||
CacheGameObject(name, go);
|
||||
return go;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GameObject를 캐시에 추가
|
||||
/// </summary>
|
||||
private void CacheGameObject(string name, UnityEngine.GameObject obj)
|
||||
{
|
||||
lock (cacheLock)
|
||||
{
|
||||
gameObjectCache[name] = new System.WeakReference(obj);
|
||||
|
||||
// 캐시 크기 제한 (최대 100개)
|
||||
if (gameObjectCache.Count > 100)
|
||||
{
|
||||
// 만료된(파괴된) 캐시 항목 제거
|
||||
var toRemove = new List<string>();
|
||||
foreach (var kvp in gameObjectCache)
|
||||
{
|
||||
if (!kvp.Value.IsAlive)
|
||||
{
|
||||
toRemove.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
if (toRemove.Count > 0)
|
||||
{
|
||||
foreach (var key in toRemove)
|
||||
{
|
||||
gameObjectCache.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 여전히 캐시 크기가 100개를 초과하면, 일부 항목을 제거하여 공간 확보
|
||||
while (gameObjectCache.Count > 100)
|
||||
{
|
||||
// 가장 간단한 방법으로 첫 번째 항목 제거
|
||||
// 더 나은 방법은 LRU(Least Recently Used) 정책을 구현하는 것입니다
|
||||
var keyToRemove = gameObjectCache.Keys.First();
|
||||
gameObjectCache.Remove(keyToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 캐시 비우기 (테스트용 또는 메모리 정리)
|
||||
/// </summary>
|
||||
public static void ClearCache()
|
||||
{
|
||||
lock (cacheLock)
|
||||
{
|
||||
gameObjectCache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get full path of GameObject in hierarchy
|
||||
/// </summary>
|
||||
protected string GetGameObjectPath(UnityEngine.GameObject obj)
|
||||
{
|
||||
string path = obj.name;
|
||||
var parent = obj.transform.parent;
|
||||
while (parent != null)
|
||||
{
|
||||
path = parent.name + "/" + path;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d527c88236424740b94eab71b673607
|
||||
165
skills/assets/unity-package/Runtime/Handlers/ConsoleHandler.cs
Normal file
165
skills/assets/unity-package/Runtime/Handlers/ConsoleHandler.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEditorToolkit.Protocol;
|
||||
|
||||
namespace UnityEditorToolkit.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for Console commands
|
||||
/// </summary>
|
||||
public class ConsoleHandler : BaseHandler
|
||||
{
|
||||
public override string Category => "Console";
|
||||
|
||||
// Store console logs (Queue로 변경 - O(1) 삽입/삭제)
|
||||
private static Queue<ConsoleLogEntry> logEntries = new Queue<ConsoleLogEntry>(1000);
|
||||
private static readonly object logLock = new object();
|
||||
private static bool isListening = false;
|
||||
|
||||
protected override object HandleMethod(string method, JsonRpcRequest request)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case "GetLogs":
|
||||
return HandleGetLogs(request);
|
||||
case "Clear":
|
||||
return HandleClear(request);
|
||||
default:
|
||||
throw new Exception($"Unknown method: {method}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void StartListening()
|
||||
{
|
||||
if (isListening) return;
|
||||
|
||||
Application.logMessageReceived += OnLogMessageReceived;
|
||||
isListening = true;
|
||||
}
|
||||
|
||||
public static void StopListening()
|
||||
{
|
||||
if (!isListening) return;
|
||||
|
||||
Application.logMessageReceived -= OnLogMessageReceived;
|
||||
isListening = false;
|
||||
}
|
||||
|
||||
private static void OnLogMessageReceived(string message, string stackTrace, LogType type)
|
||||
{
|
||||
lock (logLock)
|
||||
{
|
||||
logEntries.Enqueue(new ConsoleLogEntry
|
||||
{
|
||||
message = message,
|
||||
stackTrace = stackTrace,
|
||||
type = (int)type,
|
||||
timestamp = DateTime.Now.ToString("HH:mm:ss.fff")
|
||||
});
|
||||
|
||||
// Keep only last 1000 logs (✅ O(1) 연산으로 최적화)
|
||||
if (logEntries.Count > 1000)
|
||||
{
|
||||
logEntries.Dequeue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object HandleGetLogs(JsonRpcRequest request)
|
||||
{
|
||||
var param = request.GetParams<GetLogsParams>() ?? new GetLogsParams { count = 50 };
|
||||
|
||||
lock (logLock)
|
||||
{
|
||||
var logs = new List<ConsoleLogEntry>();
|
||||
|
||||
// Queue를 Array로 변환하여 인덱스 접근
|
||||
var logArray = logEntries.ToArray();
|
||||
int start = Math.Max(0, logArray.Length - param.count);
|
||||
|
||||
for (int i = start; i < logArray.Length; i++)
|
||||
{
|
||||
var log = logArray[i];
|
||||
|
||||
// Filter by type
|
||||
if (param.errorsOnly)
|
||||
{
|
||||
if (log.type != (int)LogType.Error && log.type != (int)LogType.Exception)
|
||||
continue;
|
||||
}
|
||||
else if (!param.includeWarnings)
|
||||
{
|
||||
if (log.type == (int)LogType.Warning)
|
||||
continue;
|
||||
}
|
||||
|
||||
logs.Add(log);
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
}
|
||||
|
||||
private object HandleClear(JsonRpcRequest request)
|
||||
{
|
||||
lock (logLock)
|
||||
{
|
||||
logEntries.Clear();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Also clear Unity Editor console (✅ Reflection null 체크 추가)
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.GetAssembly(typeof(UnityEditor.Editor));
|
||||
if (assembly != null)
|
||||
{
|
||||
var type = assembly.GetType("UnityEditor.LogEntries");
|
||||
if (type != null)
|
||||
{
|
||||
var method = type.GetMethod("Clear");
|
||||
if (method != null)
|
||||
{
|
||||
method.Invoke(null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("LogEntries.Clear method not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("UnityEditor.LogEntries type not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to clear Editor console: {ex.Message}");
|
||||
}
|
||||
#endif
|
||||
|
||||
return new { success = true };
|
||||
}
|
||||
|
||||
// Parameter classes
|
||||
[Serializable]
|
||||
public class GetLogsParams
|
||||
{
|
||||
public int count = 50;
|
||||
public bool errorsOnly = false;
|
||||
public bool includeWarnings = false;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ConsoleLogEntry
|
||||
{
|
||||
public string message;
|
||||
public string stackTrace;
|
||||
public int type; // LogType: Error=0, Assert=1, Warning=2, Log=3, Exception=4
|
||||
public string timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cce460e9a41c1244180b19652fd2de16
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEditorToolkit.Protocol;
|
||||
|
||||
namespace UnityEditorToolkit.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for GameObject commands
|
||||
/// </summary>
|
||||
public class GameObjectHandler : BaseHandler
|
||||
{
|
||||
public override string Category => "GameObject";
|
||||
|
||||
protected override object HandleMethod(string method, JsonRpcRequest request)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case "Find":
|
||||
return HandleFind(request);
|
||||
case "Create":
|
||||
return HandleCreate(request);
|
||||
case "Destroy":
|
||||
return HandleDestroy(request);
|
||||
case "SetActive":
|
||||
return HandleSetActive(request);
|
||||
default:
|
||||
throw new Exception($"Unknown method: {method}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find GameObject by name or path
|
||||
/// </summary>
|
||||
private object HandleFind(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<FindParams>(request, "name");
|
||||
var obj = FindGameObject(param.name);
|
||||
|
||||
if (obj == null)
|
||||
{
|
||||
throw new Exception($"GameObject not found: {param.name}");
|
||||
}
|
||||
|
||||
return new GameObjectInfo
|
||||
{
|
||||
name = obj.name,
|
||||
instanceId = obj.GetInstanceID(),
|
||||
path = GetGameObjectPath(obj),
|
||||
active = obj.activeSelf,
|
||||
tag = obj.tag,
|
||||
layer = obj.layer
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new GameObject
|
||||
/// </summary>
|
||||
private object HandleCreate(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<CreateParams>(request, "name");
|
||||
|
||||
GameObject obj = new GameObject(param.name);
|
||||
|
||||
// Set parent if specified
|
||||
if (!string.IsNullOrEmpty(param.parent))
|
||||
{
|
||||
var parentObj = FindGameObject(param.parent);
|
||||
if (parentObj == null)
|
||||
{
|
||||
GameObject.DestroyImmediate(obj);
|
||||
throw new Exception($"Parent GameObject not found: {param.parent}");
|
||||
}
|
||||
obj.transform.SetParent(parentObj.transform);
|
||||
}
|
||||
|
||||
// Register undo
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Undo.RegisterCreatedObjectUndo(obj, "Create GameObject");
|
||||
#endif
|
||||
|
||||
return new GameObjectInfo
|
||||
{
|
||||
name = obj.name,
|
||||
instanceId = obj.GetInstanceID(),
|
||||
path = GetGameObjectPath(obj),
|
||||
active = obj.activeSelf,
|
||||
tag = obj.tag,
|
||||
layer = obj.layer
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroy GameObject
|
||||
/// </summary>
|
||||
private object HandleDestroy(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<FindParams>(request, "name");
|
||||
var obj = FindGameObject(param.name);
|
||||
|
||||
if (obj == null)
|
||||
{
|
||||
throw new Exception($"GameObject not found: {param.name}");
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Undo.DestroyObjectImmediate(obj);
|
||||
#else
|
||||
GameObject.DestroyImmediate(obj);
|
||||
#endif
|
||||
|
||||
return new { success = true };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set GameObject active state
|
||||
/// </summary>
|
||||
private object HandleSetActive(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<SetActiveParams>(request, "name and active");
|
||||
var obj = FindGameObject(param.name);
|
||||
|
||||
if (obj == null)
|
||||
{
|
||||
throw new Exception($"GameObject not found: {param.name}");
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// ✅ RegisterCompleteObjectUndo 사용 (GameObject 전체 상태 기록)
|
||||
UnityEditor.Undo.RegisterCompleteObjectUndo(obj, "Set Active");
|
||||
#endif
|
||||
|
||||
obj.SetActive(param.active);
|
||||
|
||||
return new { success = true, active = obj.activeSelf };
|
||||
}
|
||||
|
||||
// Parameter classes (✅ private으로 변경)
|
||||
[Serializable]
|
||||
private class FindParams
|
||||
{
|
||||
public string name;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class CreateParams
|
||||
{
|
||||
public string name;
|
||||
public string parent;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class SetActiveParams
|
||||
{
|
||||
public string name;
|
||||
public bool active;
|
||||
}
|
||||
|
||||
// Response classes
|
||||
[Serializable]
|
||||
public class GameObjectInfo
|
||||
{
|
||||
public string name;
|
||||
public int instanceId;
|
||||
public string path;
|
||||
public bool active;
|
||||
public string tag;
|
||||
public int layer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2545c55dd409a984a8bde59ac3f6ea5c
|
||||
103
skills/assets/unity-package/Runtime/Handlers/HierarchyHandler.cs
Normal file
103
skills/assets/unity-package/Runtime/Handlers/HierarchyHandler.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEditorToolkit.Protocol;
|
||||
|
||||
namespace UnityEditorToolkit.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for Hierarchy commands
|
||||
/// </summary>
|
||||
public class HierarchyHandler : BaseHandler
|
||||
{
|
||||
public override string Category => "Hierarchy";
|
||||
|
||||
protected override object HandleMethod(string method, JsonRpcRequest request)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case "Get":
|
||||
return HandleGet(request);
|
||||
default:
|
||||
throw new Exception($"Unknown method: {method}");
|
||||
}
|
||||
}
|
||||
|
||||
private object HandleGet(JsonRpcRequest request)
|
||||
{
|
||||
var param = request.GetParams<GetParams>() ?? new GetParams();
|
||||
|
||||
var rootObjects = new List<GameObjectInfo>();
|
||||
var scene = SceneManager.GetActiveScene();
|
||||
|
||||
if (!scene.IsValid())
|
||||
{
|
||||
return rootObjects;
|
||||
}
|
||||
|
||||
foreach (var rootGO in scene.GetRootGameObjects())
|
||||
{
|
||||
// Skip inactive if requested
|
||||
if (!param.includeInactive && !rootGO.activeSelf)
|
||||
continue;
|
||||
|
||||
var info = BuildGameObjectInfo(rootGO, !param.rootOnly, param.includeInactive);
|
||||
rootObjects.Add(info);
|
||||
}
|
||||
|
||||
return rootObjects;
|
||||
}
|
||||
|
||||
private GameObjectInfo BuildGameObjectInfo(GameObject obj, bool includeChildren, bool includeInactive)
|
||||
{
|
||||
var info = new GameObjectInfo
|
||||
{
|
||||
name = obj.name,
|
||||
instanceId = obj.GetInstanceID(),
|
||||
path = GetGameObjectPath(obj),
|
||||
active = obj.activeSelf,
|
||||
tag = obj.tag,
|
||||
layer = obj.layer
|
||||
};
|
||||
|
||||
if (includeChildren && obj.transform.childCount > 0)
|
||||
{
|
||||
info.children = new List<GameObjectInfo>();
|
||||
for (int i = 0; i < obj.transform.childCount; i++)
|
||||
{
|
||||
var child = obj.transform.GetChild(i).gameObject;
|
||||
|
||||
// Skip inactive if requested
|
||||
if (!includeInactive && !child.activeSelf)
|
||||
continue;
|
||||
|
||||
var childInfo = BuildGameObjectInfo(child, true, includeInactive);
|
||||
info.children.Add(childInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
// Parameter classes
|
||||
[Serializable]
|
||||
public class GetParams
|
||||
{
|
||||
public bool rootOnly = false;
|
||||
public bool includeInactive = false;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GameObjectInfo
|
||||
{
|
||||
public string name;
|
||||
public int instanceId;
|
||||
public string path;
|
||||
public bool active;
|
||||
public string tag;
|
||||
public int layer;
|
||||
public List<GameObjectInfo> children;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 021a56e56e85019408bdd9dd044b87e7
|
||||
115
skills/assets/unity-package/Runtime/Handlers/SceneHandler.cs
Normal file
115
skills/assets/unity-package/Runtime/Handlers/SceneHandler.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEditorToolkit.Protocol;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor.SceneManagement;
|
||||
#endif
|
||||
|
||||
namespace UnityEditorToolkit.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for Scene commands
|
||||
/// </summary>
|
||||
public class SceneHandler : BaseHandler
|
||||
{
|
||||
public override string Category => "Scene";
|
||||
|
||||
protected override object HandleMethod(string method, JsonRpcRequest request)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case "GetCurrent":
|
||||
return HandleGetCurrent(request);
|
||||
case "GetAll":
|
||||
return HandleGetAll(request);
|
||||
case "Load":
|
||||
return HandleLoad(request);
|
||||
default:
|
||||
throw new Exception($"Unknown method: {method}");
|
||||
}
|
||||
}
|
||||
|
||||
private object HandleGetCurrent(JsonRpcRequest request)
|
||||
{
|
||||
var scene = SceneManager.GetActiveScene();
|
||||
return GetSceneInfo(scene);
|
||||
}
|
||||
|
||||
private object HandleGetAll(JsonRpcRequest request)
|
||||
{
|
||||
var scenes = new List<SceneInfo>();
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||||
{
|
||||
var scene = SceneManager.GetSceneAt(i);
|
||||
scenes.Add(GetSceneInfo(scene));
|
||||
}
|
||||
return scenes;
|
||||
}
|
||||
|
||||
private object HandleLoad(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<LoadParams>(request, "name");
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Editor mode: Use EditorSceneManager for proper undo/redo
|
||||
try
|
||||
{
|
||||
var mode = param.additive ? OpenSceneMode.Additive : OpenSceneMode.Single;
|
||||
EditorSceneManager.OpenScene(param.name, mode);
|
||||
return new { success = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to load scene: {ex.Message}");
|
||||
}
|
||||
#else
|
||||
// Runtime mode: Use SceneManager
|
||||
try
|
||||
{
|
||||
var mode = param.additive ? LoadSceneMode.Additive : LoadSceneMode.Single;
|
||||
SceneManager.LoadScene(param.name, mode);
|
||||
return new { success = true };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to load scene: {ex.Message}");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private SceneInfo GetSceneInfo(Scene scene)
|
||||
{
|
||||
return new SceneInfo
|
||||
{
|
||||
name = scene.name,
|
||||
path = scene.path,
|
||||
buildIndex = scene.buildIndex,
|
||||
isLoaded = scene.isLoaded,
|
||||
isDirty = scene.isDirty,
|
||||
rootCount = scene.rootCount
|
||||
};
|
||||
}
|
||||
|
||||
// Parameter classes
|
||||
[Serializable]
|
||||
public class LoadParams
|
||||
{
|
||||
public string name;
|
||||
public bool additive;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SceneInfo
|
||||
{
|
||||
public string name;
|
||||
public string path;
|
||||
public int buildIndex;
|
||||
public bool isLoaded;
|
||||
public bool isDirty;
|
||||
public int rootCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2273c4afb7092144b92bbc657dbae285
|
||||
197
skills/assets/unity-package/Runtime/Handlers/TransformHandler.cs
Normal file
197
skills/assets/unity-package/Runtime/Handlers/TransformHandler.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEditorToolkit.Protocol;
|
||||
|
||||
namespace UnityEditorToolkit.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for Transform commands
|
||||
/// </summary>
|
||||
public class TransformHandler : BaseHandler
|
||||
{
|
||||
public override string Category => "Transform";
|
||||
|
||||
protected override object HandleMethod(string method, JsonRpcRequest request)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case "GetPosition":
|
||||
return HandleGetPosition(request);
|
||||
case "SetPosition":
|
||||
return HandleSetPosition(request);
|
||||
case "GetRotation":
|
||||
return HandleGetRotation(request);
|
||||
case "SetRotation":
|
||||
return HandleSetRotation(request);
|
||||
case "GetScale":
|
||||
return HandleGetScale(request);
|
||||
case "SetScale":
|
||||
return HandleSetScale(request);
|
||||
default:
|
||||
throw new Exception($"Unknown method: {method}");
|
||||
}
|
||||
}
|
||||
|
||||
private object HandleGetPosition(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<NameParam>(request, "name");
|
||||
var transform = GetTransform(param.name);
|
||||
|
||||
return new Vector3Data
|
||||
{
|
||||
x = transform.position.x,
|
||||
y = transform.position.y,
|
||||
z = transform.position.z
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleSetPosition(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<SetPositionParam>(request, "name and position");
|
||||
var transform = GetTransform(param.name);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Undo.RecordObject(transform, "Set Position");
|
||||
#endif
|
||||
|
||||
// ✅ ToVector3() 사용 (유효성 검증 포함)
|
||||
transform.position = param.position.ToVector3();
|
||||
|
||||
return new { success = true };
|
||||
}
|
||||
|
||||
private object HandleGetRotation(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<NameParam>(request, "name");
|
||||
var transform = GetTransform(param.name);
|
||||
var euler = transform.eulerAngles;
|
||||
|
||||
return new Vector3Data
|
||||
{
|
||||
x = euler.x,
|
||||
y = euler.y,
|
||||
z = euler.z
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleSetRotation(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<SetRotationParam>(request, "name and rotation");
|
||||
var transform = GetTransform(param.name);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Undo.RecordObject(transform, "Set Rotation");
|
||||
#endif
|
||||
|
||||
// ✅ ToVector3() 사용 (유효성 검증 포함)
|
||||
transform.eulerAngles = param.rotation.ToVector3();
|
||||
|
||||
return new { success = true };
|
||||
}
|
||||
|
||||
private object HandleGetScale(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<NameParam>(request, "name");
|
||||
var transform = GetTransform(param.name);
|
||||
|
||||
return new Vector3Data
|
||||
{
|
||||
x = transform.localScale.x,
|
||||
y = transform.localScale.y,
|
||||
z = transform.localScale.z
|
||||
};
|
||||
}
|
||||
|
||||
private object HandleSetScale(JsonRpcRequest request)
|
||||
{
|
||||
var param = ValidateParam<SetScaleParam>(request, "name and scale");
|
||||
var transform = GetTransform(param.name);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Undo.RecordObject(transform, "Set Scale");
|
||||
#endif
|
||||
|
||||
// ✅ ToVector3() 사용 (유효성 검증 포함)
|
||||
transform.localScale = param.scale.ToVector3();
|
||||
|
||||
return new { success = true };
|
||||
}
|
||||
|
||||
private Transform GetTransform(string name)
|
||||
{
|
||||
var obj = FindGameObject(name);
|
||||
if (obj == null)
|
||||
{
|
||||
throw new Exception($"GameObject not found: {name}");
|
||||
}
|
||||
return obj.transform;
|
||||
}
|
||||
|
||||
// Parameter classes (✅ private으로 변경)
|
||||
[Serializable]
|
||||
private class NameParam
|
||||
{
|
||||
public string name;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class SetPositionParam
|
||||
{
|
||||
public string name;
|
||||
public Vector3Data position;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class SetRotationParam
|
||||
{
|
||||
public string name;
|
||||
public Vector3Data rotation;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class SetScaleParam
|
||||
{
|
||||
public string name;
|
||||
public Vector3Data scale;
|
||||
}
|
||||
|
||||
// Response classes
|
||||
[Serializable]
|
||||
public class Vector3Data
|
||||
{
|
||||
public float x;
|
||||
public float y;
|
||||
public float z;
|
||||
|
||||
/// <summary>
|
||||
/// Vector3로 변환 (✅ 유효성 검증 추가)
|
||||
/// </summary>
|
||||
public Vector3 ToVector3()
|
||||
{
|
||||
// NaN 및 Infinity 체크
|
||||
if (float.IsNaN(x) || float.IsInfinity(x))
|
||||
{
|
||||
throw new ArgumentException($"Invalid x value: {x}");
|
||||
}
|
||||
if (float.IsNaN(y) || float.IsInfinity(y))
|
||||
{
|
||||
throw new ArgumentException($"Invalid y value: {y}");
|
||||
}
|
||||
if (float.IsNaN(z) || float.IsInfinity(z))
|
||||
{
|
||||
throw new ArgumentException($"Invalid z value: {z}");
|
||||
}
|
||||
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vector3에서 생성
|
||||
/// </summary>
|
||||
public static Vector3Data FromVector3(Vector3 v)
|
||||
{
|
||||
return new Vector3Data { x = v.x, y = v.y, z = v.z };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4fd55a1342224c4799f08358b3f35c3
|
||||
8
skills/assets/unity-package/Runtime/Protocol.meta
Normal file
8
skills/assets/unity-package/Runtime/Protocol.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 335da90d5cc2248419636f9003f9aaf8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
skills/assets/unity-package/Runtime/Protocol/JsonRpcError.cs
Normal file
85
skills/assets/unity-package/Runtime/Protocol/JsonRpcError.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace UnityEditorToolkit.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 Error object
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JsonRpcError
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public object Data { get; set; }
|
||||
|
||||
public JsonRpcError() { }
|
||||
|
||||
public JsonRpcError(int code, string message, object data = null)
|
||||
{
|
||||
Code = code;
|
||||
Message = message;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
// Standard JSON-RPC 2.0 error codes
|
||||
public static readonly int PARSE_ERROR = -32700;
|
||||
public static readonly int INVALID_REQUEST = -32600;
|
||||
public static readonly int METHOD_NOT_FOUND = -32601;
|
||||
public static readonly int INVALID_PARAMS = -32602;
|
||||
public static readonly int INTERNAL_ERROR = -32603;
|
||||
|
||||
// Custom Unity error codes
|
||||
public static readonly int UNITY_NOT_CONNECTED = -32000;
|
||||
public static readonly int UNITY_COMMAND_FAILED = -32001;
|
||||
public static readonly int UNITY_OBJECT_NOT_FOUND = -32002;
|
||||
public static readonly int UNITY_SCENE_NOT_FOUND = -32003;
|
||||
public static readonly int UNITY_COMPONENT_NOT_FOUND = -32004;
|
||||
|
||||
// Factory methods for common errors
|
||||
public static JsonRpcError ParseError(string details = null)
|
||||
{
|
||||
return new JsonRpcError(PARSE_ERROR, "Parse error", details);
|
||||
}
|
||||
|
||||
public static JsonRpcError InvalidRequest(string details = null)
|
||||
{
|
||||
return new JsonRpcError(INVALID_REQUEST, "Invalid Request", details);
|
||||
}
|
||||
|
||||
public static JsonRpcError MethodNotFound(string method)
|
||||
{
|
||||
return new JsonRpcError(METHOD_NOT_FOUND, $"Method not found: {method}");
|
||||
}
|
||||
|
||||
public static JsonRpcError InvalidParams(string details)
|
||||
{
|
||||
return new JsonRpcError(INVALID_PARAMS, "Invalid params", details);
|
||||
}
|
||||
|
||||
public static JsonRpcError InternalError(string details)
|
||||
{
|
||||
return new JsonRpcError(INTERNAL_ERROR, "Internal error", details);
|
||||
}
|
||||
|
||||
public static JsonRpcError ObjectNotFound(string objectName)
|
||||
{
|
||||
return new JsonRpcError(UNITY_OBJECT_NOT_FOUND, $"GameObject not found: {objectName}");
|
||||
}
|
||||
|
||||
public static JsonRpcError SceneNotFound(string sceneName)
|
||||
{
|
||||
return new JsonRpcError(UNITY_SCENE_NOT_FOUND, $"Scene not found: {sceneName}");
|
||||
}
|
||||
|
||||
public static JsonRpcError CommandFailed(string details)
|
||||
{
|
||||
return new JsonRpcError(UNITY_COMMAND_FAILED, "Command failed", details);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c34766799e11684ca74b00411016acb
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace UnityEditorToolkit.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 Request
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JsonRpcRequest
|
||||
{
|
||||
[JsonProperty("jsonrpc")]
|
||||
public string JsonRpc { get; set; } = "2.0";
|
||||
|
||||
[JsonProperty("id")]
|
||||
public object Id { get; set; }
|
||||
|
||||
[JsonProperty("method")]
|
||||
public string Method { get; set; }
|
||||
|
||||
[JsonProperty("params")]
|
||||
public JToken Params { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get strongly-typed parameters (✅ 에러 처리 개선)
|
||||
/// </summary>
|
||||
public T GetParams<T>() where T : class
|
||||
{
|
||||
if (Params == null || Params.Type == JTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Params.ToObject<T>();
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonException ex)
|
||||
{
|
||||
// JSON 역직렬화 실패 시 예외를 다시 던져서 호출자가 처리하도록
|
||||
UnityEngine.Debug.LogError($"Failed to deserialize params to {typeof(T).Name}: {ex.Message}");
|
||||
throw new ArgumentException($"Invalid parameter format for {typeof(T).Name}: {ex.Message}", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 기타 예외도 동일하게 처리
|
||||
UnityEngine.Debug.LogError($"Unexpected error deserializing params: {ex.Message}");
|
||||
throw new ArgumentException($"Failed to deserialize parameters: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if request is valid
|
||||
/// </summary>
|
||||
public bool IsValid()
|
||||
{
|
||||
return JsonRpc == "2.0" && !string.IsNullOrEmpty(Method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize request to JSON string
|
||||
/// </summary>
|
||||
public string ToJson()
|
||||
{
|
||||
return JsonConvert.SerializeObject(this, Formatting.None, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize JSON string to request object
|
||||
/// </summary>
|
||||
public static JsonRpcRequest FromJson(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<JsonRpcRequest>(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 840ef3c5f8e0d8241ba5a475dbf83da0
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace UnityEditorToolkit.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 Success Response
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JsonRpcResponse
|
||||
{
|
||||
[JsonProperty("jsonrpc")]
|
||||
public string JsonRpc { get; set; } = "2.0";
|
||||
|
||||
[JsonProperty("id")]
|
||||
public object Id { get; set; }
|
||||
|
||||
[JsonProperty("result")]
|
||||
public object Result { get; set; }
|
||||
|
||||
public JsonRpcResponse() { }
|
||||
|
||||
public JsonRpcResponse(object id, object result)
|
||||
{
|
||||
Id = id;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
return JsonConvert.SerializeObject(this, Formatting.None, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON-RPC 2.0 Error Response
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class JsonRpcErrorResponse
|
||||
{
|
||||
[JsonProperty("jsonrpc")]
|
||||
public string JsonRpc { get; set; } = "2.0";
|
||||
|
||||
[JsonProperty("id")]
|
||||
public object Id { get; set; }
|
||||
|
||||
[JsonProperty("error")]
|
||||
public JsonRpcError Error { get; set; }
|
||||
|
||||
public JsonRpcErrorResponse() { }
|
||||
|
||||
public JsonRpcErrorResponse(object id, JsonRpcError error)
|
||||
{
|
||||
Id = id;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
return JsonConvert.SerializeObject(this, Formatting.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9b7796e1d31f1944a4925d7deb2c5b0
|
||||
8
skills/assets/unity-package/Runtime/Server.meta
Normal file
8
skills/assets/unity-package/Runtime/Server.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 545d7abf1570220418219a23213e1290
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
195
skills/assets/unity-package/Runtime/Server/ServerStatus.cs
Normal file
195
skills/assets/unity-package/Runtime/Server/ServerStatus.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace UnityEditorToolkit.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages server status file for Unity WebSocket Server
|
||||
///
|
||||
/// Stores current server state in .unity-websocket/server-status.json
|
||||
/// allowing CLI tools to discover the correct port and server state.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ServerStatus
|
||||
{
|
||||
// Constants
|
||||
private const int HeartbeatStaleSeconds = 30;
|
||||
|
||||
public string version = "1.0";
|
||||
public int port;
|
||||
public bool isRunning;
|
||||
public int pid;
|
||||
public string editorVersion;
|
||||
public string startedAt;
|
||||
public string lastHeartbeat;
|
||||
|
||||
/// <summary>
|
||||
/// Get server status file path
|
||||
/// </summary>
|
||||
public static string GetStatusFilePath(string projectRoot)
|
||||
{
|
||||
string statusDir = Path.Combine(projectRoot, ".unity-websocket");
|
||||
return Path.Combine(statusDir, "server-status.json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new server status
|
||||
/// </summary>
|
||||
public static ServerStatus Create(int port)
|
||||
{
|
||||
return new ServerStatus
|
||||
{
|
||||
version = "1.0",
|
||||
port = port,
|
||||
isRunning = true,
|
||||
pid = System.Diagnostics.Process.GetCurrentProcess().Id,
|
||||
editorVersion = Application.unityVersion,
|
||||
startedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
||||
lastHeartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save server status to file (atomic write)
|
||||
/// </summary>
|
||||
public static bool Save(ServerStatus status, string projectRoot)
|
||||
{
|
||||
string tempPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
string statusDir = Path.Combine(projectRoot, ".unity-websocket");
|
||||
if (!Directory.Exists(statusDir))
|
||||
{
|
||||
Directory.CreateDirectory(statusDir);
|
||||
}
|
||||
|
||||
string statusPath = GetStatusFilePath(projectRoot);
|
||||
tempPath = statusPath + ".tmp";
|
||||
|
||||
// Serialize to JSON
|
||||
string json = JsonConvert.SerializeObject(status, Formatting.Indented);
|
||||
|
||||
// Write to temp file first
|
||||
File.WriteAllText(tempPath, json, Encoding.UTF8);
|
||||
|
||||
// Atomic replace using File.Replace (crash-safe)
|
||||
if (File.Exists(statusPath))
|
||||
{
|
||||
// File.Replace is atomic - no data loss even if crash occurs
|
||||
File.Replace(tempPath, statusPath, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// First time, just move
|
||||
File.Move(tempPath, statusPath);
|
||||
}
|
||||
|
||||
tempPath = null; // Successfully handled, no cleanup needed
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Unity Editor Toolkit: Failed to save server status: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup temp file if it still exists (error case)
|
||||
if (tempPath != null && File.Exists(tempPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
catch (Exception cleanupEx)
|
||||
{
|
||||
Debug.LogWarning($"Unity Editor Toolkit: Failed to cleanup temp file: {cleanupEx.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load server status from file
|
||||
/// </summary>
|
||||
public static ServerStatus Load(string projectRoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
string statusPath = GetStatusFilePath(projectRoot);
|
||||
|
||||
if (!File.Exists(statusPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(statusPath, Encoding.UTF8);
|
||||
return JsonConvert.DeserializeObject<ServerStatus>(json);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"Unity Editor Toolkit: Failed to load server status: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update heartbeat timestamp
|
||||
/// </summary>
|
||||
public void UpdateHeartbeat()
|
||||
{
|
||||
lastHeartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark server as stopped
|
||||
/// </summary>
|
||||
public static bool MarkStopped(string projectRoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
ServerStatus status = Load(projectRoot);
|
||||
if (status == null)
|
||||
{
|
||||
return true; // Already no status file
|
||||
}
|
||||
|
||||
status.isRunning = false;
|
||||
status.UpdateHeartbeat();
|
||||
return Save(status, projectRoot);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Unity Editor Toolkit: Failed to mark server as stopped: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if status is stale (heartbeat > configured seconds old)
|
||||
/// </summary>
|
||||
public bool IsStale()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(lastHeartbeat))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
DateTime lastBeat = DateTime.Parse(lastHeartbeat);
|
||||
double secondsSinceLastBeat = (DateTime.UtcNow - lastBeat).TotalSeconds;
|
||||
|
||||
return secondsSinceLastBeat > HeartbeatStaleSeconds;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // If we can't parse, assume stale
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86b5800f8a79d5e4e92cb3aefe90b39d
|
||||
380
skills/assets/unity-package/Runtime/Server/UnityEditorServer.cs
Normal file
380
skills/assets/unity-package/Runtime/Server/UnityEditorServer.cs
Normal file
@@ -0,0 +1,380 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
using UnityEditorToolkit.Protocol;
|
||||
using UnityEditorToolkit.Handlers;
|
||||
using UnityEditorToolkit.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
// Note: websocket-sharp requires adding the DLL to ThirdParty folder
|
||||
// Download from: https://github.com/sta/websocket-sharp
|
||||
using WebSocketSharp;
|
||||
using WebSocketSharp.Server;
|
||||
|
||||
namespace UnityEditorToolkit.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity Editor WebSocket Server
|
||||
/// Provides JSON-RPC 2.0 API for controlling Unity Editor via WebSocket
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
public class UnityEditorServer : MonoBehaviour
|
||||
{
|
||||
public enum LogLevel
|
||||
{
|
||||
None = 0,
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Info = 3,
|
||||
Debug = 4
|
||||
}
|
||||
|
||||
[Header("Server Settings")]
|
||||
[Tooltip("WebSocket server port (default: 9500)")]
|
||||
public int port = 9500;
|
||||
|
||||
[Tooltip("Auto-start server on scene load")]
|
||||
public bool autoStart = true;
|
||||
|
||||
[Tooltip("Maximum number of concurrent connections")]
|
||||
public int maxConnections = 5;
|
||||
|
||||
[Tooltip("Command execution timeout in seconds")]
|
||||
public float commandTimeout = 30f;
|
||||
|
||||
[Header("Logging")]
|
||||
[Tooltip("Logging level for debugging")]
|
||||
public LogLevel logLevel = LogLevel.Info;
|
||||
|
||||
[Header("Status")]
|
||||
[SerializeField] private bool isRunning = false;
|
||||
[SerializeField] private int connectedClients = 0;
|
||||
|
||||
private WebSocketServer server;
|
||||
private Dictionary<string, BaseHandler> handlers;
|
||||
private HashSet<string> activeConnections = new HashSet<string>();
|
||||
private float serverStartTime = 0f;
|
||||
private float lastHeartbeatTime = 0f;
|
||||
private const float HeartbeatInterval = 5f; // Update heartbeat every 5 seconds
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Ensure Main Thread Dispatcher exists
|
||||
UnityMainThreadDispatcher.Instance();
|
||||
|
||||
// Initialize handlers
|
||||
handlers = new Dictionary<string, BaseHandler>
|
||||
{
|
||||
{ "GameObject", new GameObjectHandler() },
|
||||
{ "Transform", new TransformHandler() },
|
||||
{ "Scene", new SceneHandler() },
|
||||
{ "Console", new ConsoleHandler() },
|
||||
{ "Hierarchy", new HierarchyHandler() }
|
||||
};
|
||||
|
||||
// Start console logging
|
||||
ConsoleHandler.StartListening();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (autoStart)
|
||||
{
|
||||
StartServer();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
StopServer();
|
||||
ConsoleHandler.StopListening();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Periodic heartbeat update
|
||||
if (isRunning && Time.realtimeSinceStartup - lastHeartbeatTime > HeartbeatInterval)
|
||||
{
|
||||
lastHeartbeatTime = Time.realtimeSinceStartup;
|
||||
|
||||
try
|
||||
{
|
||||
string projectRoot = Path.GetDirectoryName(Application.dataPath);
|
||||
ServerStatus status = ServerStatus.Load(projectRoot);
|
||||
|
||||
if (status != null)
|
||||
{
|
||||
status.UpdateHeartbeat();
|
||||
|
||||
bool saved = ServerStatus.Save(status, projectRoot);
|
||||
if (!saved)
|
||||
{
|
||||
Log("Failed to save heartbeat update", LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log("Failed to load server status for heartbeat update", LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log($"Error updating heartbeat: {e.Message}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start WebSocket server
|
||||
/// </summary>
|
||||
public void StartServer()
|
||||
{
|
||||
if (isRunning)
|
||||
{
|
||||
Log($"Server already running on port {port}", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
server = new WebSocketServer(port);
|
||||
server.AddWebSocketService<EditorService>("/", () => new EditorService(this));
|
||||
server.Start();
|
||||
|
||||
isRunning = true;
|
||||
serverStartTime = Time.realtimeSinceStartup;
|
||||
lastHeartbeatTime = Time.realtimeSinceStartup;
|
||||
|
||||
// Save server status
|
||||
string projectRoot = Path.GetDirectoryName(Application.dataPath);
|
||||
ServerStatus status = ServerStatus.Create(port);
|
||||
ServerStatus.Save(status, projectRoot);
|
||||
|
||||
Log($"✓ Unity Editor Server started on ws://127.0.0.1:{port}", LogLevel.Info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Failed to start server: {ex.Message}", LogLevel.Error);
|
||||
isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop WebSocket server
|
||||
/// </summary>
|
||||
public void StopServer()
|
||||
{
|
||||
if (!isRunning || server == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Mark server as stopped
|
||||
string projectRoot = Path.GetDirectoryName(Application.dataPath);
|
||||
ServerStatus.MarkStopped(projectRoot);
|
||||
|
||||
server.Stop();
|
||||
server = null;
|
||||
isRunning = false;
|
||||
connectedClients = 0;
|
||||
activeConnections.Clear();
|
||||
Log("Unity Editor Server stopped", LogLevel.Info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Error stopping server: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle JSON-RPC request (메인 스레드에서 실행됨)
|
||||
/// </summary>
|
||||
internal string HandleRequest(string message)
|
||||
{
|
||||
JsonRpcRequest request = null;
|
||||
float startTime = Time.realtimeSinceStartup;
|
||||
|
||||
try
|
||||
{
|
||||
// Parse JSON-RPC request
|
||||
request = JsonConvert.DeserializeObject<JsonRpcRequest>(message);
|
||||
|
||||
if (request == null || !request.IsValid())
|
||||
{
|
||||
return new JsonRpcErrorResponse(null, JsonRpcError.InvalidRequest()).ToJson();
|
||||
}
|
||||
|
||||
Log($"Request: {request.Method}", LogLevel.Debug);
|
||||
|
||||
// Health check (ping)
|
||||
if (request.Method == "ping")
|
||||
{
|
||||
return new JsonRpcResponse(request.Id, new
|
||||
{
|
||||
status = "ok",
|
||||
version = "0.1.0",
|
||||
uptime = Time.realtimeSinceStartup - serverStartTime,
|
||||
handlers = new List<string>(handlers.Keys),
|
||||
connectedClients = connectedClients
|
||||
}).ToJson();
|
||||
}
|
||||
|
||||
// Extract category from method (e.g., "GameObject.Find" -> "GameObject")
|
||||
string category = GetCategory(request.Method);
|
||||
|
||||
if (!handlers.ContainsKey(category))
|
||||
{
|
||||
return new JsonRpcErrorResponse(request.Id, JsonRpcError.MethodNotFound(request.Method)).ToJson();
|
||||
}
|
||||
|
||||
// Handle request with appropriate handler
|
||||
var handler = handlers[category];
|
||||
var result = handler.Handle(request);
|
||||
|
||||
// Check timeout
|
||||
float elapsed = Time.realtimeSinceStartup - startTime;
|
||||
if (elapsed > commandTimeout)
|
||||
{
|
||||
Log($"Command timeout: {request.Method} took {elapsed:F2}s", LogLevel.Warning);
|
||||
}
|
||||
|
||||
// Return success response
|
||||
return new JsonRpcResponse(request.Id, result).ToJson();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Log($"JSON Parse Error: {ex.Message}", LogLevel.Error);
|
||||
return new JsonRpcErrorResponse(null, JsonRpcError.ParseError(ex.Message)).ToJson();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Request Handler Error: {ex.Message}\n{ex.StackTrace}", LogLevel.Error);
|
||||
// ✅ request?.Id 사용 (High 이슈 해결)
|
||||
return new JsonRpcErrorResponse(request?.Id, JsonRpcError.InternalError(ex.Message)).ToJson();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCategory(string method)
|
||||
{
|
||||
int dotIndex = method.IndexOf('.');
|
||||
if (dotIndex < 0)
|
||||
{
|
||||
throw new Exception($"Invalid method format: {method}");
|
||||
}
|
||||
return method.Substring(0, dotIndex);
|
||||
}
|
||||
|
||||
internal bool OnClientConnected(string connectionId)
|
||||
{
|
||||
if (activeConnections.Count >= maxConnections)
|
||||
{
|
||||
Log($"Max connections reached ({maxConnections}), rejecting connection", LogLevel.Warning);
|
||||
return false;
|
||||
}
|
||||
|
||||
activeConnections.Add(connectionId);
|
||||
connectedClients++;
|
||||
Log($"Client connected: {connectionId} (total: {connectedClients})", LogLevel.Info);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void OnClientDisconnected(string connectionId)
|
||||
{
|
||||
if (activeConnections.Remove(connectionId))
|
||||
{
|
||||
connectedClients--;
|
||||
Log($"Client disconnected: {connectionId} (remaining: {connectedClients})", LogLevel.Info);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 로깅 메서드
|
||||
/// </summary>
|
||||
private void Log(string message, LogLevel level)
|
||||
{
|
||||
if (level <= logLevel)
|
||||
{
|
||||
string prefix = "[UnityEditorToolkit]";
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel.Error:
|
||||
Debug.LogError($"{prefix} {message}");
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
Debug.LogWarning($"{prefix} {message}");
|
||||
break;
|
||||
case LogLevel.Info:
|
||||
case LogLevel.Debug:
|
||||
Debug.Log($"{prefix} {message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket service behavior
|
||||
/// </summary>
|
||||
private class EditorService : WebSocketBehavior
|
||||
{
|
||||
private UnityEditorServer server;
|
||||
|
||||
public EditorService(UnityEditorServer server)
|
||||
{
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
protected override void OnOpen()
|
||||
{
|
||||
// Check max connections
|
||||
if (!server.OnClientConnected(ID))
|
||||
{
|
||||
Context.WebSocket.Close(CloseStatusCode.PolicyViolation, "Max connections reached");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClose(CloseEventArgs e)
|
||||
{
|
||||
server.OnClientDisconnected(ID);
|
||||
}
|
||||
|
||||
protected override void OnMessage(MessageEventArgs e)
|
||||
{
|
||||
// ✅ Critical 이슈 해결: WebSocket 스레드에서 메인 스레드로 작업 전달
|
||||
string message = e.Data;
|
||||
|
||||
// Unity API는 반드시 메인 스레드에서만 호출
|
||||
UnityMainThreadDispatcher.Instance().Enqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// 메인 스레드에서 요청 처리
|
||||
string response = server.HandleRequest(message);
|
||||
|
||||
// 응답 전송 (Send는 스레드 안전)
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
Send(response);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
server.Log($"Error processing message: {ex.Message}", LogLevel.Error);
|
||||
|
||||
// 에러 응답 전송
|
||||
var errorResponse = new JsonRpcErrorResponse(null,
|
||||
JsonRpcError.InternalError(ex.Message)).ToJson();
|
||||
Send(errorResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
|
||||
{
|
||||
server.Log($"WebSocket Error: {e.Message}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28fbab6e6d8088744b0598d5505cc79d
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "UnityEditorToolkit",
|
||||
"rootNamespace": "UnityEditorToolkit",
|
||||
"references": [
|
||||
"Unity.Nuget.Newtonsoft-Json"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d0c459e099288547aee4569f4b646db
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
skills/assets/unity-package/Runtime/Utils.meta
Normal file
8
skills/assets/unity-package/Runtime/Utils.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a67fb2b4f6ccc704493bbc5335b252d7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditorToolkit.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity 메인 스레드에서 작업을 실행하기 위한 Dispatcher
|
||||
/// WebSocket 등 다른 스레드에서 Unity API를 호출할 때 사용
|
||||
/// </summary>
|
||||
public class UnityMainThreadDispatcher : MonoBehaviour
|
||||
{
|
||||
private static UnityMainThreadDispatcher instance;
|
||||
private static readonly Queue<Action> executionQueue = new Queue<Action>();
|
||||
private static readonly object @lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Singleton 인스턴스 가져오기
|
||||
/// </summary>
|
||||
public static UnityMainThreadDispatcher Instance()
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
// 메인 스레드에서만 GameObject 생성 가능
|
||||
if (UnityEngine.Object.FindObjectOfType<UnityMainThreadDispatcher>() == null)
|
||||
{
|
||||
var go = new GameObject("UnityMainThreadDispatcher");
|
||||
instance = go.AddComponent<UnityMainThreadDispatcher>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
else
|
||||
{
|
||||
instance = UnityEngine.Object.FindObjectOfType<UnityMainThreadDispatcher>();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else if (instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 스레드에서 실행할 작업 등록
|
||||
/// </summary>
|
||||
/// <param name="action">실행할 작업</param>
|
||||
public void Enqueue(Action action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
lock (@lock)
|
||||
{
|
||||
executionQueue.Enqueue(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 스레드에서 실행할 작업 등록 (콜백 포함)
|
||||
/// </summary>
|
||||
/// <param name="action">실행할 작업</param>
|
||||
/// <param name="callback">완료 후 콜백</param>
|
||||
public void Enqueue(Action action, Action<Exception> callback)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
lock (@lock)
|
||||
{
|
||||
executionQueue.Enqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke();
|
||||
callback?.Invoke(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
callback?.Invoke(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 메인 스레드에서 큐에 있는 작업 실행
|
||||
lock (@lock)
|
||||
{
|
||||
while (executionQueue.Count > 0)
|
||||
{
|
||||
var action = executionQueue.Dequeue();
|
||||
try
|
||||
{
|
||||
action.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[UnityMainThreadDispatcher] Error executing action: {ex.Message}\n{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (instance == this)
|
||||
{
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 큐에 있는 작업 개수
|
||||
/// </summary>
|
||||
public int QueueCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
return executionQueue.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 큐 비우기
|
||||
/// </summary>
|
||||
public void ClearQueue()
|
||||
{
|
||||
lock (@lock)
|
||||
{
|
||||
executionQueue.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0dd7854ca02e9f741b76eff4a8f15b16
|
||||
Reference in New Issue
Block a user