Last active
January 31, 2022 11:04
-
-
Save arimger/00842a217ea8ab03d4e1b81f11592cf3 to your computer and use it in GitHub Desktop.
Simple tool to create objects on a specific layer in Unity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | |
} | |
} | |
} | |
} |
Author
arimger
commented
Nov 20, 2019
•
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment