Search code examples
c#unity-game-engineunity-editor

How can I adjust shape/dimensions of one clone to affect all other clones in the scene view


I would like to change/adjust the shape/dimensions of several cloned objects within the scene view by adjusting one. This object could be say a quad or a line renderer that needs to be extended. For example as one game objects line renderer is extended (using the mouse) in the scene view, all other clones are affected. I know that it's a lot simpler to adjust shape/dimensions of one object before cloning it, also this change can be made on a prefab and applied to all, but I need to see the dynamic changes as they happen to make my design process more effective. I would also like to turn this functionality on and off, when the need arises. Please see how I created my clones in this question.

Note that I don't want to achieve this at runtime.


Solution

  • How can I adjust shape/dimensions of one clone to affect all other clones in the scene view

    Synchronize properties when modifications to the component are made. To work with the existing code-base, we'll also need to clone the modified object to ensure that it's not destroyed when we re-create the other objects.

    This object could be say a quad or a line renderer that needs to be extended

    So we want to know when any property on any component has been modified. With custom scripts it's trivial with OnValidate, but with a sealed component such LineRenderer it's a little trickier. Fortunately, since we're working with the Editor we have access to some of its core features.

    Specifically, we can hook into the Editor's Undo.postprocessModifications event to get a callback whenever modifications are made to the scene. This delegate will provide us with an UndoPropertyModification array that will contain a list of modified component properties which we can use to synchronize the other objects via EditorUtility.CopySerializedIfDifferent.


    CircleSpawn

    [ExecuteInEditMode]
    public class CircleSpawn : MonoBehaviour
    {
        public List<GameObject> Objects;
        public GameObject OriginalObject;
        public GameObject PreviousObject;
        public GameObject ActiveObject;
        public SpawnData Data;
    
        private void OnEnable ()
        {
            if (Objects == null) Objects = new List<GameObject>();
    
            // Register modification event
            Undo.postprocessModifications += OnPropertyModification;
        }
    
        private void OnDisable ()
        {
            // Deregister modification event
            Undo.postprocessModifications -= OnPropertyModification;
        }
    
        private UndoPropertyModification[] OnPropertyModification (
                UndoPropertyModification[] modifications)
        {
            // Iterate through modifications
            foreach (var mod in modifications)
            {
                var trg = mod.currentValue.target as Component;
                if (trg)
                {
                    // Filter only those objects that we've created
                    if (Objects.Contains(trg.gameObject))
                    {
                        // Clone the object and make it 'active'
                        if (!ActiveObject.Equals(trg.gameObject))
                        {
                            SetActiveObj(Instantiate(trg.gameObject));
                            ActiveObject.name = OriginalObject.name;
                            ActiveObject.hideFlags =
                            HideFlags.DontSaveInBuild | HideFlags.HideInHierarchy;
                            ActiveObject.SetActive(false);
                        }
    
                        // Synchronize the other object properties
                        foreach (var obj in Objects)
                        {
                            var type = mod.currentValue.target.GetType();
                            var comp = obj.GetComponent(type);
                            if (comp == null)
                                comp = obj.AddComponent(type);
    
                            EditorUtility.CopySerializedIfDifferent(trg, comp);
                        }
    
                        UpdateTransforms();
                        break;
                    }
                }
            }
    
            return modifications;
        }
    
        public void SetActiveObj (GameObject active)
        {
            // Destroy the active object
            if (!OriginalObject.Equals(ActiveObject) &&
                 PreviousObject && !PreviousObject.Equals(ActiveObject))
                 DestroyImmediate(ActiveObject);
    
            ActiveObject = active;
        }
    
        public void UpdateObjects ()
        {    
            // Destroy old objects
            foreach (var obj in Objects) DestroyImmediate(obj);
    
            Objects.Clear();
    
            var steps = 360.0f / Data.Count;
            var angle = 0f;
    
            // Instantiate new objects
            for (var i = 0; i < Data.Count; i++)
            {
                var rot = Quaternion.Euler(0f, 0f, Data.Angle + angle);
                var pos = rot * Vector3.right * Data.Radius;
                var obj = Instantiate(ActiveObject, transform.position + pos, rot);
                obj.SetActive(true);
                Objects.Add(obj);
                angle += steps;
            }
        }
    
        public void UpdateTransforms ()
        {
            var steps = 360.0f / Objects.Count;
            var angle = 0f;
    
            // Set transforms based on Angle and Radius
            for (var i = 0; i < Objects.Count; i++)
            {
                var rot = Quaternion.Euler(0f, 0f, Data.Angle + angle);
                var pos = rot * Vector3.right * Data.Radius;
                Objects[i].transform.position =
                transform.position + pos;
                Objects[i].transform.rotation = rot;
                angle += steps;
            }
        }
    }
    

    CircleSpawnEditor

    [CustomEditor(typeof(CircleSpawn))]
    public class CircleSpawnEditor : Editor
    {
        public override void OnInspectorGUI ()
        {
            GUI.enabled = !EditorApplication.isPlaying;
            var spawner = (CircleSpawn)target;
    
            // Draw object field
            EditorGUILayout.LabelField("Object");
            spawner.OriginalObject = (GameObject)EditorGUILayout.ObjectField(
            spawner.OriginalObject, typeof(GameObject), true);
            if (!spawner.OriginalObject) return;
    
            // Restore original object
            if (GUILayout.Button("Revert") || !spawner.ActiveObject ||
               !spawner.OriginalObject.Equals(spawner.PreviousObject))
            {
                // Store data reference
                spawner.Data = spawner.OriginalObject.GetComponent<SpawnData>();
                if (!spawner.Data) return;
    
                spawner.SetActiveObj(spawner.OriginalObject);
                spawner.PreviousObject = spawner.OriginalObject;
                spawner.UpdateObjects();
            }
    
            // Draw numeric sliders
            EditorGUILayout.LabelField("Radius"); // Set as required
            spawner.Data.Radius = EditorGUILayout.Slider(spawner.Data.Radius, 0f, 100f);
            EditorGUILayout.LabelField("Angle"); // Set as required
            spawner.Data.Angle = EditorGUILayout.Slider(spawner.Data.Angle, 0f, 360f);
            EditorGUILayout.LabelField("Count"); // Set as required
            spawner.Data.Count = EditorGUILayout.IntSlider(spawner.Data.Count, 0, 36);  
    
            // Update objects on Count slider change
            if (spawner.Data.Count != spawner.Objects.Count)
                spawner.UpdateObjects();
    
            // Update transforms on Angle or Radius slider change
            if (!Mathf.Approximately(spawner.Data.Angle, spawner.Data.LastAngle) ||
                !Mathf.Approximately(spawner.Data.Radius, spawner.Data.LastRadius))
            {
                spawner.Data.LastAngle = spawner.Data.Angle;
                spawner.Data.LastRadius = spawner.Data.Radius;
                spawner.UpdateTransforms();
            }
        }
    }
    

    SpawnData

    public class SpawnData : MonoBehaviour
    {
        public int Count;
        public float Radius, LastRadius, Angle, LastAngle;
    }
    

    I've refactored the code a little, but for the most part, changes are minimal