5

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.

Lece
  • 2,339
  • 1
  • 17
  • 21
NSSwift
  • 395
  • 1
  • 12
  • Is it runtime? Then you can't. Otherwise, are they instances of a prefab? Apply the changes to the prefab. – Draco18s no longer trusts SE May 04 '18 at 05:56
  • @Draco18s Actually you can during run-time and without modifying the prefab like OP wanted. – Programmer May 04 '18 at 13:23
  • @Draco18s I don't want to do this at runtime. For now, I am actually cloning a game object, not a prefab. Though I guess implementing it as a game object and a prefab wouldn't be too different. I stated in the question that I need to see the dynamic changes. For example, as I increase the size of a quad, I need to see all clones grow in size at the same time or should I say real-time. Applying changes to a prefab would mean I have to increase one clone, then hit apply button to see the changes in all the other clones. That is not what I need. – NSSwift May 04 '18 at 14:21
  • @Programmer Really? And I don't mean in a "store a list of objects and apply the same changes to the whole list" kind of way, that's pretty obvious. NSSwift: I don't think there's a built in way of doing that. You'd have to write some kind of custom editor script (and I'd probably suggest still using prefabs). – Draco18s no longer trusts SE May 04 '18 at 18:15
  • @Draco18s Yeah, I want to write a custom editor script for this – NSSwift May 04 '18 at 18:23
  • @Draco18s Yes, it can be done without a prefab and without using list of objects too. I don't think it's a good idea to modify a prefab. The idea of a prefab is to have object with pre-settings that can be re-used not to have an Object you can change to change to objects.Prefabs should't be modified. – Programmer May 04 '18 at 18:27
  • @Programmer Depends on what you need a prefab for, I suppose. Anyway, question was more of an "ok, how?" – Draco18s no longer trusts SE May 04 '18 at 18:32
  • @Programmer how will it be possible to modify all clones at runtime without creating a list? This question was quite interesting. Can you point out to it? If you want I can make it as another question. Will the method be same to both in editor and at runtime? – killer_mech May 04 '18 at 19:08
  • I suggest to use a scriptable object for this (with `CreateAssetMenu` attribute). Have a component on all objects that should be effected that can store it's values into the scriptable object or change it's values based on the scriptable object. Use `ExecuteInEditMode` for this too. – Gunnar B. May 05 '18 at 12:03

1 Answers1

6

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

Lece
  • 2,339
  • 1
  • 17
  • 21
  • 1
    @Lece thanks, this is precisely what I was trying to achieve. – NSSwift May 07 '18 at 20:16
  • 1
    @NSSwift Excellent! I've just tweaked it, so please grab the update. It now supports object switching and handles play-mode better. – Lece May 07 '18 at 22:24
  • 3
    @Lece Thanks once again. I have been trying to figure an appropriate way to clone more than one game object. What’s an appropriate way to go about this? Also, my original object to be cloned has got parameters that are triggered through buttons in its own custom editor. These button presses are only recognized by the selected clone objects, not all clones. – NSSwift May 16 '18 at 05:22
  • @Lece I think I have an idea what might be preventing all clones from firing the buttons. Each button in the custom editor, generally calls a function and these functions/buttons are not serialised (just created as normal custom editor buttons) hence I don't think `EditorUtility.CopySerializedIfDifferent` is working on these buttons. Is this right? if so how can I circumvent it? – NSSwift May 17 '18 at 16:13
  • @Lece still trying to get the issues sorted out – NSSwift May 18 '18 at 12:29
  • Hi @NSSwift. With regards to **1.** If you're cloning more than one object, then I wonder how that would fit into *adjusting one clone to affect all others*? Perhaps using multiple spawners would be better? **2.** The solution's based on scene edits (as requested) which are automatically recorded. You probably need to inform the editor. See: [Undo.RecordObject](https://docs.unity3d.com/ScriptReference/Undo.RecordObject.html). – Lece May 18 '18 at 23:52
  • 1
    I just asked another question https://stackoverflow.com/questions/51089098/how-to-clone-several-game-objects-in-a-way-that-clone-properties-of-one-can-be-a. I was replicating the behaviour by adding duplicate methods for other objects in the `CircleSpawn` class but this is definitely impractical for several game objects. Would really appreciate if you could take a look at this since it is closely related. – NSSwift Jun 28 '18 at 18:42
  • I was unable to achieve https://stackoverflow.com/questions/51089098/how-to-clone-several-game-objects-in-a-way-that-clone-properties-of-one-can-be-a which I posted in my previous comment. I just added a bounty for the question and would be grateful if you could assist, it's directly related to this question. – NSSwift Sep 07 '18 at 08:03
  • 1
    @Lece NSSwift's bounty will be ending in less than 24 hours :) – TenOutOfTen Sep 13 '18 at 18:21
  • @NSSwift I recommend you seek help elsewhere. It seems quite clear that Lece is uninterested in answering the other question. I've been following your question for a while, tried to solve it but wasn't able to. There seems to be a general interest in the question since it has raked in 8 up votes. – TenOutOfTen Sep 17 '18 at 16:20