Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Simple tool to create objects on a specific layer in Unity
using UnityEditor;
using UnityEngine;
//NOTE: editor 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)
{
//base height and additional 4 spaces for separation lines
return EditorGUI.GetPropertyHeight(property, label, property.isExpanded) +
(property.isExpanded ? 4 * Style.spacing : 0);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var basePosition = position;
//get all available properties
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 = Style.height;
if (property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, label))
{
//draw vertical line
//EditorGUI.DrawRect(new Rect(position.x, position.y + position.height, Style.lineWidth, basePosition.height - position.height - Style.spacing), Style.lineColor);
EditorGUI.indentLevel++;
position.y += position.height;
position.height = EditorGUI.GetPropertyHeight(targetProperty, true);
EditorGUI.PropertyField(position, targetProperty);
//add additional spacing for horizontal line
position.y += position.height + Style.spacing + Style.spacing;
position.height = EditorGUI.GetPropertyHeight(useRandomRotationProperty);
//draw horizontal line
//EditorGUI.DrawRect(new Rect(position.x, position.y - Style.spacing / 2, position.width, Style.lineWidth), Style.lineColor);
EditorGUI.PropertyField(position, useRandomRotationProperty);
EditorGUI.BeginDisabledGroup(!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--;
}
EditorGUI.EndDisabledGroup();
//add additional spacing for horizontal line
position.y += position.height + Style.spacing + Style.spacing;
position.height = EditorGUI.GetPropertyHeight(useRandomScaleProperty);
//draw horizontal line
//EditorGUI.DrawRect(new Rect(position.x, position.y - Style.spacing / 2, position.width, Style.lineWidth), Style.lineColor);
EditorGUI.PropertyField(position, useRandomScaleProperty);
EditorGUI.BeginDisabledGroup(!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--;
}
EditorGUI.EndDisabledGroup();
position.y += position.height + Style.spacing + Style.spacing;
position.height = EditorGUI.GetPropertyHeight(useRandomScaleProperty);
//draw horizontal line
//EditorGUI.DrawRect(new Rect(position.x, position.y - Style.spacing / 2, position.width, Style.lineWidth), Style.lineColor);
//add spacing for slider
position.y += Style.spacing + Style.spacing;
EditorGUI.PropertyField(position, densityProperty);
EditorGUI.indentLevel--;
}
EditorGUI.EndProperty();
}
private static class Style
{
internal static readonly float height = EditorGUIUtility.singleLineHeight;
internal static readonly float spacing = EditorGUIUtility.standardVerticalSpacing;
internal static readonly float lineWidth = 1.0f;
internal static readonly Color lineColor = new Color(0.3f, 0.3f, 0.3f);
}
}
}
using System;
using System.Collections;
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 extension - 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 prefabsParent;
[SerializeField]
private List<BrushPrefab> brushPrefabs;
public void AddBrushPrefab(BrushPrefab prefab)
{
if (brushPrefabs == null) brushPrefabs = new List<BrushPrefab>();
brushPrefabs.Add(prefab);
}
public void RemoveBrushPrefab(BrushPrefab prefab)
{
brushPrefabs?.Remove(prefab);
}
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, params BrushPrefab[] objects)
{
PlaceObjectsInBrush(center, gridSize, radius, density, ~0, prefabsParent, objects);
}
public void PlaceObjectsInBrush(Vector3 center, float gridSize, float radius, float density, LayerMask layer, params BrushPrefab[] objects)
{
PlaceObjectsInBrush(center, gridSize, radius, density, layer, prefabsParent, 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("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 (int 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(nameof(PrefabsPainter) + " - Ignored empty prefab target.");
continue;
}
GameObject gameObject;
//instantiate new object, use prefab utility 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 PrefabsParent
{
get => prefabsParent;
set => prefabsParent = value;
}
public BrushPrefab[] BrushPrefabs
{
get => brushPrefabs.ToArray();
}
}
}
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using UnityTools = UnityEditor.Tools;
//NOTE: editor 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 static bool paintingToolToggle;
private static float gridSize = 1.0f;
private static float brushRadius = 8.0f;
private static float brushDensity = 0.5f;
private static Color brushColor = new Color(0, 0, 0, 0.4f);
private SerializedProperty layerProperty;
private SerializedProperty parentProperty;
private SerializedProperty brushPrefabsProperty;
private ReorderableList brushPrefabsList;
/// <summary>
/// Tool initialization.
/// </summary>
private void OnEnable()
{
//set proper brush settings
gridSize = EditorPrefs.GetFloat("TerrainEditor.gridSize", gridSize);
brushRadius = EditorPrefs.GetFloat("TerrainEditor.brushRadius", brushRadius);
brushDensity = EditorPrefs.GetFloat("TerrainEditor.brushDensity", brushDensity);
var r = EditorPrefs.GetFloat("TerrainEditor.brushColor.r", brushColor.r);
var g = EditorPrefs.GetFloat("TerrainEditor.brushColor.g", brushColor.g);
var b = EditorPrefs.GetFloat("TerrainEditor.brushColor.b", brushColor.b);
var a = EditorPrefs.GetFloat("TerrainEditor.brushColor.a", brushColor.a);
brushColor = new Color(r, g, b, a);
//get all needed and serialized properties from component
layerProperty = serializedObject.FindProperty("targetLayer");
parentProperty = serializedObject.FindProperty("prefabsParent");
brushPrefabsProperty = serializedObject.FindProperty("brushPrefabs");
//creation of prefabs list
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()
{
paintingToolToggle = false;
EditorPrefs.SetFloat("TerrainEditor.gridSize", gridSize);
EditorPrefs.SetFloat("TerrainEditor.brushRadius", brushRadius);
EditorPrefs.SetFloat("TerrainEditor.brushDensity", brushDensity);
EditorPrefs.SetFloat("TerrainEditor.brushColor.r", brushColor.r);
EditorPrefs.SetFloat("TerrainEditor.brushColor.g", brushColor.g);
EditorPrefs.SetFloat("TerrainEditor.brushColor.b", brushColor.b);
EditorPrefs.SetFloat("TerrainEditor.brushColor.a", brushColor.a);
}
/// <summary>
/// Scene view managment used in tool functionality.
/// </summary>
private void OnSceneGUI()
{
if (!paintingToolToggle)
{
return;
}
var controlId = GUIUtility.GetControlID(FocusType.Passive);
if (UnityTools.current != Tool.None)
{
UnityTools.current = Tool.None;
}
var ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
//check for possible surfaces using provided terrain layer
if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, Target.TargetLayer))
{
Handles.color = brushColor;
//brush visualization using solid arc
Handles.DrawSolidArc(hit.point, Vector3.up, Vector3.right, 360, brushRadius);
if (Event.current.type == EventType.MouseDown)
{
if (Event.current.button == 0)
{
if (paintingToolToggle)
{
Target.PlaceObjectsInBrush(hit.point, gridSize, brushRadius, brushDensity, Target.TargetLayer, Target.PrefabsParent, Target.BrushPrefabs);
}
GUIUtility.hotControl = controlId;
Event.current.Use();
}
}
}
SceneView.RepaintAll();
}
/// <summary>
/// Draws all functional buttons(tool toggles).
/// </summary>
private void DrawActionSection()
{
//TODO:
const float toolLabelPadding = 35.0f;
const float toolLabelSpacing = 4.0f;
const float toolLabelWidth = 100.0f;
const float toolToggleWidth = 40.0f;
GUILayout.Space(10);
GUILayout.BeginHorizontal();
//get control rect for tool toggle
var rect = EditorGUILayout.GetControlRect(true, Style.toolToggleStyle.CalcHeight(Style.brushToggleContent, 1));
rect.x = rect.xMin + EditorGUIUtility.labelWidth + EditorGUIUtility.standardVerticalSpacing;
rect.width = toolToggleWidth;
//draw tool toggle and additional information label
paintingToolToggle = GUI.Toggle(rect, paintingToolToggle, Style.brushToggleContent, Style.toolToggleStyle);
rect.x += toolLabelPadding;
rect.y += toolLabelSpacing;
rect.width = toolLabelWidth;
GUI.Label(rect, paintingToolToggle ? "De-activate Tool" : "Activate Tool");
GUILayout.EndHorizontal();
GUILayout.Space(2);
if (!paintingToolToggle)
{
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);
}
/// <summary>
/// Draws possible settings in grouped form.
/// </summary>
private void DrawSettingsSection()
{
EditorGUILayout.BeginVertical();
if (!parentProperty.objectReferenceValue)
{
EditorGUILayout.HelpBox("Parent not assigned.", MessageType.Warning);
}
EditorGUILayout.PropertyField(parentProperty, parentProperty.isExpanded);
EditorGUILayout.Space();
if (layerProperty.intValue == 0)
{
EditorGUILayout.HelpBox("Layer property should not be \"Nothing\".", MessageType.Warning);
}
EditorGUILayout.PropertyField(layerProperty, layerProperty.isExpanded);
gridSize = Mathf.Max(EditorGUILayout.FloatField("Grid Size", gridSize), 0);
brushRadius = EditorGUILayout.Slider("Brush Size", brushRadius, 0, 100);
brushDensity = EditorGUILayout.Slider("Brush Density", brushDensity, 0, 1);
brushColor = EditorGUILayout.ColorField("Brush Color", brushColor);
EditorGUILayout.Space();
EditorGUILayout.Space();
brushPrefabsList.DoLayoutList();
EditorGUILayout.EndVertical();
}
/// <summary>
/// Editor re-draw method.
/// </summary>
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawActionSection();
DrawSettingsSection();
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// Serialized component.
/// </summary>
public PrefabsPainter Target => target as PrefabsPainter;
public static bool MassPlace(int count)
{
return EditorUtility.DisplayDialog("Mass Place", "Are you sure about placing " + count + " objects?", "Yes", "Cancel");
}
/// <summary>
/// Internal styling class.
/// </summary>
internal static class Style
{
internal static Texture treeIcon;
internal static Texture noneIcon;
internal static Texture brushIcon;
internal static Texture terrainIcon;
internal static Texture settingsIcon;
internal static GUIStyle groupStyle;
internal static GUIStyle smallIconStyle;
internal static GUIStyle toolToggleStyle;
internal static GUIStyle headerToggleStyle;
internal static GUIContent settingsContent;
internal static GUIContent treeToggleContent;
internal static GUIContent noneToggleContent;
internal static GUIContent brushToggleContent;
internal static GUIContent terrainToggleContent;
internal static GUILayoutOption[] toggleOptions;
internal static GUILayoutOption[] smallIconOptions;
static Style()
{
noneIcon = null;
treeIcon = EditorGUIUtility.IconContent("d_TerrainInspector.TerrainToolTrees")?.image;
brushIcon = EditorGUIUtility.IconContent("d_TerrainInspector.TerrainToolSplat")?.image;
terrainIcon = null;//EditorGUIUtility.IconContent("TerrainInspector.TerrainToolSetheightAlt")?.image;
settingsIcon = EditorGUIUtility.IconContent("d_TerrainInspector.TerrainToolSettings On")?.image;
groupStyle = new GUIStyle(GUI.skin.box);
smallIconStyle = new GUIStyle()
{
alignment = TextAnchor.MiddleCenter
};
toolToggleStyle = new GUIStyle("Command");
{ };
headerToggleStyle = new GUIStyle(EditorStyles.foldout)
{
fontStyle = FontStyle.Bold
};
settingsContent = new GUIContent("Settings", settingsIcon);
treeToggleContent = new GUIContent(treeIcon, "");
noneToggleContent = new GUIContent(noneIcon, "Not implemented");
brushToggleContent = new GUIContent(brushIcon, "(De)Activate Tool");
terrainToggleContent = new GUIContent(terrainIcon, "Not implemented");
toggleOptions = new GUILayoutOption[]
{ };
smallIconOptions = new GUILayoutOption[]
{
GUILayout.MaxWidth(20),
GUILayout.MaxHeight(20)
};
}
}
}
}
@arimger

This comment has been minimized.

Copy link
Owner 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
You can’t perform that action at this time.