Refactor: Extract GameObject/Component serialization to GameObjectSerializer helper
Moved serialization logic (GetGameObjectData, GetComponentData, metadata caching, JSON conversion helpers) from ManageGameObject tool to a dedicated GameObjectSerializer class in the Helpers namespace. This improves separation of concerns and reduces the size/complexity of ManageGameObject.cs. Updated ManageGameObject to use the new helper class.main
parent
15ba68f473
commit
dd0113d258
|
|
@ -0,0 +1,378 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityMcpBridge.Runtime.Serialization; // For Converters
|
||||
|
||||
namespace UnityMcpBridge.Editor.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles serialization of GameObjects and Components for MCP responses.
|
||||
/// Includes reflection helpers and caching for performance.
|
||||
/// </summary>
|
||||
public static class GameObjectSerializer
|
||||
{
|
||||
// --- Data Serialization ---
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serializable representation of a GameObject.
|
||||
/// </summary>
|
||||
public static object GetGameObjectData(GameObject go)
|
||||
{
|
||||
if (go == null)
|
||||
return null;
|
||||
return new
|
||||
{
|
||||
name = go.name,
|
||||
instanceID = go.GetInstanceID(),
|
||||
tag = go.tag,
|
||||
layer = go.layer,
|
||||
activeSelf = go.activeSelf,
|
||||
activeInHierarchy = go.activeInHierarchy,
|
||||
isStatic = go.isStatic,
|
||||
scenePath = go.scene.path, // Identify which scene it belongs to
|
||||
transform = new // Serialize transform components carefully to avoid JSON issues
|
||||
{
|
||||
// Serialize Vector3 components individually to prevent self-referencing loops.
|
||||
// The default serializer can struggle with properties like Vector3.normalized.
|
||||
position = new
|
||||
{
|
||||
x = go.transform.position.x,
|
||||
y = go.transform.position.y,
|
||||
z = go.transform.position.z,
|
||||
},
|
||||
localPosition = new
|
||||
{
|
||||
x = go.transform.localPosition.x,
|
||||
y = go.transform.localPosition.y,
|
||||
z = go.transform.localPosition.z,
|
||||
},
|
||||
rotation = new
|
||||
{
|
||||
x = go.transform.rotation.eulerAngles.x,
|
||||
y = go.transform.rotation.eulerAngles.y,
|
||||
z = go.transform.rotation.eulerAngles.z,
|
||||
},
|
||||
localRotation = new
|
||||
{
|
||||
x = go.transform.localRotation.eulerAngles.x,
|
||||
y = go.transform.localRotation.eulerAngles.y,
|
||||
z = go.transform.localRotation.eulerAngles.z,
|
||||
},
|
||||
scale = new
|
||||
{
|
||||
x = go.transform.localScale.x,
|
||||
y = go.transform.localScale.y,
|
||||
z = go.transform.localScale.z,
|
||||
},
|
||||
forward = new
|
||||
{
|
||||
x = go.transform.forward.x,
|
||||
y = go.transform.forward.y,
|
||||
z = go.transform.forward.z,
|
||||
},
|
||||
up = new
|
||||
{
|
||||
x = go.transform.up.x,
|
||||
y = go.transform.up.y,
|
||||
z = go.transform.up.z,
|
||||
},
|
||||
right = new
|
||||
{
|
||||
x = go.transform.right.x,
|
||||
y = go.transform.right.y,
|
||||
z = go.transform.right.z,
|
||||
},
|
||||
},
|
||||
parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
|
||||
// Optionally include components, but can be large
|
||||
// components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
|
||||
// Or just component names:
|
||||
componentNames = go.GetComponents<Component>()
|
||||
.Select(c => c.GetType().FullName)
|
||||
.ToList(),
|
||||
};
|
||||
}
|
||||
|
||||
// --- Metadata Caching for Reflection ---
|
||||
private class CachedMetadata
|
||||
{
|
||||
public readonly List<PropertyInfo> SerializableProperties;
|
||||
public readonly List<FieldInfo> SerializableFields;
|
||||
|
||||
public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields)
|
||||
{
|
||||
SerializableProperties = properties;
|
||||
SerializableFields = fields;
|
||||
}
|
||||
}
|
||||
// Key becomes Tuple<Type, bool>
|
||||
private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>();
|
||||
// --- End Metadata Caching ---
|
||||
|
||||
/// <summary>
|
||||
/// Creates a serializable representation of a Component, attempting to serialize
|
||||
/// public properties and fields using reflection, with caching and control over non-public fields.
|
||||
/// </summary>
|
||||
// Add the flag parameter here
|
||||
public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
|
||||
{
|
||||
if (c == null) return null;
|
||||
Type componentType = c.GetType();
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "typeName", componentType.FullName },
|
||||
{ "instanceID", c.GetInstanceID() }
|
||||
};
|
||||
|
||||
// --- Get Cached or Generate Metadata (using new cache key) ---
|
||||
Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields);
|
||||
if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
|
||||
{
|
||||
var propertiesToCache = new List<PropertyInfo>();
|
||||
var fieldsToCache = new List<FieldInfo>();
|
||||
|
||||
// Traverse the hierarchy from the component type up to MonoBehaviour
|
||||
Type currentType = componentType;
|
||||
while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
|
||||
{
|
||||
// Get properties declared only at the current type level
|
||||
BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
foreach (var propInfo in currentType.GetProperties(propFlags))
|
||||
{
|
||||
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
|
||||
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
|
||||
// Add if not already added (handles overrides - keep the most derived version)
|
||||
if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) {
|
||||
propertiesToCache.Add(propInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get fields declared only at the current type level (both public and non-public)
|
||||
BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
var declaredFields = currentType.GetFields(fieldFlags);
|
||||
|
||||
// Process the declared Fields for caching
|
||||
foreach (var fieldInfo in declaredFields)
|
||||
{
|
||||
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
|
||||
|
||||
// Add if not already added (handles hiding - keep the most derived version)
|
||||
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
|
||||
|
||||
bool shouldInclude = false;
|
||||
if (includeNonPublicSerializedFields)
|
||||
{
|
||||
// If TRUE, include Public OR NonPublic with [SerializeField]
|
||||
shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false));
|
||||
}
|
||||
else // includeNonPublicSerializedFields is FALSE
|
||||
{
|
||||
// If FALSE, include ONLY if it is explicitly Public.
|
||||
shouldInclude = fieldInfo.IsPublic;
|
||||
}
|
||||
|
||||
if (shouldInclude)
|
||||
{
|
||||
fieldsToCache.Add(fieldInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the base type
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
// --- End Hierarchy Traversal ---
|
||||
|
||||
cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
|
||||
_metadataCache[cacheKey] = cachedData; // Add to cache with combined key
|
||||
}
|
||||
// --- End Get Cached or Generate Metadata ---
|
||||
|
||||
// --- Use cached metadata ---
|
||||
var serializablePropertiesOutput = new Dictionary<string, object>();
|
||||
// Use cached properties
|
||||
foreach (var propInfo in cachedData.SerializableProperties)
|
||||
{
|
||||
// --- Skip known obsolete/problematic Component shortcut properties ---
|
||||
string propName = propInfo.Name;
|
||||
if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
|
||||
propName == "light" || propName == "animation" || propName == "constantForce" ||
|
||||
propName == "renderer" || propName == "audio" || propName == "networkView" ||
|
||||
propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
|
||||
propName == "particleSystem" ||
|
||||
// Also skip potentially problematic Matrix properties prone to cycles/errors
|
||||
propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
|
||||
{
|
||||
continue; // Skip these properties
|
||||
}
|
||||
// --- End Skip ---
|
||||
|
||||
try
|
||||
{
|
||||
object value = propInfo.GetValue(c);
|
||||
Type propType = propInfo.PropertyType;
|
||||
AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Use cached fields
|
||||
foreach (var fieldInfo in cachedData.SerializableFields)
|
||||
{
|
||||
try
|
||||
{
|
||||
object value = fieldInfo.GetValue(c);
|
||||
string fieldName = fieldInfo.Name;
|
||||
Type fieldType = fieldInfo.FieldType;
|
||||
AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
// --- End Use cached metadata ---
|
||||
|
||||
if (serializablePropertiesOutput.Count > 0)
|
||||
{
|
||||
data["properties"] = serializablePropertiesOutput;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Helper function to decide how to serialize different types
|
||||
private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value)
|
||||
{
|
||||
// Simplified: Directly use CreateTokenFromValue which uses the serializer
|
||||
if (value == null)
|
||||
{
|
||||
dict[name] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use the helper that employs our custom serializer settings
|
||||
JToken token = CreateTokenFromValue(value, type);
|
||||
if (token != null) // Check if serialization succeeded in the helper
|
||||
{
|
||||
// Convert JToken back to a basic object structure for the dictionary
|
||||
dict[name] = ConvertJTokenToPlainObject(token);
|
||||
}
|
||||
// If token is null, it means serialization failed and a warning was logged.
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Catch potential errors during JToken conversion or addition to dictionary
|
||||
Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to convert JToken back to basic object structure
|
||||
private static object ConvertJTokenToPlainObject(JToken token)
|
||||
{
|
||||
if (token == null) return null;
|
||||
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Object:
|
||||
var objDict = new Dictionary<string, object>();
|
||||
foreach (var prop in ((JObject)token).Properties())
|
||||
{
|
||||
objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
|
||||
}
|
||||
return objDict;
|
||||
|
||||
case JTokenType.Array:
|
||||
var list = new List<object>();
|
||||
foreach (var item in (JArray)token)
|
||||
{
|
||||
list.Add(ConvertJTokenToPlainObject(item));
|
||||
}
|
||||
return list;
|
||||
|
||||
case JTokenType.Integer:
|
||||
return token.ToObject<long>(); // Use long for safety
|
||||
case JTokenType.Float:
|
||||
return token.ToObject<double>(); // Use double for safety
|
||||
case JTokenType.String:
|
||||
return token.ToObject<string>();
|
||||
case JTokenType.Boolean:
|
||||
return token.ToObject<bool>();
|
||||
case JTokenType.Date:
|
||||
return token.ToObject<DateTime>();
|
||||
case JTokenType.Guid:
|
||||
return token.ToObject<Guid>();
|
||||
case JTokenType.Uri:
|
||||
return token.ToObject<Uri>();
|
||||
case JTokenType.TimeSpan:
|
||||
return token.ToObject<TimeSpan>();
|
||||
case JTokenType.Bytes:
|
||||
return token.ToObject<byte[]>();
|
||||
case JTokenType.Null:
|
||||
return null;
|
||||
case JTokenType.Undefined:
|
||||
return null; // Treat undefined as null
|
||||
|
||||
default:
|
||||
// Fallback for simple value types not explicitly listed
|
||||
if (token is JValue jValue && jValue.Value != null)
|
||||
{
|
||||
return jValue.Value;
|
||||
}
|
||||
Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Define custom JsonSerializerSettings for OUTPUT ---
|
||||
private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter>
|
||||
{
|
||||
new Vector3Converter(),
|
||||
new Vector2Converter(),
|
||||
new QuaternionConverter(),
|
||||
new ColorConverter(),
|
||||
new RectConverter(),
|
||||
new BoundsConverter(),
|
||||
new UnityEngineObjectConverter() // Handles serialization of references
|
||||
},
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
// ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
|
||||
};
|
||||
private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings);
|
||||
// --- End Define custom JsonSerializerSettings ---
|
||||
|
||||
// Helper to create JToken using the output serializer
|
||||
private static JToken CreateTokenFromValue(object value, Type type)
|
||||
{
|
||||
if (value == null) return JValue.CreateNull();
|
||||
|
||||
try
|
||||
{
|
||||
// Use the pre-configured OUTPUT serializer instance
|
||||
return JToken.FromObject(value, _outputSerializer);
|
||||
}
|
||||
catch (JsonSerializationException e)
|
||||
{
|
||||
Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
|
||||
return null; // Indicate serialization failure
|
||||
}
|
||||
catch (Exception e) // Catch other unexpected errors
|
||||
{
|
||||
Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
|
||||
return null; // Indicate serialization failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue