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.