Creating Replace Tool in Unity for Level Designers

Do you wish you could make a level with simple cubes and later replace them all with some other object with one click? This is what we are going to create today!

A long time ago, I created posts about the basics of creating extensions for the editor. I’m not going to cover that topic here, so go ahead and check the post about creating custom editor windows. ?

This post will cover creating tool window, gathering of selected objects on the scene, and using Unity’s Undo class. ?‍?

The idea ?

Let’s elaborate a little bit on the concept for our Replace Tool. Let’s pretend that we are Level Designer and the first thing that we are doing when creating a new level, we are making a block out of it. That means that we are using simple shapes to build a scene for our game to understand the design better. After that, the next step is to replace those simple shapes with models from the graphic team. ?

At that point, we would need to go back and replace all objects on the scene manually. But we are a little bit lazy on that, so we started thinking that there should be an easier way to do it. ?

Luckily, we know how to create extensions for Unity editor! So we should create a tool that will do it for us!

Creating a tool ?‍?

The first thing that we will do is to create the Editor folder, and script in it.

Our ReplaceToolWindow script in the Editor folder.

If you don’t know why we need to do that, go back and read about creating custom windows. ?

Now, let’s create a base code in our script and let’s include Unity methods that we are going to use.

using System.Linq;
using UnityEngine;
using UnityEditor;

/// <summary>
/// Replace tool window.
/// </summary>
public class ReplaceToolWindow : EditorWindow
{
    // Register menu item to open Window.
    [MenuItem("Custom Tools/Replace with Prefab")]
    /// <summary>
    /// Method used to open ReplaceToolWindow.
    /// </summary>
    public static void ShowWindow()
    {
        // Get existing open window or if none, make a new one.
        var window = GetWindow<ReplaceToolWindow>();

        // Open / Show window.
        window.Show();
    }

    /// <summary>
    /// Drawing GUI in window.
    /// </summary>
    private void OnGUI()
    {
        
    }

    /// <summary>
    /// Unity method called when Inspector is updated.
    /// </summary>
    private void OnInspectorUpdate()
    {
        
    }

    /// <summary>
    /// Unity method called when user changed selection in editor.
    /// </summary>
    private void OnSelectionChange()
    {
        
    }
}

We need to store a little bit of data like a list of objects to replace or an object that we are going to use to replace objects from the list. Also, remember to put it in the Editor folder!

using UnityEngine;

/// <summary>
/// Data class for replace tool.
/// </summary>
public class ReplaceData : ScriptableObject
{
    // Reference to replace object.
    public GameObject replaceObject;

    // Array of object to replace.
    public GameObject[] objectsToReplace;
}

I told you it will be small ?

Let’s add it to our tool class and initialize these variables!

using System.Linq;
using UnityEngine;
using UnityEditor;

/// <summary>
/// Replace tool window.
/// </summary>
public class ReplaceToolWindow : EditorWindow
{
    // Data store for replace tool.
    ReplaceData data;

    // Data variable wrapped into SerializedObject.
    SerializedObject serializedData;

    // Prefab variable from data object. Using SerializedProperty for integrated Undo.
    SerializedProperty replaceObjectField;

    ...

    /// <summary>
    /// Inits the data if needed.
    /// </summary>
    private void InitDataIfNeeded()
    {
        // If data don't exist, create it.
        if (!data)
        {
            data = ScriptableObject.CreateInstance<ReplaceData>();
            serializedData = null;
        }

        // If data was not wrapped into SerializedObject, wrap it.
        if (serializedData == null)
        {
            serializedData = new SerializedObject(data);
            replaceObjectField = null;
        }

        // If prefab field was not assigned as SerializedProperty, assign it.
        if (replaceObjectField == null)
        {
            replaceObjectField = serializedData.FindProperty("replaceObject");
        }
    }
}

Great! I’ve decided to use SerializedObject and SerializedProperty, because thanks to them, we will be able to use Unity’s Undo action for ReplaceObject field. A small thing, but you will enjoy it ?

The next step will be to add some content to our window!

Beware! It’s lengthy! ?

using System.Linq;
using UnityEngine;
using UnityEditor;

/// <summary>
/// Replace tool window.
/// </summary>
public class ReplaceToolWindow : EditorWindow
{
    ...

    // Scroll position for list of selected objects.
    Vector2 selectObjectScrollPosition;

    ...

    /// <summary>
    /// Drawing GUI in window.
    /// </summary>
    private void OnGUI()
    {
        // Check to have data.
        InitDataIfNeeded();

        // Window title.
        EditorGUILayout.LabelField("Replace Tool", EditorStyles.boldLabel);

        // ReplaceObject field. This object will be used to replace selected game objects.
        EditorGUILayout.PropertyField(replaceObjectField);

        // Space.
        EditorGUILayout.Separator();

        // Title for section with objects to replace.
        EditorGUILayout.LabelField("Selected objects to replace", EditorStyles.boldLabel);

        // Space.
        EditorGUILayout.Separator();

        // Saving number of objects to replace.
        int objectToReplaceCount = data.objectsToReplace != null ? data.objectsToReplace.Length : 0;
        EditorGUILayout.IntField("Object count", objectToReplaceCount);

        // Adding a little indentation.
        EditorGUI.indentLevel++;

        // Printing information when no object is selected on scene.
        if (objectToReplaceCount == 0)
        {
            EditorGUILayout.Separator();
            EditorGUILayout.LabelField("Select object o objects in hierarchy to replace them", EditorStyles.wordWrappedLabel);
        }

        // Scroll view with selected game objects.
        selectObjectScrollPosition = EditorGUILayout.BeginScrollView(selectObjectScrollPosition);

        // Read only list of objects to replace
        GUI.enabled = false;
        if (data && data.objectsToReplace != null)
        {
            foreach (var go in data.objectsToReplace)
            {
                EditorGUILayout.ObjectField(go, typeof(GameObject), true);
            }
        }
        GUI.enabled = true;

        // Closing scroll view.
        EditorGUILayout.EndScrollView();

        // Removing indentation for rest of the window.
        EditorGUI.indentLevel--;

        // Space.
        EditorGUILayout.Separator();

        // Replace button.
        if (GUILayout.Button("Replace"))
        {
            
        }

        // Space.
        EditorGUILayout.Separator();

        // Applying any changes on data.
        serializedData.ApplyModifiedProperties();
    }

    ...
}

Ugh! Okay, as this is out of our way, let’s focus on selecting objects on the scene. Of course, there is no point in overriding Unity system, but we can get objects that user selected thanks to the Selection class!

using System.Linq;
using UnityEngine;
using UnityEditor;

/// <summary>
/// Replace tool window.
/// </summary>
public class ReplaceToolWindow : EditorWindow
{
    ...

    /// <summary>
    /// Unity method called when user changed selection in editor.
    /// </summary>
    private void OnSelectionChange()
    {
        // Check to have data.
        InitDataIfNeeded();

        // Creating filter to gather object only on scene.
        SelectionMode objectFilter = SelectionMode.Unfiltered ^ ~(SelectionMode.Assets | SelectionMode.DeepAssets | SelectionMode.Deep);
        Transform[] selection = Selection.GetTransforms(objectFilter);

        // Converting Transform array into GameObject array.
        data.objectsToReplace = selection.Select(s => s.gameObject).ToArray();

        // Force repaint as update is needed.
        if (serializedData.UpdateIfRequiredOrScript())
        {
            this.Repaint();
        }
    }

    ...

}

I’ve used bit operations to create a filter to gather only objects on the scene that we selected. You can read more about bit operators here.

If you would test it right now, you might encounter a little problem with window refreshment. Due to the lack of focus on the window, Unity is not calling OnGUI() method which updates our view. We have to force it using OnInspectorUpdate() method.

using System.Linq;
using UnityEngine;
using UnityEditor;

/// <summary>
/// Replace tool window.
/// </summary>
public class ReplaceToolWindow : EditorWindow
{
    ...

    /// <summary>
    /// Unity method called when Inspector is updated.
    /// </summary>
    private void OnInspectorUpdate()
    {
        // If data was changed, force repaint.
        if (serializedData != null && serializedData.UpdateIfRequiredOrScript())
        {
            this.Repaint();
        }
    }

    ...
}

We did a lot at this point, so let’s see it in action!

Selecting objects in hierarchy and displaying them in our window.

Niiiiice! ?

So the last part that we are missing now is to make the replacement work! Also, this part should utilize the same Undo action that I mention with the ReplaceObject field. However, this won’t be that easy here.

We will need to save steps that we are doing for the Undo action to work as intended.

using System.Linq;
using UnityEngine;
using UnityEditor;

/// <summary>
/// Replace tool window.
/// </summary>
public class ReplaceToolWindow : EditorWindow
{
    ...

    /// <summary>
    /// Drawing GUI in window.
    /// </summary>
    private void OnGUI()
    {

        ...

        // Replace button.
        if (GUILayout.Button("Replace"))
        {
            // Check if replace object is assigned.
            if (!replaceObjectField.objectReferenceValue)
            {
                Debug.LogErrorFormat("[Replace Tool] {0}", "Missing prefab to replace with!");
                return;
            }

            // Check if there are objects to replace.
            if (data.objectsToReplace.Length == 0)
            {
                Debug.LogErrorFormat("[Replace Tool] {0}", "Missing objects to replace!");
                return;
            }

            ReplaceSelectedObjects(data.objectsToReplace, data.replaceObject);
        }

        ...
    }

    ...

    /// <summary>
    /// Replaces game objects with provided replace object.
    /// </summary>
    /// <param name="objectToReplace">Game Objects to replace.</param>
    /// <param name="replaceObject">Replace object.</param>
    private void ReplaceSelectedObjects(GameObject[] objectToReplace, GameObject replaceObject)
    {
        Debug.Log("[Replace Tool] Replace process");

        // Loop through object to replace.
        for (int i = 0; i < objectToReplace.Length; i++)
        {
            var go = objectToReplace[i];

            // Register current game object to Undo action in editor.
            Undo.RegisterCompleteObjectUndo(go, "Saving game object state");

            // Creating replace object as the same position and same parent.
            var inst = Instantiate(replaceObject, go.transform.position, go.transform.rotation, go.transform.parent);
            inst.transform.localScale = go.transform.localScale;

            // Register object creation for Undo action in editor.
            Undo.RegisterCreatedObjectUndo(inst, "Replacement creation.");

            // Changing parent for all children of current game object.
            foreach (Transform child in go.transform)
            {
                // Saving action for Undo action in editor.
                Undo.SetTransformParent(child, inst.transform, "Parent Change");
            }

            // Destroying current game object with save for Undo action in editor.
            Undo.DestroyObjectImmediate(go);
        }

        Debug.LogFormat("[Replace Tool] {0} objects replaced on scene with {1}", objectToReplace.Length, replaceObject.name);
    }
}

You can notice that there are few lines with the Undo method called here. All of them are going to be collected and executed at once to revert our object replacement.

The result ?

As there is no more coding involved, we should see how it’s working! ?

Our tool in action! ?

We did it! What do you think about it? Did you create other tools for Unity? Let me know in the comment section below!

If you want to get notified on future content, sign up for the newsletter!

As always, the whole project is available at my public repository. ?

And I hope to see you next time! ?

5 4 votes
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
SANTIAGO DOS SANTOS LOVERA
SANTIAGO DOS SANTOS LOVERA
3 years ago

Great tutorial It helps me!
Thanks for sharing!

1
0
Would love your thoughts, please comment.x
()
x