Skip to content

Instantly share code, notes, and snippets.

@arimger
Last active January 31, 2022 11:04
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save arimger/00842a217ea8ab03d4e1b81f11592cf3 to your computer and use it in GitHub Desktop.
Save arimger/00842a217ea8ab03d4e1b81f11592cf3 to your computer and use it in GitHub Desktop.
Simple tool to create objects on a specific layer in Unity
using UnityEditor;
using UnityEngine;
//NOTE: Editor-related scripts should be placed in an Editor folder
namespace Toolbox
{
[CustomPropertyDrawer(typeof(BrushPrefab))]
public class BrushPrefabDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUI.GetPropertyHeight(property, label, property.isExpanded);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var basePosition = position;
var targetProperty = property.FindPropertyRelative("target");
var useRandomRotationProperty = property.FindPropertyRelative("useRandomRotation");
var minRandomRotationProperty = property.FindPropertyRelative("minRotation");
var maxRandomRotationProperty = property.FindPropertyRelative("maxRotation");
var useRandomScaleProperty = property.FindPropertyRelative("useRandomScale");
var minRandomScaleProperty = property.FindPropertyRelative("minScale");
var maxRandomScaleProperty = property.FindPropertyRelative("maxScale");
var densityProperty = property.FindPropertyRelative("rarity");
EditorGUI.BeginProperty(position, label, property);
position.height = EditorGUIUtility.singleLineHeight;
if (property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, label))
{
var spacing = EditorGUIUtility.standardVerticalSpacing;
EditorGUI.indentLevel++;
position.y += position.height;
position.height = EditorGUI.GetPropertyHeight(targetProperty, true);
EditorGUI.PropertyField(position, targetProperty);
position.y += position.height + spacing;
position.height = EditorGUI.GetPropertyHeight(useRandomRotationProperty);
EditorGUI.PropertyField(position, useRandomRotationProperty);
using (new EditorGUI.DisabledScope(!useRandomRotationProperty.boolValue))
{
EditorGUI.indentLevel++;
position.y += position.height;
position.height = EditorGUI.GetPropertyHeight(minRandomRotationProperty);
EditorGUI.PropertyField(position, minRandomRotationProperty);
position.y += position.height;
position.height = EditorGUI.GetPropertyHeight(maxRandomRotationProperty);
EditorGUI.PropertyField(position, maxRandomRotationProperty);
EditorGUI.indentLevel--;
}
position.y += position.height + spacing;
position.height = EditorGUI.GetPropertyHeight(useRandomScaleProperty);
EditorGUI.PropertyField(position, useRandomScaleProperty);
using (new EditorGUI.DisabledScope(!useRandomScaleProperty.boolValue))
{
EditorGUI.indentLevel++;
position.y += position.height;
position.height = EditorGUI.GetPropertyHeight(minRandomScaleProperty);
EditorGUI.PropertyField(position, minRandomScaleProperty);
position.y += position.height;
position.height = EditorGUI.GetPropertyHeight(maxRandomScaleProperty);
EditorGUI.PropertyField(position, maxRandomScaleProperty);
EditorGUI.indentLevel--;
}
position.y += position.height + spacing;
position.height = EditorGUI.GetPropertyHeight(useRandomScaleProperty);
position.y += 2 * spacing;
EditorGUI.PropertyField(position, densityProperty);
EditorGUI.indentLevel--;
}
EditorGUI.EndProperty();
}
}
}
using System;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Toolbox
{
[Serializable]
public class BrushPrefab
{
//NOTE: to use this attribute install Editor Toolbox package - https://github.com/arimger/Unity-Editor-Toolbox
//[AssetPreview]
public GameObject target;
public bool useRandomScale;
public Vector3 minScale = new Vector3(1.0f, 1.0f, 1.0f);
public Vector3 maxScale;
public bool useRandomRotation = true;
public Vector3 minRotation;
public Vector3 maxRotation = new Vector3(0.0f, 359.0f, 0.0f);
[Range(0.0f, 1.0f)]
public float rarity = 0.4f;
}
[ExecuteInEditMode, DisallowMultipleComponent]
[AddComponentMenu("Tools/PrefabsPainter", 1)]
public class PrefabsPainter : MonoBehaviour
{
[SerializeField]
private LayerMask targetLayer;
[SerializeField]
private Transform targetParent;
[SerializeField]
private List<BrushPrefab> brushPrefabs;
public void MassPlacePrefabs(int count)
{
MassPlace(count, brushPrefabs.ToArray());
}
public void MassPlace(int count, params BrushPrefab[] prefabs)
{
throw new NotImplementedException();
}
public void PlaceObjectsInBrush(Vector3 center, float gridSize, float radius, float density)
{
PlaceObjectsInBrush(center, gridSize, radius, density, targetLayer, targetParent, brushPrefabs.ToArray());
}
public void PlaceObjectsInBrush(Vector3 center, float gridSize, float radius, float density, params BrushPrefab[] objects)
{
PlaceObjectsInBrush(center, gridSize, radius, density, ~0, targetParent, objects);
}
public void PlaceObjectsInBrush(Vector3 center, float gridSize, float radius, float density, LayerMask layer, params BrushPrefab[] objects)
{
PlaceObjectsInBrush(center, gridSize, radius, density, layer, targetParent, objects);
}
public void PlaceObjectsInBrush(Vector3 center, float gridSize, float radius, float density, LayerMask layer, Transform parent, params BrushPrefab[] prefabs)
{
if (prefabs == null || prefabs.Length == 0)
{
#if UNITY_EDITOR
Debug.LogWarning("[Prefabs Painter] No objects to instantiate.");
#endif
return;
}
//list of all created positions
var grid = new List<Vector2>();
//objects count calculation using circle area, provided density and single cell size
var count = (int)Mathf.Max((Mathf.PI * radius * radius * density) / (gridSize * 2), 1);
var totalRarity = 0.0f;
for (var i = 0; i < prefabs.Length; i++)
{
totalRarity += prefabs[i].rarity;
}
for (var i = 0; i < prefabs.Length; i++)
{
var prefab = prefabs[i];
var currentPrefabCount = (int)(count * (prefab.rarity / totalRarity));
for (var j = 0; j < currentPrefabCount; j++)
{
//setting random properties
var radians = Random.Range(0, 359) * Mathf.Deg2Rad;
var distance = Random.Range(0.0f, radius);
//calculating position + grid cell position
var position = new Vector3(Mathf.Cos(radians), 0, Mathf.Sin(radians)) * distance + center;
var gridPosition = new Vector2(position.x - position.x % gridSize, position.z - position.z % gridSize);
//position validation using grid and layer
if (grid.Contains(gridPosition) ||
!Physics.Raycast(position + Vector3.up, Vector3.down, out RaycastHit hitInfo, Mathf.Infinity, layer))
{
continue;
}
grid.Add(gridPosition);
var target = prefab.target;
if (target == null)
{
Debug.LogWarning("[Prefabs Painter] Ignored empty prefab.");
continue;
}
GameObject gameObject;
//instantiate new object, use the PrefabUtility if possible to save reference
#if UNITY_EDITOR
if (PrefabUtility.GetPrefabAssetType(target) == PrefabAssetType.NotAPrefab)
{
gameObject = Instantiate(target);
}
else
{
if (!PrefabUtility.IsPartOfPrefabAsset(target))
{
target = PrefabUtility.GetCorrespondingObjectFromSource(target);
}
gameObject = PrefabUtility.InstantiatePrefab(target) as GameObject;
}
#else
gameObject = Instantiate(target);
#endif
//set random rotation if needed
if (prefab.useRandomRotation)
{
gameObject.transform.eulerAngles = new Vector3(Random.Range(prefab.minRotation.x, prefab.maxRotation.x),
Random.Range(prefab.minRotation.y, prefab.maxRotation.y), Random.Range(prefab.minRotation.z, prefab.maxRotation.z));
}
//set random scale if needed
if (prefab.useRandomScale)
{
gameObject.transform.localScale = new Vector3(Random.Range(prefab.minScale.x, prefab.maxScale.x),
Random.Range(prefab.minScale.y, prefab.maxScale.y), Random.Range(prefab.minScale.z, prefab.maxScale.z));
}
//setup final object
gameObject.transform.position = hitInfo.point;
gameObject.transform.parent = parent;
#if UNITY_EDITOR
Undo.RegisterCreatedObjectUndo(gameObject, "Created " + gameObject.name + " with painter");
#endif
}
}
}
public LayerMask TargetLayer
{
get => targetLayer;
set => targetLayer = value;
}
public Transform TargetParent
{
get => targetParent;
set => targetParent = value;
}
public List<BrushPrefab> BrushPrefabs
{
get => brushPrefabs;
set => brushPrefabs = value;
}
}
}
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityTools = UnityEditor.Tools;
//NOTE: Editor-related scripts should be placed in an Editor folder
namespace Toolbox.Editor
{
/// <summary>
/// Editor for <see cref="PrefabsPainter"/> component. Provides tools to manipulate game environment.
/// </summary>
[CustomEditor(typeof(PrefabsPainter), true, isFallback = false)]
public sealed class PrefabsPainterEditor : UnityEditor.Editor
{
private const float maxBrushRadius = 1000.0f;
private static bool isToolActive;
private static float chunkSize = 1.0f;
private static float brushSize = 8.0f;
private static float brushFill = 0.5f;
private static Color brushColor = new Color(0, 0, 0, 0.4f);
private SerializedProperty targetLayerProperty;
private SerializedProperty targetParentProperty;
private SerializedProperty brushPrefabsProperty;
private ReorderableList brushPrefabsList;
/// <summary>
/// Tool initialization.
/// </summary>
private void OnEnable()
{
chunkSize = EditorPrefs.GetFloat("PrefabsPainter.chunkSize", chunkSize);
brushSize = EditorPrefs.GetFloat("PrefabsPainter.brushSize", brushSize);
brushFill = EditorPrefs.GetFloat("PrefabsPainter.brushFill", brushFill);
var r = EditorPrefs.GetFloat("PrefabsPainter.brushColor.r", brushColor.r);
var g = EditorPrefs.GetFloat("PrefabsPainter.brushColor.g", brushColor.g);
var b = EditorPrefs.GetFloat("PrefabsPainter.brushColor.b", brushColor.b);
var a = EditorPrefs.GetFloat("PrefabsPainter.brushColor.a", brushColor.a);
brushColor = new Color(r, g, b, a);
targetLayerProperty = serializedObject.FindProperty("targetLayer");
targetParentProperty = serializedObject.FindProperty("targetParent");
brushPrefabsProperty = serializedObject.FindProperty("brushPrefabs");
brushPrefabsList = new ReorderableList(brushPrefabsProperty.serializedObject, brushPrefabsProperty, true, true, true, true)
{
drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
const float padding = 15.0f;
const float spacing = 2.0f;
rect.height -= spacing;
rect.width -= padding;
rect.y += spacing;
rect.x += padding;
var label = new GUIContent("Prefab " + index);
var element = brushPrefabsList.serializedProperty.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(rect, element, label, element.isExpanded);
},
elementHeightCallback = (int index) =>
{
const float spacing = 5.0f;
var element = brushPrefabsList.serializedProperty.GetArrayElementAtIndex(index);
return EditorGUI.GetPropertyHeight(element, element.isExpanded) + spacing;
},
drawHeaderCallback = (Rect rect) =>
{
EditorGUI.LabelField(rect, "Brush Prefabs");
},
};
}
/// <summary>
/// Tool deinitialization.
/// </summary>
private void OnDisable()
{
isToolActive = false;
EditorPrefs.SetFloat("PrefabsPainter.chunkSize", chunkSize);
EditorPrefs.SetFloat("PrefabsPainter.brushSize", brushSize);
EditorPrefs.SetFloat("PrefabsPainter.brushFill", brushFill);
EditorPrefs.SetFloat("PrefabsPainter.brushColor.r", brushColor.r);
EditorPrefs.SetFloat("PrefabsPainter.brushColor.g", brushColor.g);
EditorPrefs.SetFloat("PrefabsPainter.brushColor.b", brushColor.b);
EditorPrefs.SetFloat("PrefabsPainter.brushColor.a", brushColor.a);
}
/// <summary>
/// Scene view managment used to paint over selected <see cref="LayerMask"/>.
/// </summary>
private void OnSceneGUI()
{
if (!isToolActive)
{
return;
}
var controlId = GUIUtility.GetControlID(FocusType.Passive);
if (UnityTools.current != Tool.None)
{
UnityTools.current = Tool.None;
}
var ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, Target.TargetLayer))
{
Handles.color = brushColor;
Handles.DrawSolidArc(hit.point, Vector3.up, Vector3.right, 360, brushSize);
SceneView.RepaintAll();
if (Event.current.type != EventType.MouseDown || Event.current.button != 0)
{
return;
}
GUIUtility.hotControl = controlId;
Target.PlaceObjectsInBrush(hit.point, chunkSize, brushSize, brushFill);
Event.current.Use();
}
}
private void DrawToolInfoSection()
{
var rect = EditorGUILayout.GetControlRect(false, Style.toolToggleStyle.fixedHeight);
rect.xMin = EditorGUIUtility.labelWidth +
EditorGUIUtility.standardVerticalSpacing * 2 + Style.toolToggleStyle.fixedWidth / 2;
isToolActive = GUI.Toggle(rect, isToolActive, Style.toolToggleContent, Style.toolToggleStyle);
var text = isToolActive ? "De-activate Tool" : "Activate Tool";
rect.xMin += EditorGUIUtility.standardVerticalSpacing * 2 + Style.toolToggleStyle.fixedWidth;
EditorGUI.LabelField(rect, text);
if (!isToolActive)
{
return;
}
EditorGUILayout.HelpBox("Tool active \n\n" +
"Navigate mouse to desired layer and press left button to create objects \n\n" +
"Ctrl+Z - Undo", MessageType.Info);
}
private void DrawSettingsSection()
{
if (!targetParentProperty.objectReferenceValue)
{
EditorGUILayout.HelpBox(Style.parentWarningContent.text, MessageType.Warning);
}
EditorGUILayout.PropertyField(targetParentProperty, targetParentProperty.isExpanded);
if (targetLayerProperty.intValue == 0)
{
EditorGUILayout.HelpBox(Style.layerWarningContent.text, MessageType.Warning);
}
EditorGUILayout.PropertyField(targetLayerProperty, targetLayerProperty.isExpanded);
EditorGUILayout.Space();
chunkSize = EditorGUILayout.FloatField("Chunk Size", chunkSize);
chunkSize = Mathf.Max(chunkSize, 0);
brushSize = EditorGUILayout.Slider("Brush Size", brushSize, 0, maxBrushRadius);
brushFill = EditorGUILayout.Slider("Brush Fill", brushFill, 0, 1);
brushColor = EditorGUILayout.ColorField("Brush Color", brushColor);
EditorGUILayout.Space();
EditorGUILayout.Space();
brushPrefabsList.DoLayoutList();
}
/// <summary>
/// Editor re-draw method.
/// </summary>
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawToolInfoSection();
DrawSettingsSection();
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// Serialized component.
/// </summary>
public PrefabsPainter Target => target as PrefabsPainter;
/// <summary>
/// Internal styling class.
/// </summary>
internal static class Style
{
internal static GUIStyle toolToggleStyle;
internal static GUIContent toolToggleContent;
internal static GUIContent layerWarningContent;
internal static GUIContent parentWarningContent;
static Style()
{
toolToggleStyle = new GUIStyle("Command");
var brushIcon = EditorGUIUtility.IconContent("d_TerrainInspector.TerrainToolSplat")?.image;
toolToggleContent = new GUIContent(brushIcon, "(De)Activate Tool");
layerWarningContent = new GUIContent("Layer property should not be \"Nothing\".");
parentWarningContent = new GUIContent("Parent not assigned.");
}
}
}
}
@arimger
Copy link
Author

arimger commented Nov 20, 2019

inspector
showcase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment