Search Unity

Finally - Removing trees AND the colliders

Discussion in 'Scripting' started by jc_lvngstn, Nov 2, 2011.

  1. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    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:

    Code (csharp):
    1.  
    2.  
    3. public class TreeChop : MonoBehaviour
    4. {
    5.     public GameObject FallingTreePrefab;
    6.     private List<TreeInstance> TreeInstances;
    7.     // Use this for initialization
    8.     private void Start()
    9.     {
    10.         TreeInstances = new List<TreeInstance>(Terrain.activeTerrain.terrainData.treeInstances);
    11.         Debug.Log("Tree Instances:" + TreeInstances.Count);
    12.     }
    13.  
    14.     // Update is called once per frame
    15.     private void Update()
    16.     {
    17.         // did we click on a tree?
    18.         if (Input.GetMouseButtonDown(0))
    19.         {
    20.             DateTime start = DateTime.Now;
    21.             RaycastHit hit;
    22.             // This ray will see where we clicked er chopped
    23.             Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    24.  
    25.             // Did we hit anything at that point, out as far as 10 units?
    26.             if (Physics.Raycast(ray, out hit, 10.0f))
    27.             {
    28.                 // Did we even click er chop on the terrain/tree?
    29.                 if (hit.collider.name != Terrain.activeTerrain.name)
    30.                 {
    31.                     // No, must have been a frantic attack on a giant spider >:)
    32.                     return;
    33.                 }
    34.  
    35.                 // We hit the "terrain"! Now, how high is the ground at that point?
    36.                 float sampleHeight = Terrain.activeTerrain.SampleHeight(hit.point);
    37.  
    38.                 // If the height of the exact point we clicked/chopped at or below ground level, all we did
    39.                 // was chop dirt.
    40.                 if (hit.point.y <= sampleHeight + 0.01f)
    41.                 {
    42.                     return;
    43.                 }
    44.  
    45.                 TerrainData terrain = Terrain.activeTerrain.terrainData;
    46.                 TreeInstance[] treeInstances = terrain.treeInstances;
    47.  
    48.                 // Our current closest tree initializes to far away
    49.                 float maxDistance = float.MaxValue;
    50.                 // Track our closest tree's position
    51.                 Vector3 closestTreePosition = new Vector3();
    52.                 // Let's find the closest tree to the place we chopped and hit something
    53.                 int closestTreeIndex = 0;
    54.                 for (int i = 0; i < treeInstances.Length; i++)
    55.                 {
    56.                     TreeInstance currentTree = treeInstances[i];
    57.                     // The the actual world position of the current tree we are checking
    58.                     Vector3 currentTreeWorldPosition = Vector3.Scale(currentTree.position, terrain.size) + Terrain.activeTerrain.transform.position;
    59.  
    60.                     // Find the distance between the current tree and whatever we hit when chopping
    61.                     float distance = Vector3.Distance(currentTreeWorldPosition, hit.point);
    62.  
    63.                     // Is this tree even closer?
    64.                     if (distance < maxDistance)
    65.                     {
    66.                         maxDistance = distance;
    67.                         closestTreeIndex = i;
    68.                         closestTreePosition = currentTreeWorldPosition;
    69.                     }
    70.                 }
    71.  
    72.                 // Remove the tree from the terrain tree list
    73.                 TreeInstances.RemoveAt(closestTreeIndex);
    74.                 terrain.treeInstances = TreeInstances.ToArray();
    75.  
    76.                 // Now refresh the terrain, getting rid of the darn collider
    77.                 float[,] heights = terrain.GetHeights(0, 0, 0, 0);
    78.                 terrain.SetHeights(0, 0, heights);
    79.  
    80.                 // Put a falling tree in its place
    81.                 Instantiate(FallingTreePrefab, closestTreePosition, Quaternion.identity);
    82.                 Debug.Log(DateTime.Now - start);
    83.             }
    84.         }
    85.     }
    86. }
    87.  
     
  2. scarpelius

    scarpelius

    Joined:
    Aug 19, 2007
    Posts:
    966
    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);
     
  3. jc_lvngstn

    jc_lvngstn

    Joined:
    Jul 19, 2006
    Posts:
    1,508
    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!
     
  4. scarpelius

    scarpelius

    Joined:
    Aug 19, 2007
    Posts:
    966
    I am not saying to retrieve entire terrain data. Only the data at specific tile
    Code (csharp):
    1.  
    2. float[,] heights = terrain.GetHeights(x, y, 1, 1);
    3. terrain.SetHeights(x, y, heights);
    4.  
    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.
     
    Last edited: Nov 2, 2011
  5. madmike6537

    madmike6537

    Joined:
    Aug 20, 2012
    Posts:
    49
    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
     
  6. madmike6537

    madmike6537

    Joined:
    Aug 20, 2012
    Posts:
    49
    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!
     
  7. BPPHarv

    BPPHarv

    Joined:
    Jun 9, 2012
    Posts:
    318
    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.
     
  8. RoloJo

    RoloJo

    Joined:
    Feb 26, 2013
    Posts:
    28
    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!
     
  9. reneklacan

    reneklacan

    Joined:
    Apr 12, 2013
    Posts:
    1
    Last edited: Apr 18, 2015
  10. wightwhale

    wightwhale

    Joined:
    Jul 28, 2011
    Posts:
    397
    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
     
  11. soulreafer22

    soulreafer22

    Joined:
    May 12, 2015
    Posts:
    15
    what does x and y stands for? raycast hit position x and y? i try to figure out your changings :/
     
  12. scarpelius

    scarpelius

    Joined:
    Aug 19, 2007
    Posts:
    966
  13. teethree

    teethree

    Joined:
    Feb 9, 2016
    Posts:
    2
    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?
     
  14. wightwhale

    wightwhale

    Joined:
    Jul 28, 2011
    Posts:
    397
    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
     
  15. teethree

    teethree

    Joined:
    Feb 9, 2016
    Posts:
    2
    Awesome! Can't wait to try this out!

    Video of what I came up with so far:
     
  16. wkronfeld

    wkronfeld

    Joined:
    Sep 17, 2016
    Posts:
    2
    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?
     
  17. ChristmasEve

    ChristmasEve

    Joined:
    Apr 21, 2015
    Posts:
    45
    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.
     
  18. ZSP_NNY_DeNevar

    ZSP_NNY_DeNevar

    Joined:
    May 6, 2019
    Posts:
    1
    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...
    Code (CSharp):
    1.  
    2. // Now refresh the terrain, getting rid of the darn collider
    3. float[,] heights = terrain.GetHeights(0, 0, 0, 0);
    4. terrain.SetHeights(0, 0, heights);
    5.  
    and essentially plug in your own variation of...
    Code (CSharp):
    1. Terrain.activeTerrain.GetComponent<TerrainCollider>().enabled = false;
    2. Debug.Log("BRIEF PAUSE IN THE RUN LINE!");
    3. 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.
     
    Romenics and Halo_Shrimp like this.
  19. Halo_Shrimp

    Halo_Shrimp

    Joined:
    Aug 27, 2021
    Posts:
    11
    Thank you! i am coding codes of cut trees, you helped me a lot!
     
    Jucari likes this.