From 9b112243578d9a9c9a1b147b4b16efb992424fbf Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 9 Apr 2025 19:58:42 -0700 Subject: [PATCH] Feat: Generalize GetComponentData using reflection --- .../Editor/Tools/ManageGameObject.cs | 139 +++++++++++++++--- 1 file changed, 119 insertions(+), 20 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 414603d..691ade6 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -2182,39 +2182,138 @@ namespace UnityMcpBridge.Editor.Tools } /// - /// Creates a serializable representation of a Component. - /// TODO: Add property serialization. + /// Creates a serializable representation of a Component, attempting to serialize + /// public properties and fields using reflection. /// private static object GetComponentData(Component c) { - if (c == null) - return null; + if (c == null) return null; + var data = new Dictionary { { "typeName", c.GetType().FullName }, - { "instanceID", c.GetInstanceID() }, + { "instanceID", c.GetInstanceID() } }; - // Attempt to serialize public properties/fields (can be noisy/complex) - /* - try { - var properties = new Dictionary(); - var type = c.GetType(); - BindingFlags flags = BindingFlags.Public | BindingFlags.Instance; - - foreach (var prop in type.GetProperties(flags).Where(p => p.CanRead && p.GetIndexParameters().Length == 0)) { - try { properties[prop.Name] = prop.GetValue(c); } catch { } + var serializableProperties = new Dictionary(); + Type componentType = c.GetType(); + // Include NonPublic flags for fields, keep Public for properties initially + BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance; + + // Process Properties (Still only public for properties) + // Using propFlags here + foreach (var propInfo in componentType.GetProperties(propFlags)) + { + // Skip indexers and write-only properties, and skip the transform property as it's handled by GetGameObjectData + if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue; + + try + { + object value = propInfo.GetValue(c); + string propName = propInfo.Name; + Type propType = propInfo.PropertyType; + + AddSerializableValue(serializableProperties, propName, propType, value); } - foreach (var field in type.GetFields(flags)) { - try { properties[field.Name] = field.GetValue(c); } catch { } + catch (Exception ex) + { + Debug.LogWarning($"Could not read property {propInfo.Name} on {componentType.Name}: {ex.Message}"); } - data["properties"] = properties; - } catch (Exception ex) { - data["propertiesError"] = ex.Message; } - */ + + // Process Fields (Include NonPublic) + // Using fieldFlags here + foreach (var fieldInfo in componentType.GetFields(fieldFlags)) + { + // Skip backing fields for properties (common pattern) + if (fieldInfo.Name.EndsWith("k__BackingField")) continue; + + // Only include public fields or non-public fields with [SerializeField] + // Check if the field is explicitly marked with SerializeField or if it's public + bool isSerializable = fieldInfo.IsPublic || fieldInfo.IsDefined(typeof(SerializeField), inherit: false); // inherit: false is typical for SerializeField + + if (!isSerializable) continue; // Skip if not public and not explicitly serialized + + try + { + object value = fieldInfo.GetValue(c); + string fieldName = fieldInfo.Name; + Type fieldType = fieldInfo.FieldType; + + AddSerializableValue(serializableProperties, fieldName, fieldType, value); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}"); + } + } + + if (serializableProperties.Count > 0) + { + data["properties"] = serializableProperties; // Add the collected properties + } + return data; } + + // Helper function to decide how to serialize different types + private static void AddSerializableValue(Dictionary dict, string name, Type type, object value) + { + if (value == null) + { + dict[name] = null; + return; + } + + // Primitives & Enums + if (type.IsPrimitive || type.IsEnum || type == typeof(string)) + { + dict[name] = value; + } + // Known Unity Structs (add more as needed: Rect, Bounds, etc.) + else if (type == typeof(Vector2)) { var v = (Vector2)value; dict[name] = new { v.x, v.y }; } + else if (type == typeof(Vector3)) { var v = (Vector3)value; dict[name] = new { v.x, v.y, v.z }; } + else if (type == typeof(Vector4)) { var v = (Vector4)value; dict[name] = new { v.x, v.y, v.z, v.w }; } + else if (type == typeof(Quaternion)) { var q = (Quaternion)value; dict[name] = new { x = q.eulerAngles.x, y = q.eulerAngles.y, z = q.eulerAngles.z }; } // Serialize as Euler angles for readability + else if (type == typeof(Color)) { var c = (Color)value; dict[name] = new { c.r, c.g, c.b, c.a }; } + // UnityEngine.Object References + else if (typeof(UnityEngine.Object).IsAssignableFrom(type)) + { + var obj = value as UnityEngine.Object; + if (obj != null) { + // Use dynamic or a helper class for flexible properties if adding assetPath + var refData = new Dictionary { + { "name", obj.name }, + { "instanceID", obj.GetInstanceID() }, + { "typeName", obj.GetType().FullName } + }; + string assetPath = AssetDatabase.GetAssetPath(obj); + if (!string.IsNullOrEmpty(assetPath)) { + refData["assetPath"] = assetPath; + } + dict[name] = refData; + + } else { + dict[name] = null; // The object reference is null + } + } + // Add handling for basic Lists/Arrays of primitives? (Example for List) + else if (type == typeof(List)) { + dict[name] = value as List; // Directly serializable + } + else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { + // Could attempt to serialize lists of primitives/structs/references here if needed + dict[name] = $"[Skipped List<{type.GetGenericArguments()[0].Name}>]"; + } + else if (type.IsArray) { + dict[name] = $"[Skipped Array<{type.GetElementType().Name}>]"; + } + // Skip other complex types for now + else { + dict[name] = $"[Skipped complex type: {type.FullName}]"; + } + } } }