How Do I Interact With Terrain Trees?

How do I interact with Terrain Trees?

A terrain tree: painted via the Editor / Place Trees brush, or via use of the Mass Place Trees function, or via modification of terrainData from script. A terrain tree is not present in the project Hierarchy.

A placed tree: is visible in the Hierarchy and typically positioned manually or via scripts.

There is a common misconception that a developer, especially one working on a Sandbox, Build, RTS, Survival or Crafting system game, can build a functionality set on a placed tree (harvest, gather wood, lumberjack, chop, etc.) that might include scripts, triggers or components, turn that into a prefab then paint that tree onto the terrain and all the custom behaviours will follow. They do not. Nothing of the sort works.

This is a system shock to many developers (myself included) when they realize their whole concept needs to be reconsidered.

A sample solution is provided via two scripts.

Script #1 is attached to your Player.
Script #2 is attached to a GameManager singleton-like object that is tagged as “GameManager”, adapt to your needs.

Script #1 - attached to Player, will look for GameManager tagged object so change that line if you do something different.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

// Purpose: demonstrate script interface to interact with terrain trees

// Steps: Attach this to main(parent/top) Player gameObject, or adjust myTransform to your hierarchy,
// define Inspector values for harvestTreeDistance, respawnTimer
// Setup a prefab tree with CAPSULE collider, how to at bottom of
// http://docs.unity3d.com/Documentation/Components/terrain-Trees.html
// paint terrain tree 

// Assign the non-collider prefab version of the tree to felledTree
// press Play, left click on terrain tree

// Harvested terrain tree info is passed to a manager object QM_ResourceManager for respawn management,
// you'll need that too or you could comment out any functionality related to manager 

// Note: this is not a demo of modifying terrainData permanently - there's enough risk involved with that
// such that you shouldn't try it unless you know what you are doing.


public class QM_HarvestTerrainTree : MonoBehaviour {

	// Player, Range
	public int harvestTreeDistance;		// Set [Inspector] min. distance from Player to Tree for your scale?
	public bool rotatePlayer = true;	// Should we rotate the player to face the Tree? 
	private Transform myTransform;		// Player transform for cache

	// Terrains, Hit
	private Terrain terrain;			// Derived from hit...GetComponent<Terrain>
	private RaycastHit hit;				// For hit. methods
	private string lastTerrain;			// To avoid reassignment/GetComponent on every Terrain click

	// Tree, GameManager
	public GameObject felledTree;		// Prefab to spawn at terrain tree loc for TIIIIIIMBER!
	private QM_ResourceManager rMgr;	// Resource manager script
	public float respawnTimer;			// Duration of terrain tree respawn timer

	void Start () {

		if (harvestTreeDistance <= 0) {
			Debug.Log ("harvestTreeDistance unset in Inspector, using value: 6");
			harvestTreeDistance = 6;
		}

		if (respawnTimer <= 0) {
			Debug.Log ("respawnTimer unset in Inspector, using quick test value: 15");
			respawnTimer = 15;
		}

		myTransform = transform;
		lastTerrain = null;
		rMgr = GameObject.FindGameObjectWithTag ("GameManager").GetComponent<QM_ResourceManager> ();

	}


	void Update () {
	
		if (Input.GetMouseButtonUp (0)) {

			Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
			if (Physics.Raycast (ray, out hit, 30f)) {

				// Did we click a Terrain?
				if(hit.collider.gameObject.GetComponent<Terrain>() == null)
					return;

				// Did we click on the same Terrain as last time (or very first time?)
				if(lastTerrain == null || lastTerrain != hit.collider.name) {
					terrain = hit.collider.gameObject.GetComponent<Terrain>();
					lastTerrain = terrain.name;
				}

				// Was it the terrain or a terrain tree, based on SampleHeight()
				float groundHeight = terrain.SampleHeight(hit.point);

				if(hit.point.y - .2f > groundHeight) {

					// It's a terrain tree, check Proximity and Harvest
					if(CheckProximity()) 
						HarvestWood();

				}
			}
		}
	}


	private bool CheckProximity() {
		bool inRange = true;
		float clickDist = Vector3.Distance (myTransform.position, hit.point);

		if (clickDist > harvestTreeDistance) {
			Debug.Log ("Out of Range");
			inRange = false;
		}

		return inRange;

	}

	private bool CheckRecentUsage(string _terrainName, int _treeINDEX) {
		bool beenUsed = false;

		for (int cnt=0; cnt < rMgr.managedTrees.Count; cnt++) {
			if (rMgr.managedTrees[cnt].terrainName == _terrainName && rMgr.managedTrees [cnt].treeINDEX == _treeINDEX) {
				Debug.Log ("Tree has been used recently");
				beenUsed = true;
			}
		}

		return beenUsed;
	}


	private void HarvestWood() {
		int treeIDX = -1;
		int treeCount = terrain.terrainData.treeInstances.Length;
		float treeDist = harvestTreeDistance;
		Vector3 treePos = new Vector3 (0, 0, 0);
				
		// Notice we are looping through every terrain tree, 
		// which is a caution against a 15,000 tree terrain

		for (int cnt=0; cnt < treeCount; cnt++) {
			Vector3 thisTreePos = Vector3.Scale (terrain.terrainData.treeInstances [cnt].position, terrain.terrainData.size) + terrain.transform.position;
			float thisTreeDist = Vector3.Distance (thisTreePos, hit.point);

			if (thisTreeDist < treeDist) {
				treeIDX = cnt;
				treeDist = thisTreeDist;
				treePos = thisTreePos;
			}
		}


		if (treeIDX == -1) {
			Debug.Log ("Out of Range");
			return;
		}

		if(!CheckRecentUsage(terrain.name, treeIDX)) {

			// Success - all tests passed
			// Place a cube to show the tree, the ResourceManager will remove it after a time
			// Obviously tweak to your liking, just a visual aid to show it worked
		
			GameObject marker = GameObject.CreatePrimitive (PrimitiveType.Cube);
			marker.transform.position = treePos;

			// Example of spawning a placed tree at this location, just for demo purposes
			// it will slide through terrain and disappear in 4 seconds
			GameObject fellTree = Instantiate(felledTree,treePos,Quaternion.identity) as GameObject;
			fellTree.gameObject.AddComponent<Rigidbody>();

			Destroy(fellTree,4);

			// Add this terrain tree and cube to our Resource Manager for demo purposes
			rMgr.AddTerrainTree(terrain.name, treeIDX, Time.time+respawnTimer, marker.transform);

			if (rotatePlayer) {
				Vector3 lookRot = new Vector3 (hit.point.x, myTransform.position.y, hit.point.z);
				myTransform.LookAt (lookRot);
			}
		
			// There are too many guesses to be made here about your game mechanics to code the rest
			// For example, you might..
		
			// Start Animation
			// Play Sound Clip
			// Give player an Inventory item
			// Bump lumberjacking skill
			// Random.Range[] spawn a Forest Protector Spirit of Woe
			// etc.

		}
	}
}

Script #2 is attached to a GameManager singleton-like object that is tagged as “GameManager”, adapt to your needs.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

// Attach this to a Singleton-like GameObject of which there is only and will ever only be one,
// ala classic GameManager object.

public class QM_ResourceManager : MonoBehaviour {

	// Define List component
	public class QM_Tree {
		public string terrainName { get; set; }
		public int treeINDEX { get; set; }
		public float respawnTime { get; set; }
		public Transform marker { get; set; }

	// Constructor
	public QM_Tree(string _terrainName, int _treeINDEX, float _respawnTime, Transform _marker) {
		terrainName = _terrainName;
		treeINDEX = _treeINDEX;
		respawnTime = _respawnTime;
		marker = _marker;
	}
		
	}

	// Tree Harvest script access
	public List<QM_Tree> managedTrees = new List<QM_Tree>();

	void Start() {
		// Scan for tree to "respawn" (remove cube, make available again) every 15 seconds
		// Adjust to your needs using a fast spawn here for demo
		InvokeRepeating ("RespawnTree", 15, 15);
	}


	private void RespawnTree() {

		if (managedTrees.Count == 0)
			return;

		// Removing the demo cube and allowing tree to be used again
		for (int cnt=0; cnt < managedTrees.Count; cnt++) {
			if(managedTrees[cnt].respawnTime < Time.time) {
				Destroy(managedTrees[cnt].marker.gameObject);
				managedTrees.RemoveAt(cnt);
				return;
			}

		}
	}


	public void AddTerrainTree(string _terrainName, int _treeIDX, float _respawnTime, Transform _marker) {

		managedTrees.Add (new QM_Tree(_terrainName, _treeIDX, _respawnTime, _marker));
	}
}

Is there a way to remove that tree (treeidx) from rendering? Or, because it’s static, is it always going to be in the scene?