Some time ago I needed to make a forest. My trees were prefabs and I tried to use Unity's tree-brush tool - but it didn't work so I decided to make my own tool. This is the result:
To replicate this custom tree-brush tool you first need to add a plane which will act as a ground. It's important that this plane has a collider attached to it so we can send rays towards it to determine where we want to place the trees. Then you need to make a tree (or whatever gameobject you want to add) and make it a prefab by dragging it into the project's folder.
The first script you need is called ObjectManagerCircle and you need to attach it to an empty gameobject in the Editor, and drag the prefab to this script in the Editor. This script is kinda basic if you've been into Unity and it will just add or remove gameobjects that are within a certain radius. It will also remove all gameobjects if we want to restart the making of the forest.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectManagerCircle : MonoBehaviour
{
//The object we want to add
public GameObject prefabGO;
//Whats the radius of the circle we will add objects inside of?
public float radius = 5f;
//How many GOs will we add each time we press a button?
public int howManyObjects = 5;
//Should we add or remove objects within the circle
public enum Actions { AddObjects, RemoveObjects }
public Actions action;
//Add a prefab that we instantiated in the editor script
public void AddPrefab(GameObject newPrefabObj, Vector3 center)
{
//Get a random position within a circle in 2d space
Vector2 randomPos2D = Random.insideUnitCircle * radius;
//But we are in 3d, so make it 3d and move it to where the center is
Vector3 randomPos = new Vector3(randomPos2D.x, 0f, randomPos2D.y) + center;
newPrefabObj.transform.position = randomPos;
newPrefabObj.transform.parent = transform;
}
//Remove objects within the circle
public void RemoveObjects(Vector3 center)
{
//Get an array with all children to this transform
GameObject[] allChildren = GetAllChildren();
foreach (GameObject child in allChildren)
{
//If this child is within the circle
if (Vector3.SqrMagnitude(child.transform.position - center) < radius * radius)
{
DestroyImmediate(child);
}
}
}
//Remove all objects
public void RemoveAllObjects()
{
//Get an array with all children to this transform
GameObject[] allChildren = GetAllChildren();
//Now destroy them
foreach (GameObject child in allChildren)
{
DestroyImmediate(child);
}
}
//Get an array with all children to this GO
private GameObject[] GetAllChildren()
{
//This array will hold all children
GameObject[] allChildren = new GameObject[transform.childCount];
//Fill the array
int childCount = 0;
foreach (Transform child in transform)
{
allChildren[childCount] = child.gameObject;
childCount += 1;
}
return allChildren;
}
}
The next script you need to add is the Editor script that will improve the script above. In an editor script you will be able to instantiate a prefab, which you can't in a regular script. It's really important that you place this Editor script in a folder called Editor. Remember that you can have multiple Editor folders to make it easier to organize your project.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(ObjectManagerCircle))]
public class ObjectManagerEditor : Editor
{
private ObjectManagerCircle objectManager;
//The center of the circle
private Vector3 center;
private void OnEnable()
{
objectManager = target as ObjectManagerCircle;
//Hide the handles of the GO so we dont accidentally move it instead of moving the circle
Tools.hidden = true;
}
private void OnDisable()
{
//Unhide the handles of the GO
Tools.hidden = false;
}
private void OnSceneGUI()
{
//Move the circle when moving the mouse
//A ray from the mouse position
Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
//Where did we hit the ground?
center = hit.point;
//Need to tell Unity that we have moved the circle or the circle may be displayed at the old position
SceneView.RepaintAll();
}
//Display the circle
Handles.color = Color.white;
Handles.DrawWireDisc(center, Vector3.up, objectManager.radius);
//Add or remove objects with left mouse click
//First make sure we cant select another gameobject in the scene when we click
HandleUtility.AddDefaultControl(0);
//Have we clicked with the left mouse button?
if (Event.current.type == EventType.MouseDown && Event.current.button == 0)
{
//Should we add or remove objects?
if (objectManager.action == ObjectManagerCircle.Actions.AddObjects)
{
AddNewPrefabs();
MarkSceneAsDirty();
}
else if (objectManager.action == ObjectManagerCircle.Actions.RemoveObjects)
{
objectManager.RemoveObjects(center);
MarkSceneAsDirty();
}
}
}
//Add buttons this scripts inspector
public override void OnInspectorGUI()
{
//Add the default stuff
DrawDefaultInspector();
//Remove all objects when pressing a button
if (GUILayout.Button("Remove all objects"))
{
//Pop-up so you don't accidentally remove all objects
if (EditorUtility.DisplayDialog("Safety check!", "Do you want to remove all objects?", "Yes", "No"))
{
objectManager.RemoveAllObjects();
MarkSceneAsDirty();
}
}
}
//Force unity to save changes or Unity may not save when we have instantiated/removed prefabs despite pressing save button
private void MarkSceneAsDirty()
{
UnityEngine.SceneManagement.Scene activeScene = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene();
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(activeScene);
}
//Instantiate prefabs at random positions within the circle
private void AddNewPrefabs()
{
//How many prefabs do we want to add
int howManyObjects = objectManager.howManyObjects;
//Which prefab to we want to add
GameObject prefabGO = objectManager.prefabGO;
for (int i = 0; i < howManyObjects; i++)
{
GameObject newGO = PrefabUtility.InstantiatePrefab(prefabGO) as GameObject;
//Send it to the main script to add it at a random position within the circle
objectManager.AddPrefab(newGO, center);
}
}
}
Now select the gamobject to which you attached the script. If everything is working, you should be able to do this:
If your terrain is not flat as it often is not if you are using Unity's terrain, then you just have to make another Raycast downwards from the random position to figure out where the ground is. You might also need to check if another tree is close to this position so the trees are not intersecting with each other.