Example #1
0
        /// <summary>
        /// Tests saving, loading and updating of prefabs.
        /// </summary>
        private static void UnitTest4_Prefabs()
        {
            if (!EditorApplication.IsProjectLoaded)
            {
                Debug.LogWarning("Skipping unit test as no project is loaded.");
                return;
            }

            if (EditorApplication.IsSceneModified())
            {
                Debug.LogWarning("Cannot perform unit test as the current scene is modified.");
                return;
            }

            Action PrintSceneState = () =>
            {
                SceneObject root = Scene.Root;

                Stack <SceneObject> todo = new Stack <SceneObject>();
                todo.Push(root);

                StringBuilder output = new StringBuilder();
                while (todo.Count > 0)
                {
                    SceneObject so = todo.Pop();

                    int numChildren = so.GetNumChildren();
                    for (int i = numChildren - 1; i >= 0; i--)
                    {
                        SceneObject child = so.GetChild(i);

                        output.AppendLine(child.Name);
                        todo.Push(child);
                    }
                }

                Debug.Log(output);
            };

            // Disabled because it's a slow test, enable only when relevant (or when a build machine is set up)
            return;

            string oldScene = Scene.ActiveSceneUUID;

            Scene.Clear();

            try
            {
                // Simple scene save & load
                {
                    {
                        // unitTest4Scene_0.prefab:
                        // so0 (Comp1)
                        //  - so0_0
                        //  - so0_1 (Comp1)
                        //    - so0_1_0 (Comp1)
                        // so1 (Comp2)
                        //  - so1_0

                        SceneObject so0     = new SceneObject("so0");
                        SceneObject so1     = new SceneObject("so1");
                        SceneObject so0_0   = new SceneObject("so0_0");
                        SceneObject so0_1   = new SceneObject("so0_1");
                        SceneObject so1_0   = new SceneObject("so1_0");
                        SceneObject so0_1_0 = new SceneObject("so0_1_0");

                        so0_0.Parent   = so0;
                        so0_1.Parent   = so0;
                        so1_0.Parent   = so1;
                        so0_1_0.Parent = so0_1;

                        so0_1_0.LocalPosition = new Vector3(10.0f, 15.0f, 20.0f);
                        so0_1.LocalPosition   = new Vector3(1.0f, 2.0f, 3.0f);
                        so1_0.LocalPosition   = new Vector3(0, 123.0f, 0.0f);

                        UT1_Component1 comp0     = so0.AddComponent <UT1_Component1>();
                        UT1_Component2 comp1     = so1.AddComponent <UT1_Component2>();
                        UT1_Component1 comp1_1   = so0_1.AddComponent <UT1_Component1>();
                        UT1_Component1 comp0_1_0 = so0_1_0.AddComponent <UT1_Component1>();

                        comp0.otherSO        = so0_1_0;
                        comp0.otherComponent = comp1;

                        comp1_1.b = "originalValue2";

                        comp0_1_0.b               = "testValue";
                        comp0_1_0.otherSO         = so0;
                        comp0_1_0.otherComponent2 = comp0;

                        EditorApplication.SaveScene("unitTest4Scene_0.prefab");
                    }
                    {
                        EditorApplication.LoadScene("unitTest4Scene_0.prefab");

                        SceneObject sceneRoot = Scene.Root;
                        SceneObject so0       = sceneRoot.FindChild("so0", false);
                        SceneObject so1       = sceneRoot.FindChild("so1", false);
                        SceneObject so0_0     = so0.FindChild("so0_0", false);
                        SceneObject so0_1     = so0.FindChild("so0_1", false);
                        SceneObject so0_1_0   = so0_1.FindChild("so0_1_0", false);

                        Assert(so0_0 != null);
                        Assert(so0_1 != null);
                        Assert(so0_1_0 != null);

                        UT1_Component1 comp0     = so0.GetComponent <UT1_Component1>();
                        UT1_Component2 comp1     = so1.GetComponent <UT1_Component2>();
                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();

                        Assert(comp0 != null);
                        Assert(comp1 != null);
                        Assert(comp0_1_0 != null);
                        Assert(comp0_1_0.b == "testValue");
                        Assert(comp0.otherSO == so0_1_0);
                        Assert(comp0.otherComponent == comp1);
                        Assert(comp0_1_0.otherSO == so0);
                        Assert(comp0_1_0.otherComponent2 == comp0);
                    }
                }

                Debug.Log("Passed stage 1");

                // Load & save a scene that contains a prefab and references its objects
                {
                    {
                        // unitTest4Scene_1.prefab:
                        // parentSO0
                        //  - [unitTest4Scene_0.prefab]
                        // parentSO1
                        //  - parentSO1_0 (Comp1)

                        Scene.Clear();

                        SceneObject parentSO0   = new SceneObject("parentSO0", false);
                        SceneObject parentSO1   = new SceneObject("parentSO1", false);
                        SceneObject parentSO1_0 = new SceneObject("parentSO1_0", false);

                        parentSO1_0.Parent      = parentSO1;
                        parentSO0.LocalPosition = new Vector3(50.0f, 50.0f, 50.0f);

                        UT1_Component1 parentComp1_0 = parentSO1_0.AddComponent <UT1_Component1>();

                        Prefab      scene0Prefab   = ProjectLibrary.Load <Prefab>("unitTest4Scene_0.prefab");
                        SceneObject prefabInstance = scene0Prefab.Instantiate();
                        prefabInstance.Parent        = parentSO0;
                        prefabInstance.LocalPosition = Vector3.Zero;

                        SceneObject so0     = prefabInstance.FindChild("so0", false);
                        SceneObject so1     = prefabInstance.FindChild("so1", false);
                        SceneObject so0_1   = so0.FindChild("so0_1", false);
                        SceneObject so1_0   = so1.FindChild("so1_0", false);
                        SceneObject so0_1_0 = so0_1.FindChild("so0_1_0", false);

                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();

                        parentComp1_0.otherSO         = so1_0;
                        parentComp1_0.otherComponent2 = comp0_1_0;

                        EditorApplication.SaveScene("unitTest4Scene_1.prefab");
                    }
                    {
                        EditorApplication.LoadScene("unitTest4Scene_1.prefab");

                        SceneObject parentSO0   = Scene.Root.FindChild("parentSO0", false);
                        SceneObject parentSO1   = Scene.Root.FindChild("parentSO1", false);
                        SceneObject parentSO1_0 = parentSO1.FindChild("parentSO1_0", false);

                        UT1_Component1 parentComp1_0 = parentSO1_0.GetComponent <UT1_Component1>();

                        SceneObject prefabInstance = parentSO0.GetChild(0);
                        SceneObject so0            = prefabInstance.FindChild("so0", false);
                        SceneObject so1            = prefabInstance.FindChild("so1", false);
                        SceneObject so0_1          = so0.FindChild("so0_1", false);
                        SceneObject so1_0          = so1.FindChild("so1_0", false);
                        SceneObject so0_1_0        = so0_1.FindChild("so0_1_0", false);

                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();

                        Assert(parentComp1_0.otherSO == so1_0);
                        Assert(parentComp1_0.otherComponent2 == comp0_1_0);
                    }
                }

                Debug.Log("Passed stage 2");

                // Modify prefab, reload the scene and ensure it is updated with modified prefab
                {
                    {
                        // unitTest4Scene_0.prefab:
                        // so0
                        //  - so0_1 (Comp1)
                        //    - so0_1_0 (Comp1)
                        // so1 (Comp1, Comp2)
                        //  - so1_0
                        //  - so1_1

                        Scene.Load("unitTest4Scene_0.prefab");

                        SceneObject sceneRoot = Scene.Root;
                        SceneObject so0       = sceneRoot.FindChild("so0", false);
                        SceneObject so0_0     = so0.FindChild("so0_0", false);
                        SceneObject so0_1     = so0.FindChild("so0_1", false);
                        SceneObject so1       = sceneRoot.FindChild("so1", false);
                        SceneObject so1_0     = so1.FindChild("so1_0", false);
                        SceneObject so0_1_0   = so0_1.FindChild("so0_1_0", false);

                        SceneObject so1_1 = new SceneObject("so1_1");
                        so1_1.Parent = so1;

                        so0.RemoveComponent <UT1_Component1>();
                        UT1_Component1 comp1     = so1.AddComponent <UT1_Component1>();
                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();

                        so0_0.Destroy();

                        comp1.otherSO         = so1_0;
                        comp1.otherComponent2 = comp0_1_0;

                        comp0_1_0.otherSO         = so1_1;
                        comp0_1_0.otherComponent2 = comp1;
                        comp0_1_0.a = 123;
                        comp0_1_0.b = "modifiedValue";

                        so1.Name          = "so1_modified";
                        so1.LocalPosition = new Vector3(0, 999.0f, 0.0f);

                        EditorApplication.SaveScene("unitTest4Scene_0.prefab");
                    }

                    {
                        EditorApplication.LoadScene("unitTest4Scene_1.prefab");

                        SceneObject parentSO0   = Scene.Root.FindChild("parentSO0", false);
                        SceneObject parentSO1   = Scene.Root.FindChild("parentSO1", false);
                        SceneObject parentSO1_0 = parentSO1.FindChild("parentSO1_0", false);

                        UT1_Component1 parentComp1_0 = parentSO1_0.GetComponent <UT1_Component1>();

                        SceneObject prefabInstance = parentSO0.GetChild(0);
                        SceneObject so0            = prefabInstance.FindChild("so0", false);
                        SceneObject so1            = prefabInstance.FindChild("so1_modified", false);
                        SceneObject so0_0          = so0.FindChild("so0_0", false);
                        SceneObject so0_1          = so0.FindChild("so0_1", false);
                        SceneObject so1_0          = so1.FindChild("so1_0", false);
                        SceneObject so0_1_0        = so0_1.FindChild("so0_1_0", false);
                        SceneObject so1_1          = so1.FindChild("so1_1", false);

                        UT1_Component1 comp0     = so0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp1     = so1.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();

                        Assert(parentComp1_0.otherSO == so1_0);
                        Assert(parentComp1_0.otherComponent2 == comp0_1_0);
                        Assert(so1_1 != null);
                        Assert(so0_0 == null);
                        Assert(comp0 == null);
                        Assert(comp0_1_0.otherSO == so1_1);
                        Assert(comp0_1_0.otherComponent2 == comp1);
                        Assert(comp0_1_0.a == 123);
                        Assert(comp0_1_0.b == "modifiedValue");
                        Assert(comp1.otherSO == so1_0);
                        Assert(comp1.otherComponent2 == comp0_1_0);
                        Assert(MathEx.ApproxEquals(so1.LocalPosition.y, 999.0f));
                    }
                }

                Debug.Log("Passed stage 3");

                // Make instance specific changes to the prefab, modify the prefab itself and ensure
                // both changes persist
                {
                    // Create new scene referencing the prefab and make instance modifications
                    {
                        // unitTest4Scene_2.prefab:
                        // parent2SO0
                        //  - [unitTest4Scene_0.prefab]
                        // parent2SO1
                        //  - parent2SO1_0 (Comp1)

                        // unitTest4Scene_0.prefab (unitTest4Scene_2.prefab instance):
                        // so0 (Comp1(INSTANCE))
                        //  - so0_0 (INSTANCE)
                        //  - so0_1 (Comp1)
                        //    - so0_1_0 (Comp1)
                        // so1 (Comp2)
                        //  - so1_0

                        Scene.Clear();

                        SceneObject parent2SO0   = new SceneObject("parent2SO0");
                        SceneObject parent2SO1   = new SceneObject("parent2SO1");
                        SceneObject parent2SO1_0 = new SceneObject("parent2SO1_0");

                        parent2SO1_0.Parent = parent2SO1;

                        UT1_Component1 parentComp1_0 = parent2SO1_0.AddComponent <UT1_Component1>();

                        Prefab      scene0Prefab   = ProjectLibrary.Load <Prefab>("unitTest4Scene_0.prefab");
                        SceneObject prefabInstance = scene0Prefab.Instantiate();
                        prefabInstance.Parent = parent2SO0;

                        SceneObject so0 = prefabInstance.FindChild("so0", false);
                        SceneObject so1 = prefabInstance.FindChild("so1_modified", false);

                        SceneObject so0_1   = so0.FindChild("so0_1", false);
                        SceneObject so1_0   = so1.FindChild("so1_0", false);
                        SceneObject so1_1   = so1.FindChild("so1_1", false);
                        SceneObject so0_1_0 = so0_1.FindChild("so0_1_0", false);

                        UT1_Component2 comp1     = so1.GetComponent <UT1_Component2>();
                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0_1   = so0_1.GetComponent <UT1_Component1>();

                        SceneObject so0_0 = new SceneObject("so0_0");
                        so0_0.Parent = so0;
                        UT1_Component1 comp0 = so0.AddComponent <UT1_Component1>();

                        so1.RemoveComponent <UT1_Component1>();
                        so1_1.Destroy();

                        comp0.otherSO        = so0_1_0;
                        comp0.otherComponent = comp1;

                        parentComp1_0.otherSO         = so1_0;
                        parentComp1_0.otherComponent2 = comp0_1_0;

                        comp0_1_0.otherSO         = parent2SO1_0;
                        comp0_1_0.otherComponent2 = parentComp1_0;
                        comp0_1_0.b = "instanceValue";

                        comp0_1.b = "instanceValue2";

                        EditorApplication.SaveScene("unitTest4Scene_2.prefab");
                    }

                    Debug.Log("Passed stage 4.1");

                    // Reload the scene and ensure instance modifications remain
                    {
                        EditorApplication.LoadScene("unitTest4Scene_2.prefab");

                        SceneObject root         = Scene.Root;
                        SceneObject parent2SO0   = root.FindChild("parent2SO0", false);
                        SceneObject parent2SO1   = root.FindChild("parent2SO1", false);
                        SceneObject parent2SO1_0 = parent2SO1.FindChild("parent2SO1_0", false);

                        SceneObject prefabInstance = parent2SO0.GetChild(0);

                        SceneObject so0     = prefabInstance.FindChild("so0", false);
                        SceneObject so1     = prefabInstance.FindChild("so1_modified", false);
                        SceneObject so0_0   = so0.FindChild("so0_0", false);
                        SceneObject so0_1   = so0.FindChild("so0_1", false);
                        SceneObject so1_0   = so1.FindChild("so1_0", false);
                        SceneObject so1_1   = so1.FindChild("so1_1", false);
                        SceneObject so0_1_0 = so0_1.FindChild("so0_1_0", false);

                        UT1_Component1 parentComp1_0 = parent2SO1_0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0         = so0.GetComponent <UT1_Component1>();
                        UT1_Component2 comp1         = so1.GetComponent <UT1_Component2>();
                        UT1_Component1 comp11        = so1.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0_1_0     = so0_1_0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0_1       = so0_1.GetComponent <UT1_Component1>();

                        Assert(so0_0 != null);
                        Assert(comp0 != null);
                        Assert(so1_1 == null);
                        Assert(comp11 == null);

                        Assert(comp0.otherSO == so0_1_0);
                        Assert(comp0.otherComponent == comp1);

                        Assert(parentComp1_0.otherSO == so1_0);
                        Assert(parentComp1_0.otherComponent2 == comp0_1_0);

                        Debug.Log(comp0_1_0.otherSO == null);
                        if (comp0_1_0.otherSO != null)
                        {
                            Debug.Log(comp0_1_0.otherSO.InstanceId + " - " + parent2SO1_0.InstanceId);
                        }

                        Assert(comp0_1_0.otherSO == parent2SO1_0);
                        Assert(comp0_1_0.otherComponent2 == parentComp1_0);
                        Assert(comp0_1_0.b == "instanceValue");

                        Assert(comp0_1.b == "instanceValue2");
                    }

                    Debug.Log("Passed stage 4.2");

                    // Load original scene and ensure instance modifications didn't influence it
                    {
                        EditorApplication.LoadScene("unitTest4Scene_1.prefab");

                        SceneObject parentSO0   = Scene.Root.FindChild("parentSO0", false);
                        SceneObject parentSO1   = Scene.Root.FindChild("parentSO1", false);
                        SceneObject parentSO1_0 = parentSO1.FindChild("parentSO1_0", false);

                        UT1_Component1 parentComp1_0 = parentSO1_0.GetComponent <UT1_Component1>();

                        SceneObject prefabInstance = parentSO0.GetChild(0);
                        SceneObject so0            = prefabInstance.FindChild("so0", false);
                        SceneObject so1            = prefabInstance.FindChild("so1_modified", false);
                        SceneObject so0_0          = so0.FindChild("so0_0", false);
                        SceneObject so0_1          = so0.FindChild("so0_1", false);
                        SceneObject so1_0          = so1.FindChild("so1_0", false);
                        SceneObject so0_1_0        = so0_1.FindChild("so0_1_0", false);
                        SceneObject so1_1          = so1.FindChild("so1_1", false);

                        UT1_Component1 comp0     = so0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp1     = so1.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0_1   = so0_1.GetComponent <UT1_Component1>();

                        Assert(parentComp1_0.otherSO == so1_0);
                        Assert(parentComp1_0.otherComponent2 == comp0_1_0);
                        Assert(so1_1 != null);
                        Assert(so0_0 == null);
                        Assert(comp0 == null);
                        Assert(comp0_1_0.otherSO == so1_1);
                        Assert(comp0_1_0.otherComponent2 == comp1);
                        Assert(comp0_1_0.a == 123);
                        Assert(comp0_1_0.b == "modifiedValue");
                        Assert(comp1.otherSO == so1_0);
                        Assert(comp1.otherComponent2 == comp0_1_0);
                        Assert(comp0_1.b == "originalValue2");
                        Assert(MathEx.ApproxEquals(so1.LocalPosition.y, 999.0f));
                    }

                    Debug.Log("Passed stage 4.3");

                    // Modify prefab and ensure both prefab and instance modifications remain
                    {
                        // unitTest4Scene_0.prefab:
                        // so0 (Comp1)
                        //  - so0_1
                        //    - so0_1_0 (Comp1)
                        // so1 (Comp1, Comp2)
                        //  - so1_1
                        //  - so1_2 (Comp1)

                        // unitTest4Scene_0.prefab (unitTest4Scene_2.prefab instance):
                        // so0 (Comp1)
                        //  - so0_0
                        //  - so0_1 (Comp1)
                        //    - so0_1_0 (Comp1)
                        // so1 (Comp2)
                        //  - so1_2 (Comp1)

                        Scene.Load("unitTest4Scene_0.prefab");

                        SceneObject sceneRoot = Scene.Root;
                        SceneObject so0       = sceneRoot.FindChild("so0", false);
                        SceneObject so0_1     = so0.FindChild("so0_1", false);
                        SceneObject so1       = sceneRoot.FindChild("so1_modified", false);
                        SceneObject so1_0     = so1.FindChild("so1_0", false);
                        SceneObject so0_1_0   = so0_1.FindChild("so0_1_0", false);

                        SceneObject so1_2 = new SceneObject("so1_2");
                        so1_2.Parent = so1;

                        so0.AddComponent <UT1_Component1>();
                        so0_1.RemoveComponent <UT1_Component1>();
                        so1_0.Destroy();

                        UT1_Component1 comp3     = so1_2.AddComponent <UT1_Component1>();
                        UT1_Component1 comp0_1_0 = so0_1_0.GetComponent <UT1_Component1>();
                        comp0_1_0.b = "modifiedValueAgain";
                        so1.Name    = "so1_modifiedAgain";

                        comp3.otherSO         = so0_1;
                        comp3.otherComponent2 = comp0_1_0;

                        EditorApplication.SaveScene("unitTest4Scene_0.prefab");
                    }

                    Debug.Log("Passed stage 4.4");

                    // Reload the scene and ensure both instance and prefab modifications remain
                    {
                        EditorApplication.LoadScene("unitTest4Scene_2.prefab");

                        SceneObject root         = Scene.Root;
                        SceneObject parent2SO0   = root.FindChild("parent2SO0", false);
                        SceneObject parent2SO1   = root.FindChild("parent2SO1", false);
                        SceneObject parent2SO1_0 = parent2SO1.FindChild("parent2SO1_0", false);

                        SceneObject prefabInstance = parent2SO0.GetChild(0);

                        SceneObject so0     = prefabInstance.FindChild("so0", false);
                        SceneObject so1     = prefabInstance.FindChild("so1_modifiedAgain", false);
                        SceneObject so0_0   = so0.FindChild("so0_0", false);
                        SceneObject so0_1   = so0.FindChild("so0_1", false);
                        SceneObject so1_0   = so1.FindChild("so1_0", false);
                        SceneObject so1_1   = so1.FindChild("so1_1", false);
                        SceneObject so1_2   = so1.FindChild("so1_2", false);
                        SceneObject so0_1_0 = so0_1.FindChild("so0_1_0", false);

                        UT1_Component1 parentComp1_0 = parent2SO1_0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0         = so0.GetComponent <UT1_Component1>();
                        UT1_Component2 comp1         = so1.GetComponent <UT1_Component2>();
                        UT1_Component1 comp11        = so1.GetComponent <UT1_Component1>();
                        UT1_Component1 comp0_1_0     = so0_1_0.GetComponent <UT1_Component1>();
                        UT1_Component1 comp3         = so1_2.AddComponent <UT1_Component1>();

                        // Check instance modifications (they should override any prefab modifications)
                        Assert(so0_0 != null);
                        Assert(comp0 != null);
                        Assert(so1_1 == null);
                        Assert(comp11 == null);

                        Assert(comp0.otherSO == so0_1_0);
                        Assert(comp0.otherComponent == comp1);

                        Assert(parentComp1_0.otherSO == so1_0);
                        Assert(parentComp1_0.otherComponent2 == comp0_1_0);

                        Assert(comp0_1_0.otherSO == parent2SO1_0);
                        Assert(comp0_1_0.otherComponent2 == parentComp1_0);
                        Assert(comp0_1_0.b == "instanceValue");

                        // Check prefab modifications
                        Assert(so1_0 == null);
                        Assert(so1.Name == "so1_modifiedAgain");
                        Assert(comp3.otherSO == so0_1);
                        Assert(comp3.otherComponent2 == comp0_1_0);
                    }

                    Debug.Log("Passed stage 4.5");
                }
            }
            catch
            {
                PrintSceneState();

                throw;
            }
            finally
            {
                if (!string.IsNullOrEmpty(oldScene))
                {
                    Scene.Load(ProjectLibrary.GetPath(oldScene));
                }
                else
                {
                    Scene.Clear();
                }

                ProjectLibrary.Delete("unitTest4Scene_0.prefab");
                ProjectLibrary.Delete("unitTest4Scene_1.prefab");
                ProjectLibrary.Delete("unitTest4Scene_2.prefab");
            }
        }
Example #2
0
        /// <summary>
        /// Searches the scene object hierarchy to find a property at the given path.
        /// </summary>
        /// <param name="root">Root scene object to which the path is relative to.</param>
        /// <param name="path">Path to the property, where each element of the path is separated with "/".
        ///
        ///                    Path elements prefixed with "!" signify names of child scene objects (first one relative to
        ///                    <paramref name="root"/>. Name of the root element should not be included in the path.
        ///
        ///                    Path element prefixed with ":" signify names of components. If a path doesn't have a
        ///                    component element, it is assumed the field is relative to the scene object itself (only
        ///                    "Position", "Rotation" and "Scale" fields are supported in such case). Only one component
        ///                    path element per path is allowed.
        ///
        ///                    Path entries with no prefix are considered regular script object fields. Each path must have
        ///                    at least one such entry.
        ///
        ///                    A field path can be followed by an indexer [n] where n is a zero-based index. Such paths
        ///                    are assumed to be referencing an index within an array or a list.
        ///
        ///                    A field path can also be followed by a suffix (after the indexer, if any) separated from the
        ///                    path name with ".". This suffix is not parsed internally, but will be returned as
        ///                    <paramref name="suffix"/>.
        ///
        ///                    Path examples:
        ///                     :MyComponent/myInt (path to myInt variable on a component attached to the root object)
        ///                     :MyComponent/myArray[0] (path to first element of myArray on the same component as above)
        ///                     !childSO/:MyComponent/myInt (path to myInt variable on a child scene object)
        ///                     !childSO/Position (path to the scene object position)
        ///                     :MyComponent/myVector.z (path to the z component of myVector on the root object)
        /// </param>
        /// <param name="suffix">Suffix of the last field entry, if it has any. Contains the suffix separator ".".</param>
        /// <returns>If found, property object you can use for setting and getting the value from the property, otherwise
        ///          null.</returns>
        internal static SerializableProperty FindProperty(SceneObject root, string path, out string suffix)
        {
            suffix = null;

            if (string.IsNullOrEmpty(path) || root == null)
            {
                return(null);
            }

            string trimmedPath = path.Trim('/');

            string[] entries = trimmedPath.Split('/');

            // Find scene object referenced by the path
            SceneObject so      = root;
            int         pathIdx = 0;

            for (; pathIdx < entries.Length; pathIdx++)
            {
                string entry = entries[pathIdx];

                if (string.IsNullOrEmpty(entry))
                {
                    continue;
                }

                // Not a scene object, break
                if (entry[0] != '!')
                {
                    break;
                }

                string childName = entry.Substring(1, entry.Length - 1);
                so = so.FindChild(childName);

                if (so == null)
                {
                    break;
                }
            }

            // Child scene object couldn't be found
            if (so == null)
            {
                return(null);
            }

            // Path too short, no field entry
            if (pathIdx >= entries.Length)
            {
                return(null);
            }

            // Check if path is referencing a component, and if so find it
            Component component = null;

            {
                string entry = entries[pathIdx];
                if (entry[0] == ':')
                {
                    string componentName = entry.Substring(1, entry.Length - 1);

                    Component[] components = so.GetComponents();
                    component = Array.Find(components, x => x.GetType().Name == componentName);

                    // Cannot find component with specified type
                    if (component == null)
                    {
                        return(null);
                    }
                }
            }

            // Look for a field within a component
            if (component != null)
            {
                pathIdx++;
                if (pathIdx >= entries.Length)
                {
                    return(null);
                }

                SerializableObject componentObj = new SerializableObject(component);

                StringBuilder pathBuilder = new StringBuilder();
                for (; pathIdx < entries.Length - 1; pathIdx++)
                {
                    pathBuilder.Append(entries[pathIdx] + "/");
                }

                // Check last path entry for suffix and remove it
                int suffixIdx = entries[pathIdx].LastIndexOf(".");
                if (suffixIdx != -1)
                {
                    string entryNoSuffix = entries[pathIdx].Substring(0, suffixIdx);
                    suffix = entries[pathIdx].Substring(suffixIdx, entries[pathIdx].Length - suffixIdx);

                    pathBuilder.Append(entryNoSuffix);
                }
                else
                {
                    pathBuilder.Append(entries[pathIdx]);
                }

                return(componentObj.FindProperty(pathBuilder.ToString()));
            }
            else // Field is one of the builtin ones on the SceneObject itself
            {
                if ((pathIdx + 1) < entries.Length)
                {
                    return(null);
                }

                string entry = entries[pathIdx];
                if (entry == "Position")
                {
                    SerializableProperty property = new SerializableProperty(
                        SerializableProperty.FieldType.Vector3,
                        typeof(Vector3),
                        () => so.LocalPosition,
                        (x) => so.LocalPosition = (Vector3)x);

                    return(property);
                }
                else if (entry == "Rotation")
                {
                    SerializableProperty property = new SerializableProperty(
                        SerializableProperty.FieldType.Vector3,
                        typeof(Vector3),
                        () => so.LocalRotation.ToEuler(),
                        (x) => so.LocalRotation = Quaternion.FromEuler((Vector3)x));

                    return(property);
                }
                else if (entry == "Scale")
                {
                    SerializableProperty property = new SerializableProperty(
                        SerializableProperty.FieldType.Vector3,
                        typeof(Vector3),
                        () => so.LocalScale,
                        (x) => so.LocalScale = (Vector3)x);

                    return(property);
                }

                return(null);
            }
        }