Finally - Removing trees AND the colliders

The problem: Removing a tree’s instance data from the terrain TreeInstances doesn’t remove the tree’s collider. So while the tree may go…you keep bumping into its invisible collider.

I had to do this:
float[,] heights = terrain.GetHeights(0, 0, 0, 0);
terrain.SetHeights(0, 0, heights);

to force the terrain to regenerate.
The gotcha (always gonna be one) is, large number of trees or details will cause a hitch. With 10000 trees alone, it caused a .25 second hitch in gameplay.
Luckily (for me), my terrains are relatively small, just for this type of thing. I tile them, so changes to any one terrain aren’t going to matter much. 2000 trees is barely noticeable.

Hope this helps others. If it is useful to you, I’d appreciate it if someone could post an even faster search algorithm…maybe a quadtree (yuk yuk) or something. The linear search probably isn’t the fastest. To be honest though…it is a VERY small amount of the time. The terrain regen is the big part.

Here is the code:

public class TreeChop : MonoBehaviour
{
    public GameObject FallingTreePrefab;
    private List<TreeInstance> TreeInstances;
    // Use this for initialization
    private void Start()
    {
        TreeInstances = new List<TreeInstance>(Terrain.activeTerrain.terrainData.treeInstances);
        Debug.Log("Tree Instances:" + TreeInstances.Count);
    }

    // Update is called once per frame
    private void Update()
    {
        // did we click on a tree?
        if (Input.GetMouseButtonDown(0))
        {
            DateTime start = DateTime.Now;
            RaycastHit hit;
            // This ray will see where we clicked er chopped
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            // Did we hit anything at that point, out as far as 10 units?
            if (Physics.Raycast(ray, out hit, 10.0f))
            {
                // Did we even click er chop on the terrain/tree?
                if (hit.collider.name != Terrain.activeTerrain.name)
                {
                    // No, must have been a frantic attack on a giant spider >:)
                    return;
                }

                // We hit the "terrain"! Now, how high is the ground at that point?
                float sampleHeight = Terrain.activeTerrain.SampleHeight(hit.point);

                // If the height of the exact point we clicked/chopped at or below ground level, all we did
                // was chop dirt.
                if (hit.point.y <= sampleHeight + 0.01f)
                {
                    return;
                }

                TerrainData terrain = Terrain.activeTerrain.terrainData;
                TreeInstance[] treeInstances = terrain.treeInstances;

                // Our current closest tree initializes to far away
                float maxDistance = float.MaxValue;
                // Track our closest tree's position
                Vector3 closestTreePosition = new Vector3();
                // Let's find the closest tree to the place we chopped and hit something
                int closestTreeIndex = 0;
                for (int i = 0; i < treeInstances.Length; i++)
                {
                    TreeInstance currentTree = treeInstances[i];
                    // The the actual world position of the current tree we are checking
                    Vector3 currentTreeWorldPosition = Vector3.Scale(currentTree.position, terrain.size) + Terrain.activeTerrain.transform.position;

                    // Find the distance between the current tree and whatever we hit when chopping
                    float distance = Vector3.Distance(currentTreeWorldPosition, hit.point);

                    // Is this tree even closer?
                    if (distance < maxDistance)
                    {
                        maxDistance = distance;
                        closestTreeIndex = i;
                        closestTreePosition = currentTreeWorldPosition;
                    }
                }

                // Remove the tree from the terrain tree list
                TreeInstances.RemoveAt(closestTreeIndex);
                terrain.treeInstances = TreeInstances.ToArray();

                // Now refresh the terrain, getting rid of the darn collider
                float[,] heights = terrain.GetHeights(0, 0, 0, 0);
                terrain.SetHeights(0, 0, heights);

                // Put a falling tree in its place
                Instantiate(FallingTreePrefab, closestTreePosition, Quaternion.identity);
                Debug.Log(DateTime.Now - start);
            }
        }
    }
}
5 Likes

I felt in the same trap too when i want to modify my terrain at runtime. Instead of updating the whole terrain you should update only the cell that the tree reside in, something like terrain.SetHeights(x, y, heightValue);

I’ll give that a shot, though it seems to me that just calling SetHeights anywhere would update the entire terrain. In my example above:

float[,] heights = terrain.GetHeights(0, 0, 0, 0);
terrain.SetHeights(0, 0, heights);

This updates the entire terrain…but maybe it only does the entire terrain because I’m calling it with essentially empty values.

Thanks!

I am not saying to retrieve entire terrain data. Only the data at specific tile

float[,] heights = terrain.GetHeights(x, y, 1, 1);
terrain.SetHeights(x, y, heights);

This approach helped me to paint terrain at runtime in Update function with acceptable performance. So, doing it once is not going to be noticeable.

Hey thanks for posting the code. I have recently hit a similar roadblock after coding everything for my tree prefab I realized there was no way I could use it with the tree painting tool. Its too bad really :(. Seems we are trying to do similar things with trees falling over, in my case being chopped then falling over.

If you make any significant progress on this would you mind sharing? Would really help me out! You could post here or email me directly if you prefer not to post it.

mdavis6537@gmail.com

Hey just wanted to throw out another huge thanks for this script I have been able to use it to get my little tree chopping peice of my game up and running - works great. The lighting changes ever so slightly on the new instantiated tree but I dont know that the player who doesnt know what is happening will notice.

Also, I have found that if your player stands too close to the new tree it will go flying since it has a rigidbody, but I think I will be able to fix this by increasing the trunk collider size out a bit to keep them away. Thanks again!

This might happen because unity trees have a bit of a variance that you can choose, it affects the size, color, and so forth. What I do to side step this was copy the terrain tree data values for the that specific tree before deletion then used them to customize the instantiated falling tree model to match color, size and tilt angle. Don’t know if this is the case for you though.

Could someone perhaps help me in optimising my code. I am using SetHeights to remove the collider but I get a delay (approx. 1 second) when removing the tree. If I omit the code (shown below) the tree is removed instantaneously, but the collider remains.

float[,]heights=terrain.terrainData.GetHeights(Mathf.FloorToInt(treesInVacinity_.position.x),Mathf.FloorToInt(treesInVacinity*.position.z),1,1);_
terrain.terrainData.SetHeights(Mathf.FloorToInt(treesInVacinity_.position.x),Mathf.FloorToInt(treesInVacinity.position.z), heights);*
treesInVacinity is a list of TreeInstances.
Any help would be appreciated!_

He I found a way how to do it without SetHeights

http://rene.klacan.sk/unity3d/games/2014/10/28/destroyable-terrain-trees-in-unity/

float hmWidth = grav.currentTerrain.terrainData.heightmapWidth;
float hmHeight = grav.currentTerrain.terrainData.heightmapHeight;
// get the normalized position of this game object relative to the terrain
Vector3 tempCoord = (transform.position - grav.currentTerrain.gameObject.transform.position);
Vector3 coord;
coord.x = tempCoord.x / grav.currentTerrain.terrainData.size.x;
coord.y = tempCoord.y / grav.currentTerrain.terrainData.size.y;
coord.z = tempCoord.z / grav.currentTerrain.terrainData.size.z;
// get the position of the terrain heightmap where this game object is
int posXInTerrain = (int)(coord.x * hmWidth);
int posYInTerrain = (int)(coord.z * hmHeight);
// we set an offset so that all the raising terrain is under this game object
//int offset = size / 2;

// get the heights of the terrain under this game object
float[,] heights = grav.currentTerrain.terrainData.GetHeights(posXInTerrain, posYInTerrain, 1, 1);
grav.currentTerrain.terrainData.SetHeights(posXInTerrain, posYInTerrain, heights); //THIS CHANGES TERRAIN FOR GOOD

what does x and y stands for? raycast hit position x and y? i try to figure out your changings :confused:

Hello, I saw this code and I’m still trying to wrap my head around C#, for the “grav” instance/variable, what should it be set to with the OP code to work correctly?

grav is just a script of mine that keeps track of gravity type operations. It has a link to the current terrain. In grav I update the current terrain because I have multiple terrains. You can just delete that if you wanted

Awesome! Can’t wait to try this out!

Video of what I came up with so far:

I tried the above methods but it no longer seems to work in Unity 2017 or 2018. The tree is removed but the collider stays. Any tips?

wkronfeld, I’m in the same position. I was so excited to find this thread but that technique does nothing to remove colliders. Unity is always changing things and often not for the better. So, do they seriously have no way to remove a tree a runtime? It seems like it should be such a trivial task. I hope someone knows a workaround that doesn’t impact performance horribly.

Thanks for the directional help, this code has helped push a fun little concept I was toying with for a few days in the right direction. 2018.x does seem to make things a little strange with the splitting of Terrain.terrainData and the TerrainCollider.terrainData but I found a dirty work around over the last couple of hours of testing. I ran a basic idea through some paces so it’s very messy and needs to be properly plugged into variables…

remove this…

// Now refresh the terrain, getting rid of the darn collider
float[,] heights = terrain.GetHeights(0, 0, 0, 0);
terrain.SetHeights(0, 0, heights);

and essentially plug in your own variation of…

Terrain.activeTerrain.GetComponent<TerrainCollider>().enabled = false;
Debug.Log("BRIEF PAUSE IN THE RUN LINE!");
Terrain.activeTerrain.GetComponent<TerrainCollider>().enabled = true;

I am not at this moment sure if it will function as designed in a build version as I’m not able to make one at the moment due to a few other errors I’ve got going. But in editor that Debug print seems to be enough of a pause for the system to disable the collider and bring it back without characters falling through the world, and the collider updates itself to not have the tree collider still sitting there.

Best of luck to all of you. Hope this helps anyone else with the same issue, or maybe find a better solution.

2 Likes

Thank you! i am coding codes of cut trees, you helped me a lot!

1 Like