Skip to content

Commit dd0113d

Browse files
committed
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.
1 parent 15ba68f commit dd0113d

File tree

2 files changed

+746
-790
lines changed

2 files changed

+746
-790
lines changed
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using Newtonsoft.Json;
6+
using Newtonsoft.Json.Linq;
7+
using UnityEditor;
8+
using UnityEngine;
9+
using UnityMcpBridge.Runtime.Serialization; // For Converters
10+
11+
namespace UnityMcpBridge.Editor.Helpers
12+
{
13+
/// <summary>
14+
/// Handles serialization of GameObjects and Components for MCP responses.
15+
/// Includes reflection helpers and caching for performance.
16+
/// </summary>
17+
public static class GameObjectSerializer
18+
{
19+
// --- Data Serialization ---
20+
21+
/// <summary>
22+
/// Creates a serializable representation of a GameObject.
23+
/// </summary>
24+
public static object GetGameObjectData(GameObject go)
25+
{
26+
if (go == null)
27+
return null;
28+
return new
29+
{
30+
name = go.name,
31+
instanceID = go.GetInstanceID(),
32+
tag = go.tag,
33+
layer = go.layer,
34+
activeSelf = go.activeSelf,
35+
activeInHierarchy = go.activeInHierarchy,
36+
isStatic = go.isStatic,
37+
scenePath = go.scene.path, // Identify which scene it belongs to
38+
transform = new // Serialize transform components carefully to avoid JSON issues
39+
{
40+
// Serialize Vector3 components individually to prevent self-referencing loops.
41+
// The default serializer can struggle with properties like Vector3.normalized.
42+
position = new
43+
{
44+
x = go.transform.position.x,
45+
y = go.transform.position.y,
46+
z = go.transform.position.z,
47+
},
48+
localPosition = new
49+
{
50+
x = go.transform.localPosition.x,
51+
y = go.transform.localPosition.y,
52+
z = go.transform.localPosition.z,
53+
},
54+
rotation = new
55+
{
56+
x = go.transform.rotation.eulerAngles.x,
57+
y = go.transform.rotation.eulerAngles.y,
58+
z = go.transform.rotation.eulerAngles.z,
59+
},
60+
localRotation = new
61+
{
62+
x = go.transform.localRotation.eulerAngles.x,
63+
y = go.transform.localRotation.eulerAngles.y,
64+
z = go.transform.localRotation.eulerAngles.z,
65+
},
66+
scale = new
67+
{
68+
x = go.transform.localScale.x,
69+
y = go.transform.localScale.y,
70+
z = go.transform.localScale.z,
71+
},
72+
forward = new
73+
{
74+
x = go.transform.forward.x,
75+
y = go.transform.forward.y,
76+
z = go.transform.forward.z,
77+
},
78+
up = new
79+
{
80+
x = go.transform.up.x,
81+
y = go.transform.up.y,
82+
z = go.transform.up.z,
83+
},
84+
right = new
85+
{
86+
x = go.transform.right.x,
87+
y = go.transform.right.y,
88+
z = go.transform.right.z,
89+
},
90+
},
91+
parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
92+
// Optionally include components, but can be large
93+
// components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
94+
// Or just component names:
95+
componentNames = go.GetComponents<Component>()
96+
.Select(c => c.GetType().FullName)
97+
.ToList(),
98+
};
99+
}
100+
101+
// --- Metadata Caching for Reflection ---
102+
private class CachedMetadata
103+
{
104+
public readonly List<PropertyInfo> SerializableProperties;
105+
public readonly List<FieldInfo> SerializableFields;
106+
107+
public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields)
108+
{
109+
SerializableProperties = properties;
110+
SerializableFields = fields;
111+
}
112+
}
113+
// Key becomes Tuple<Type, bool>
114+
private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>();
115+
// --- End Metadata Caching ---
116+
117+
/// <summary>
118+
/// Creates a serializable representation of a Component, attempting to serialize
119+
/// public properties and fields using reflection, with caching and control over non-public fields.
120+
/// </summary>
121+
// Add the flag parameter here
122+
public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
123+
{
124+
if (c == null) return null;
125+
Type componentType = c.GetType();
126+
127+
var data = new Dictionary<string, object>
128+
{
129+
{ "typeName", componentType.FullName },
130+
{ "instanceID", c.GetInstanceID() }
131+
};
132+
133+
// --- Get Cached or Generate Metadata (using new cache key) ---
134+
Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields);
135+
if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
136+
{
137+
var propertiesToCache = new List<PropertyInfo>();
138+
var fieldsToCache = new List<FieldInfo>();
139+
140+
// Traverse the hierarchy from the component type up to MonoBehaviour
141+
Type currentType = componentType;
142+
while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
143+
{
144+
// Get properties declared only at the current type level
145+
BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
146+
foreach (var propInfo in currentType.GetProperties(propFlags))
147+
{
148+
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
149+
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
150+
// Add if not already added (handles overrides - keep the most derived version)
151+
if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) {
152+
propertiesToCache.Add(propInfo);
153+
}
154+
}
155+
156+
// Get fields declared only at the current type level (both public and non-public)
157+
BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
158+
var declaredFields = currentType.GetFields(fieldFlags);
159+
160+
// Process the declared Fields for caching
161+
foreach (var fieldInfo in declaredFields)
162+
{
163+
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
164+
165+
// Add if not already added (handles hiding - keep the most derived version)
166+
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
167+
168+
bool shouldInclude = false;
169+
if (includeNonPublicSerializedFields)
170+
{
171+
// If TRUE, include Public OR NonPublic with [SerializeField]
172+
shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false));
173+
}
174+
else // includeNonPublicSerializedFields is FALSE
175+
{
176+
// If FALSE, include ONLY if it is explicitly Public.
177+
shouldInclude = fieldInfo.IsPublic;
178+
}
179+
180+
if (shouldInclude)
181+
{
182+
fieldsToCache.Add(fieldInfo);
183+
}
184+
}
185+
186+
// Move to the base type
187+
currentType = currentType.BaseType;
188+
}
189+
// --- End Hierarchy Traversal ---
190+
191+
cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
192+
_metadataCache[cacheKey] = cachedData; // Add to cache with combined key
193+
}
194+
// --- End Get Cached or Generate Metadata ---
195+
196+
// --- Use cached metadata ---
197+
var serializablePropertiesOutput = new Dictionary<string, object>();
198+
// Use cached properties
199+
foreach (var propInfo in cachedData.SerializableProperties)
200+
{
201+
// --- Skip known obsolete/problematic Component shortcut properties ---
202+
string propName = propInfo.Name;
203+
if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
204+
propName == "light" || propName == "animation" || propName == "constantForce" ||
205+
propName == "renderer" || propName == "audio" || propName == "networkView" ||
206+
propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
207+
propName == "particleSystem" ||
208+
// Also skip potentially problematic Matrix properties prone to cycles/errors
209+
propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
210+
{
211+
continue; // Skip these properties
212+
}
213+
// --- End Skip ---
214+
215+
try
216+
{
217+
object value = propInfo.GetValue(c);
218+
Type propType = propInfo.PropertyType;
219+
AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
220+
}
221+
catch (Exception ex)
222+
{
223+
Debug.LogWarning($"Could not read property {propName} on {componentType.Name}: {ex.Message}");
224+
}
225+
}
226+
227+
// Use cached fields
228+
foreach (var fieldInfo in cachedData.SerializableFields)
229+
{
230+
try
231+
{
232+
object value = fieldInfo.GetValue(c);
233+
string fieldName = fieldInfo.Name;
234+
Type fieldType = fieldInfo.FieldType;
235+
AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
236+
}
237+
catch (Exception ex)
238+
{
239+
Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}: {ex.Message}");
240+
}
241+
}
242+
// --- End Use cached metadata ---
243+
244+
if (serializablePropertiesOutput.Count > 0)
245+
{
246+
data["properties"] = serializablePropertiesOutput;
247+
}
248+
249+
return data;
250+
}
251+
252+
// Helper function to decide how to serialize different types
253+
private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value)
254+
{
255+
// Simplified: Directly use CreateTokenFromValue which uses the serializer
256+
if (value == null)
257+
{
258+
dict[name] = null;
259+
return;
260+
}
261+
262+
try
263+
{
264+
// Use the helper that employs our custom serializer settings
265+
JToken token = CreateTokenFromValue(value, type);
266+
if (token != null) // Check if serialization succeeded in the helper
267+
{
268+
// Convert JToken back to a basic object structure for the dictionary
269+
dict[name] = ConvertJTokenToPlainObject(token);
270+
}
271+
// If token is null, it means serialization failed and a warning was logged.
272+
}
273+
catch (Exception e)
274+
{
275+
// Catch potential errors during JToken conversion or addition to dictionary
276+
Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
277+
}
278+
}
279+
280+
// Helper to convert JToken back to basic object structure
281+
private static object ConvertJTokenToPlainObject(JToken token)
282+
{
283+
if (token == null) return null;
284+
285+
switch (token.Type)
286+
{
287+
case JTokenType.Object:
288+
var objDict = new Dictionary<string, object>();
289+
foreach (var prop in ((JObject)token).Properties())
290+
{
291+
objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
292+
}
293+
return objDict;
294+
295+
case JTokenType.Array:
296+
var list = new List<object>();
297+
foreach (var item in (JArray)token)
298+
{
299+
list.Add(ConvertJTokenToPlainObject(item));
300+
}
301+
return list;
302+
303+
case JTokenType.Integer:
304+
return token.ToObject<long>(); // Use long for safety
305+
case JTokenType.Float:
306+
return token.ToObject<double>(); // Use double for safety
307+
case JTokenType.String:
308+
return token.ToObject<string>();
309+
case JTokenType.Boolean:
310+
return token.ToObject<bool>();
311+
case JTokenType.Date:
312+
return token.ToObject<DateTime>();
313+
case JTokenType.Guid:
314+
return token.ToObject<Guid>();
315+
case JTokenType.Uri:
316+
return token.ToObject<Uri>();
317+
case JTokenType.TimeSpan:
318+
return token.ToObject<TimeSpan>();
319+
case JTokenType.Bytes:
320+
return token.ToObject<byte[]>();
321+
case JTokenType.Null:
322+
return null;
323+
case JTokenType.Undefined:
324+
return null; // Treat undefined as null
325+
326+
default:
327+
// Fallback for simple value types not explicitly listed
328+
if (token is JValue jValue && jValue.Value != null)
329+
{
330+
return jValue.Value;
331+
}
332+
Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null.");
333+
return null;
334+
}
335+
}
336+
337+
// --- Define custom JsonSerializerSettings for OUTPUT ---
338+
private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
339+
{
340+
Converters = new List<JsonConverter>
341+
{
342+
new Vector3Converter(),
343+
new Vector2Converter(),
344+
new QuaternionConverter(),
345+
new ColorConverter(),
346+
new RectConverter(),
347+
new BoundsConverter(),
348+
new UnityEngineObjectConverter() // Handles serialization of references
349+
},
350+
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
351+
// ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
352+
};
353+
private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings);
354+
// --- End Define custom JsonSerializerSettings ---
355+
356+
// Helper to create JToken using the output serializer
357+
private static JToken CreateTokenFromValue(object value, Type type)
358+
{
359+
if (value == null) return JValue.CreateNull();
360+
361+
try
362+
{
363+
// Use the pre-configured OUTPUT serializer instance
364+
return JToken.FromObject(value, _outputSerializer);
365+
}
366+
catch (JsonSerializationException e)
367+
{
368+
Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
369+
return null; // Indicate serialization failure
370+
}
371+
catch (Exception e) // Catch other unexpected errors
372+
{
373+
Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
374+
return null; // Indicate serialization failure
375+
}
376+
}
377+
}
378+
}

0 commit comments

Comments
 (0)