Skip to content

Instantly share code, notes, and snippets.

@akaashhakim
Created May 10, 2025 11:30
Show Gist options
  • Save akaashhakim/bdff81123d2b9787e5fc3623fd826ec4 to your computer and use it in GitHub Desktop.
Save akaashhakim/bdff81123d2b9787e5fc3623fd826ec4 to your computer and use it in GitHub Desktop.
πŸ”„ Quick Replace Tool for Unity A powerful editor extension to batch-replace GameObjects while preserving transforms, with snapping and randomization options!
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
public class QuickReplaceTool : EditorWindow
{
#region INSTRUCTIONS
/*
* ===== QUICK REPLACE TOOL =====
* HOW TO USE:
* 1. SELECT objects in scene
* 2. Click [Add Selected to Queue]
* 3. Set replacement prefab
* 4. Configure options:
* - Transform preservation (position/rotation/scale)
* - Ground snapping (layers/tags)
* - Random prefabs (optional)
* 5. Click [Replace Objects]
*
* TIPS:
* - Hold ALT while clicking for quick access
* - Use CTRL+Z to undo replacements
* - Enable debug logs for troubleshooting
*/
#endregion
#region CORE_VARIABLES
private List<GameObject> replacementQueue = new List<GameObject>();
private GameObject replacementPrefab;
private Vector2 scrollPosition;
#endregion
#region TRANSFORM_VARIABLES
private Vector3 positionOffset = Vector3.zero;
private Vector3 rotationOffset = Vector3.zero;
private Vector3 scaleOffset = Vector3.one;
private bool preserveScale = true;
private bool preserveRotation = true;
private bool preservePosition = true;
#endregion
#region SNAPPING_VARIABLES
private bool snapToGround = false;
private bool alignToNormal = false;
[SerializeField] private LayerMask groundLayerMask = ~0;
private string groundTagFilter = "";
private float snapRaycastDistance = 1000f;
#endregion
#region RANDOMIZATION_VARIABLES
private bool useRandomPrefab = false;
[SerializeField] private List<GameObject> randomPrefabList = new List<GameObject>();
#endregion
#region UI_VARIABLES
private bool showAdvancedSnapping = false;
private bool showTransformOptions = true;
private bool showReplacementSettings = true;
private bool showSnappingOptions = true;
private bool logReport = true;
#endregion
[MenuItem("Tools/Akaash/Quick Replace Tool &r")]
public static void ShowWindow() => GetWindow<QuickReplaceTool>("Quick Replace");
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
DrawHeader();
DrawMainContent();
EditorGUILayout.EndScrollView();
}
private void DrawHeader()
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Quick Replace Tool", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"1. Select objects in scene\n" +
"2. Add to queue\n" +
"3. Set replacement prefab\n" +
"4. Configure options\n" +
"5. Click Replace!\n\n" +
"Shortcut: Alt+R | Undo: Ctrl+Z",
MessageType.Info);
EditorGUILayout.Space();
}
private void DrawMainContent()
{
DrawQueueSection();
DrawReplacementSettings();
DrawTransformOptions();
DrawSnappingOptions();
DrawActionButtons();
}
#region UI_SECTIONS
private void DrawQueueSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Replacement Queue", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Selected", GUILayout.Height(25)))
{
AddSelectedToQueue();
}
if (GUILayout.Button("Clear Queue", GUILayout.Height(25)))
{
replacementQueue.Clear();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.HelpBox($"Objects in queue: {replacementQueue.Count}", MessageType.None);
// Display queue items
if (replacementQueue.Count > 0)
{
EditorGUILayout.LabelField("Objects to replace:", EditorStyles.miniBoldLabel);
EditorGUI.indentLevel++;
foreach (var obj in replacementQueue)
{
EditorGUILayout.ObjectField(obj, typeof(GameObject), true);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private void DrawReplacementSettings()
{
showReplacementSettings = EditorGUILayout.Foldout(showReplacementSettings, "Replacement Settings", true);
if (!showReplacementSettings) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
replacementPrefab = (GameObject)EditorGUILayout.ObjectField("Replacement Prefab", replacementPrefab, typeof(GameObject), false);
EditorGUILayout.Space();
useRandomPrefab = EditorGUILayout.Toggle("Use Random Prefabs", useRandomPrefab);
if (useRandomPrefab)
{
EditorGUI.indentLevel++;
SerializedObject so = new SerializedObject(this);
EditorGUILayout.PropertyField(so.FindProperty("randomPrefabList"), new GUIContent("Prefab List"), true);
so.ApplyModifiedProperties();
EditorGUI.indentLevel--;
if (randomPrefabList.Count == 0)
{
EditorGUILayout.HelpBox("Add prefabs to the list for randomization", MessageType.Warning);
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private void DrawTransformOptions()
{
showTransformOptions = EditorGUILayout.Foldout(showTransformOptions, "Transform Options", true);
if (!showTransformOptions) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
preservePosition = EditorGUILayout.Toggle("Preserve Position", preservePosition);
preserveRotation = EditorGUILayout.Toggle("Preserve Rotation", preserveRotation);
preserveScale = EditorGUILayout.Toggle("Preserve Scale", preserveScale);
EditorGUILayout.Space();
if (!preservePosition)
{
EditorGUI.indentLevel++;
positionOffset = EditorGUILayout.Vector3Field("Position Offset", positionOffset);
EditorGUI.indentLevel--;
}
if (!preserveRotation)
{
EditorGUI.indentLevel++;
rotationOffset = EditorGUILayout.Vector3Field("Rotation Offset", rotationOffset);
EditorGUI.indentLevel--;
}
if (!preserveScale)
{
EditorGUI.indentLevel++;
scaleOffset = EditorGUILayout.Vector3Field("Scale Offset", scaleOffset);
EditorGUI.indentLevel--;
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private void DrawSnappingOptions()
{
showSnappingOptions = EditorGUILayout.Foldout(showSnappingOptions, "Snapping Options", true);
if (!showSnappingOptions) return;
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
snapToGround = EditorGUILayout.Toggle("Snap to Ground", snapToGround);
if (snapToGround)
{
EditorGUI.indentLevel++;
snapRaycastDistance = EditorGUILayout.FloatField("Raycast Distance", snapRaycastDistance);
alignToNormal = EditorGUILayout.Toggle("Align to Surface Normal", alignToNormal);
EditorGUILayout.Space();
showAdvancedSnapping = EditorGUILayout.Foldout(showAdvancedSnapping, "Advanced Filters");
if (showAdvancedSnapping)
{
EditorGUI.indentLevel++;
groundLayerMask = EditorGUILayout.MaskField("Ground Layers",
groundLayerMask,
UnityEditorInternal.InternalEditorUtility.layers);
groundTagFilter = EditorGUILayout.TagField("Ground Tag Filter", groundTagFilter);
EditorGUI.indentLevel--;
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private void DrawActionButtons()
{
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.enabled = replacementQueue.Count > 0 && (replacementPrefab != null || (useRandomPrefab && randomPrefabList.Count > 0));
if (GUILayout.Button("Replace Objects", GUILayout.Height(30), GUILayout.Width(150)))
{
ReplaceObjects();
}
GUI.enabled = true;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// Add GitHub link and thank you message
DrawFooter();
}
#endregion
#region CORE_FUNCTIONALITY
private void AddSelectedToQueue()
{
if (Selection.gameObjects.Length == 0)
{
EditorUtility.DisplayDialog("No Selection", "Please select objects in the scene first", "OK");
return;
}
foreach (var obj in Selection.gameObjects)
{
if (!replacementQueue.Contains(obj))
replacementQueue.Add(obj);
}
}
private void ReplaceObjects()
{
if (replacementQueue.Count == 0)
{
Debug.LogWarning("Queue is empty!");
return;
}
if (!replacementPrefab && !useRandomPrefab)
{
Debug.LogError("No replacement prefab assigned!");
return;
}
if (useRandomPrefab && randomPrefabList.Count == 0)
{
Debug.LogError("Random prefab list is empty!");
return;
}
Undo.SetCurrentGroupName("Quick Replace");
int replacedCount = 0;
foreach (var oldObj in replacementQueue)
{
if (!oldObj) continue;
GameObject newPrefab = useRandomPrefab
? randomPrefabList[Random.Range(0, randomPrefabList.Count)]
: replacementPrefab;
GameObject newObj = (GameObject)PrefabUtility.InstantiatePrefab(newPrefab);
// Apply transforms
newObj.transform.position = preservePosition ? oldObj.transform.position : oldObj.transform.position + positionOffset;
newObj.transform.rotation = preserveRotation ? oldObj.transform.rotation : oldObj.transform.rotation * Quaternion.Euler(rotationOffset);
newObj.transform.localScale = preserveScale ? oldObj.transform.localScale : Vector3.Scale(oldObj.transform.localScale, scaleOffset);
// Advanced snapping
if (snapToGround)
SnapToGround(newObj);
// Undo/Redo tracking
Undo.RegisterCreatedObjectUndo(newObj, "Replace Object");
Undo.DestroyObjectImmediate(oldObj);
replacedCount++;
}
if (logReport)
{
Debug.Log($"Replaced {replacedCount} objects. " +
(useRandomPrefab ? $"Used {randomPrefabList.Count} random prefabs" : $"Used prefab: {replacementPrefab.name}"));
}
replacementQueue.Clear();
}
private void SnapToGround(GameObject obj)
{
// Create a list of all colliders from objects in the replacement queue
List<Collider> collidersToIgnore = new List<Collider>();
foreach (var go in replacementQueue)
{
if (go != null)
{
var collider = go.GetComponent<Collider>();
if (collider != null)
{
collidersToIgnore.Add(collider);
}
}
}
Vector3 rayStart = obj.transform.position + (Vector3.up * 100);
RaycastHit hit;
// Perform the raycast and ignore the colliders in our exclusion list
if (RaycastIgnore(rayStart, Vector3.down, out hit, snapRaycastDistance, groundLayerMask, collidersToIgnore))
{
// Check tag filter if specified
if (!string.IsNullOrEmpty(groundTagFilter) && !hit.collider.CompareTag(groundTagFilter))
return;
obj.transform.position = hit.point;
if (alignToNormal)
{
obj.transform.rotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
}
}
}
// Custom raycast function that ignores specific colliders
private bool RaycastIgnore(Vector3 origin, Vector3 direction, out RaycastHit hit, float distance, LayerMask layerMask, List<Collider> collidersToIgnore)
{
// Get all hits along the ray
RaycastHit[] allHits = Physics.RaycastAll(origin, direction, distance, layerMask);
// Sort hits by distance
System.Array.Sort(allHits, (x, y) => x.distance.CompareTo(y.distance));
// Find the first hit that isn't in our ignore list
foreach (var potentialHit in allHits)
{
if (!collidersToIgnore.Contains(potentialHit.collider))
{
hit = potentialHit;
return true;
}
}
hit = new RaycastHit();
return false;
}
#endregion
private void DrawFooter()
{
EditorGUILayout.Space(20);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
// Create style for yellow bold text
GUIStyle yellowBoldStyle = new GUIStyle(EditorStyles.label);
yellowBoldStyle.normal.textColor = Color.yellow;
yellowBoldStyle.fontStyle = FontStyle.Bold;
yellowBoldStyle.alignment = TextAnchor.MiddleCenter;
yellowBoldStyle.wordWrap = true;
// Create regular centered style for other text
GUIStyle centeredStyle = new GUIStyle(EditorStyles.label);
centeredStyle.alignment = TextAnchor.MiddleCenter;
centeredStyle.wordWrap = true;
EditorGUILayout.LabelField("Thank you for using Quick Replace Tool!", yellowBoldStyle);
EditorGUILayout.Space(5);
if (GUILayout.Button("⭐ Visit GitHub Repository", EditorStyles.miniButton))
{
Application.OpenURL("https://github.com/akaashhakim");
}
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("Consider starring the repo if you find it useful!", centeredStyle);
EditorGUILayout.EndVertical();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment