Skip to content

Commit 5ffd3ea

Browse files
authored
Merge pull request #470 from Unity-Technologies/UT-2054-fix-references-in-scene-objects
Ut 2054 fix references in scene objects
2 parents ce0b734 + 021c216 commit 5ffd3ea

File tree

2 files changed

+204
-5
lines changed

2 files changed

+204
-5
lines changed

com.unity.formats.fbx.tests/Tests/FbxTests/ConvertToModelTest.cs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,56 @@ protected void AssertSameMeshesAndMaterials(GameObject expectedHierarchy, GameOb
128128
}
129129
}
130130

131+
[Test]
132+
public void TestReferencesInScene()
133+
{
134+
// test that references that scene objects hold to the converted object
135+
// are maintained
136+
var a = GameObject.CreatePrimitive(PrimitiveType.Cube);
137+
var b = GameObject.CreatePrimitive(PrimitiveType.Sphere);
138+
b.transform.SetParent(a.transform);
139+
140+
var c = new GameObject();
141+
var reference = c.AddComponent<ReferenceComponent>();
142+
reference.m_goList = new GameObject[] { a, b };
143+
reference.m_collider = a.GetComponent<BoxCollider>();
144+
reference.m_transform = b.transform;
145+
146+
var fbxPath = GetRandomFbxFilePath();
147+
148+
// Convert it to a prefab
149+
var prefab = ConvertToNestedPrefab.Convert(a,
150+
fbxFullPath: fbxPath, prefabFullPath: Path.ChangeExtension(fbxPath, "prefab"));
151+
Assert.That(prefab);
152+
Assert.That(!a);
153+
154+
var newA = prefab;
155+
var newB = prefab.transform.GetChild(0).gameObject;
156+
157+
Assert.That(reference.m_goList.Length, Is.EqualTo(2));
158+
Assert.That(reference.m_goList[0], Is.EqualTo(newA));
159+
Assert.That(reference.m_goList[1], Is.EqualTo(newB));
160+
Assert.That(reference.m_collider, Is.EqualTo(newA.GetComponent<BoxCollider>()));
161+
Assert.That(reference.m_transform, Is.EqualTo(newB.transform));
162+
163+
// Test undo and redo still maintains the right references
164+
Undo.PerformUndo();
165+
166+
Assert.That(reference.m_goList.Length, Is.EqualTo(2));
167+
Assert.That(reference.m_goList[0], Is.EqualTo(a));
168+
Assert.That(reference.m_goList[1], Is.EqualTo(b));
169+
Assert.That(reference.m_collider, Is.EqualTo(a.GetComponent<BoxCollider>()));
170+
Assert.That(reference.m_transform, Is.EqualTo(b.transform));
171+
172+
Undo.PerformRedo();
173+
174+
Assert.That(reference.m_goList.Length, Is.EqualTo(2));
175+
Assert.That(reference.m_goList[0], Is.EqualTo(newA));
176+
Assert.That(reference.m_goList[1], Is.EqualTo(newB));
177+
Assert.That(reference.m_collider, Is.EqualTo(newA.GetComponent<BoxCollider>()));
178+
Assert.That(reference.m_transform, Is.EqualTo(newB.transform));
179+
}
180+
131181
[Test]
132182
public void TestReferences()
133183
{
@@ -357,7 +407,7 @@ public void TestStaticHelpers()
357407
Assert.AreEqual(Vector3.zero, b.transform.localPosition);
358408
Assert.AreNotEqual (a.GetComponent<MeshFilter>().sharedMesh, b.GetComponent<MeshFilter> ().sharedMesh);
359409
var nameMap = ConvertToNestedPrefab.MapNameToSourceRecursive(b, a);
360-
ConvertToNestedPrefab.CopyComponents(b, a, nameMap);
410+
ConvertToNestedPrefab.CopyComponents(b, a, a, nameMap);
361411
Assert.IsTrue(b.GetComponent<BoxCollider>());
362412
Assert.AreEqual(a.transform.localPosition, b.transform.localPosition);
363413
Assert.AreNotEqual (a.GetComponent<MeshFilter>().sharedMesh, b.GetComponent<MeshFilter> ().sharedMesh);
@@ -450,6 +500,37 @@ public void TestStaticHelpers()
450500

451501
Assert.That(bReferenceComponent.m_transform.name, Is.EqualTo(a.transform.name));
452502
}
503+
504+
// Test GetSceneReferencesToObject()
505+
{
506+
var a = GameObject.CreatePrimitive(PrimitiveType.Cube);
507+
var b = new GameObject();
508+
var c = new GameObject();
509+
510+
var reference = b.AddComponent<ReferenceComponent>();
511+
var constraint = c.AddComponent<UnityEngine.Animations.PositionConstraint>();
512+
513+
reference.m_collider = a.GetComponent<BoxCollider>();
514+
515+
var constraintSource = new UnityEngine.Animations.ConstraintSource();
516+
constraintSource.sourceTransform = a.transform;
517+
constraintSource.weight = 0.5f;
518+
constraint.AddSource(constraintSource);
519+
520+
var sceneRefs = ConvertToNestedPrefab.GetSceneReferencesToObject(a);
521+
Assert.That(sceneRefs.Count, Is.EqualTo(2));
522+
Assert.That(sceneRefs.Contains(a)); // GameObjects also reference themself
523+
Assert.That(sceneRefs.Contains(b));
524+
525+
sceneRefs = ConvertToNestedPrefab.GetSceneReferencesToObject(a.GetComponent<BoxCollider>());
526+
Assert.That(sceneRefs.Count, Is.EqualTo(1));
527+
Assert.That(sceneRefs.Contains(b));
528+
529+
sceneRefs = ConvertToNestedPrefab.GetSceneReferencesToObject(a.transform);
530+
Assert.That(sceneRefs.Count, Is.EqualTo(2));
531+
Assert.That(sceneRefs.Contains(b));
532+
Assert.That(sceneRefs.Contains(c));
533+
}
453534
}
454535

455536
[Test]

com.unity.formats.fbx/Editor/ConvertToNestedPrefab.cs

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,112 @@ public static GameObject[] CreateInstantiatedModelPrefab(
207207
}
208208
return converted.ToArray();
209209
}
210+
211+
/// <summary>
212+
/// For all scene objects holding a reference to origObj, replaces the references to newObj.
213+
///
214+
/// If one of the scene objects is toConvertRoot or a child of it, then do not fix its references as it
215+
/// will be deleted after conversion.
216+
/// </summary>
217+
/// <param name="origObj"></param>
218+
/// <param name="newObj"></param>
219+
/// <param name="toConvertRoot"></param>
220+
internal static void FixSceneReferences(Object origObj, Object newObj, GameObject toConvertRoot)
221+
{
222+
var sceneObjs = GetSceneReferencesToObject(origObj);
223+
224+
// try to fix references on each component of each scene object, if applicable
225+
foreach(var sceneObj in sceneObjs)
226+
{
227+
var go = ModelExporter.GetGameObject(sceneObj);
228+
if (go && go.transform.IsChildOf(toConvertRoot.transform))
229+
{
230+
// if this is a child of what we are converting, don't update its references.
231+
continue;
232+
}
233+
234+
var components = sceneObj.GetComponents<Component>();
235+
foreach(var component in components)
236+
{
237+
var serializedComponent = new SerializedObject(component);
238+
var property = serializedComponent.GetIterator();
239+
property.Next(true); // skip generic field
240+
// For SkinnedMeshRenderer, the bones array doesn't have visible children, but may have references that need to be fixed.
241+
// For everything else, filtering by visible children in the while loop and then copying properties that don't have visible children,
242+
// ensures that only the leaf properties are copied over. Copying other properties is not usually necessary and may break references that
243+
// were not meant to be copied.
244+
while (property.Next((component is SkinnedMeshRenderer) ? property.hasChildren : property.hasVisibleChildren))
245+
{
246+
if (!property.hasVisibleChildren)
247+
{
248+
// with Undo operations, copying m_Father reference causes issues. Also, it is not required as the reference is fixed when
249+
// the transform is parented under the correct hierarchy (which happens before this).
250+
if (property.propertyType == SerializedPropertyType.ObjectReference && property.propertyPath != "m_GameObject" &&
251+
property.propertyPath != "m_Father" && property.objectReferenceValue &&
252+
(property.objectReferenceValue == origObj))
253+
{
254+
property.objectReferenceValue = newObj;
255+
serializedComponent.ApplyModifiedProperties();
256+
}
257+
}
258+
}
259+
}
260+
}
261+
}
262+
263+
/// <summary>
264+
/// Helper for getting a property from an instance with reflection.
265+
/// </summary>
266+
/// <param name="instance"></param>
267+
/// <param name="propertyName"></param>
268+
/// <param name="isPublic"></param>
269+
/// <returns></returns>
270+
private static object GetPropertyReflection(object instance, string propertyName, bool isPublic)
271+
{
272+
return instance.GetType().GetProperty(propertyName, (isPublic ? System.Reflection.BindingFlags.Public : System.Reflection.BindingFlags.NonPublic) |
273+
System.Reflection.BindingFlags.Instance).GetValue(instance, null);
274+
}
275+
276+
/// <summary>
277+
/// Returns a list of GameObjects in the scene that contain references to the given object.
278+
/// </summary>
279+
/// <param name="obj"></param>
280+
/// <returns></returns>
281+
internal static List<GameObject> GetSceneReferencesToObject(Object obj)
282+
{
283+
var sceneHierarchyWindowType = typeof(UnityEditor.SearchableEditorWindow).Assembly.GetType("UnityEditor.SceneHierarchyWindow");
284+
var sceneHierarchyWindow = EditorWindow.GetWindow(sceneHierarchyWindowType);
285+
var instanceID = obj.GetInstanceID();
286+
var idFormat = "ref:{0}:";
287+
288+
var sceneHierarchy = GetPropertyReflection(sceneHierarchyWindow, "sceneHierarchy", isPublic: true);
289+
var previousSearchFilter = sceneHierarchy.GetType().GetField("m_SearchFilter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(sceneHierarchy);
290+
291+
// Set the search filter to find all references in the scene to the given object
292+
var setSearchFilterMethod = sceneHierarchyWindowType.GetMethod("SetSearchFilter", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
293+
setSearchFilterMethod.Invoke(sceneHierarchyWindow, new object[] { string.Format(idFormat, instanceID), SearchableEditorWindow.SearchMode.All, true, false });
294+
295+
// Get objects from list of instance IDs of currently visible objects
296+
var treeView = GetPropertyReflection(sceneHierarchy, "treeView", isPublic: false);
297+
var data = GetPropertyReflection(treeView, "data", isPublic: true);
298+
var getRows = data.GetType().GetMethod("GetRows");
299+
var rows = getRows.Invoke(data, null) as IEnumerable;
300+
301+
var sceneObjects = new List<GameObject>();
302+
foreach (var row in rows)
303+
{
304+
var id = (int)GetPropertyReflection(row, "id", isPublic: true);
305+
var gameObject = EditorUtility.InstanceIDToObject(id) as GameObject;
306+
if (gameObject)
307+
{
308+
sceneObjects.Add(gameObject);
309+
}
310+
}
311+
312+
// remove the filter when done
313+
setSearchFilterMethod.Invoke(sceneHierarchyWindow, new object[] { previousSearchFilter, SearchableEditorWindow.SearchMode.Name, true, false });
314+
return sceneObjects;
315+
}
210316

211317
/// <summary>
212318
/// Convert one object (and the hierarchy below it) to a prefab variant of a model prefab.
@@ -626,7 +732,8 @@ internal static void UpdateFromSourceRecursive(GameObject dest, GameObject sourc
626732
PrefabUtility.UnpackPrefabInstance(sourceGO, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
627733
}
628734

629-
CopyComponents(destGO, sourceGO, goDict);
735+
FixSceneReferences(sourceGO, destGO, source);
736+
CopyComponents(destGO, sourceGO, source, goDict);
630737

631738
// also make sure GameObject properties, such as tag and layer
632739
// are copied over as well
@@ -741,8 +848,10 @@ internal static void CopySerializedProperty(SerializedObject serializedObject, S
741848
/// are already in the FBX.
742849
///
743850
/// The 'from' hierarchy is not modified.
851+
///
852+
/// Note: 'root' is the root object that is being converted
744853
/// </summary>
745-
internal static void CopyComponents(GameObject to, GameObject from, Dictionary<string, GameObject> nameMap)
854+
internal static void CopyComponents(GameObject to, GameObject from, GameObject root, Dictionary<string, GameObject> nameMap)
746855
{
747856
// copy components on "from" to "to". Don't want to copy over meshes and materials that were exported
748857
var originalComponents = new List<Component>(from.GetComponents<Component>());
@@ -755,8 +864,15 @@ internal static void CopyComponents(GameObject to, GameObject from, Dictionary<s
755864
continue;
756865
}
757866

758-
// ignore MeshFilter and FbxPrefab (when converting LinkedPrefabs)
759-
if (fromComponent is MeshFilter || fromComponent is UnityEngine.Formats.Fbx.Exporter.FbxPrefab)
867+
// ignore MeshFilter, but still ensure scene references are maintained
868+
if (fromComponent is MeshFilter)
869+
{
870+
FixSceneReferences(fromComponent, to.GetComponent<MeshFilter>(), root);
871+
continue;
872+
}
873+
874+
// ignore FbxPrefab (when converting LinkedPrefabs)
875+
if (fromComponent is UnityEngine.Formats.Fbx.Exporter.FbxPrefab)
760876
{
761877
continue;
762878
}
@@ -804,6 +920,8 @@ internal static void CopyComponents(GameObject to, GameObject from, Dictionary<s
804920
toComponent = to.AddComponent(fromComponent.GetType());
805921
}
806922

923+
FixSceneReferences(fromComponent, toComponent, root);
924+
807925
// Do not try to copy materials for ParticleSystemRenderer, since it is not in the
808926
// FBX file
809927
if (fromComponent is Renderer && !(fromComponent is ParticleSystemRenderer))

0 commit comments

Comments
 (0)