Search Unity

Non-moving rigidbody objects degrade 2D raycast performance over time

Discussion in 'Physics' started by AdmiralOberstein, Dec 25, 2019.

  1. AdmiralOberstein

    AdmiralOberstein

    Joined:
    Dec 20, 2019
    Posts:
    10
    Hello,

    I've run into a problem with 2D raycasts which seem to be getting slower over time, but only if no force is applied to some permanent objects in the scene. Here is a small test setup to reproduce the problem (code at the bottom, using Unity 2019.2.17f1; i can also provide the whole project if needed):
    1. Spawn 1000 objects at start of simulation (not moving, but with rigidbody and collider).
    2. Every few frames, create 1000 objects (also with rigidbody and collider, but moved by adding random forces each frame), wait for a few frames and then delete them again.
    3. Every frame, do a few raycasts per object created in step 2 (for those that currently exist).
    This degrades the performance of the raycasts over time, even though the number of casts and objects in the scene will always be the same (1000 non moving objects from step 1 and 1000 objects from step 2).
    To verify this, open the profiler and just keep track of the time required to run the script for about 2 minutes or so.

    However, the performance does NOT decrease, if we add a random force to each of the 1000 objects from step 1 each frame.
    This really surprised me, since adding random forces (and therefore movement) to the objects should only make things harder for the engine. In fact, the performance returns to normal levels after applying a force to objects created in step 1, even when starting to apply forces later in the simulation. When stopping to apply the force, performance will degrade again, until force is applied again. The attached screenshot shows what happens when you start applying the force (performance suddenly returns to the level it has been when the simulation started). Almost all of the time in orange is due to raycasts:

    PerformanceAfterApplyingForce.png

    TLDR
    : The raycast performance gets worse if objects stop moving (i.e. no force applied), and gets better as soon as objects start moving again.

    Is there any way to avoid this behavior? Also, is there no way of running rayscasts in parallel for Physics2D or any other way to improve performance? Using jobs or threads throws "UnityException: get_queriesHitTriggers can only be called from the main thread".

    Here's the code that can be added to an empty gameobject (the "Test Prefab" field has to be filled with a prefab having a Ridigbody2D and a BoxCollider2D. Also make sure to turn off "Auto Simulation" in Physics 2D settings and disable the option for "Queries start in colliders").

    Code (CSharp):
    1. /*
    2. *
    3. * Controls
    4. *    Press "r" to respawn the non-moving objects (improves performance again),
    5. *    Press "x" to apply some random force to the objects (improves performance again; if not, press it a few times in a row)
    6. *
    7. *
    8. */
    9. public class GameLogic : MonoBehaviour
    10. {
    11.  
    12.   public GameObject testPrefab; // Test box object with rigidbody and collider
    13.  
    14.  
    15.   private List<GameObject> testObjects = new List<GameObject>();
    16.   private List<GameObject> testObjectsStatic = new List<GameObject>();
    17.  
    18.   private int frameNum = 0;
    19.   private int curStep = 0;
    20.  
    21.  
    22.   Vector3 GetRandVector()
    23.   {
    24.     float worldSize = 5000.0f;
    25.     return new Vector3(UnityEngine.Random.Range(-worldSize, worldSize), UnityEngine.Random.Range(-worldSize, worldSize), 0);
    26.   }
    27.  
    28.  
    29.   void Update()
    30.   {
    31.     frameNum++;
    32.     if (frameNum % 10 == 0) Debug.Log($"Frame {frameNum}, Step {curStep}");
    33.  
    34.     Physics2D.Simulate(Time.fixedDeltaTime); // Make sure to turn off auto physics in the settings
    35.  
    36.  
    37.     // SETTINGS
    38.     int maxSteps = 301; // keep this uneven
    39.     int objectsPerRun = 1000;
    40.     int fpsPerRun = 50;
    41.     bool startRaysInDynamicObjects = true;
    42.     int stopDegradePerformanceStep = 100000; // Step when we want to stop degrading the performance by starting to move the first 1000 objects
    43.     // #########
    44.  
    45.  
    46.     // Stepping
    47.     bool newStep = false;
    48.     if (frameNum % fpsPerRun == 0)
    49.     {
    50.       curStep++;
    51.       newStep = true;
    52.     }
    53.  
    54.     // (1) Spawn "static" objects (meaning they exist the whole time, but they are still "Dynamic" rigid bodies and can move and collide)
    55.     if (newStep && curStep == 1 || Input.GetKeyDown(KeyCode.R))
    56.     {
    57.       for (int i = 0; i < testObjectsStatic.Count; i++) Destroy(testObjectsStatic[i].gameObject);
    58.       testObjectsStatic.Clear();
    59.       for (int i = 0; i < objectsPerRun; i++)
    60.       {
    61.         GameObject c = GameLogic.Instantiate(testPrefab, GetRandVector(), Quaternion.identity);
    62.         Rigidbody2D rb = c.GetComponent<Rigidbody2D>();
    63.         // rb.sleepMode = RigidbodySleepMode2D.NeverSleep; // Doesn't stop performance from getting worse
    64.         testObjectsStatic.Add(c);
    65.       }
    66.     }
    67.  
    68.     // (2a) Create dynamic objects every uneven step (without spawning and despawning these, the performance would not degrade)
    69.     if (newStep && curStep % 2 == 1 && curStep <= maxSteps) {
    70.       for (int i = 0; i < objectsPerRun; i++)
    71.       {
    72.         GameObject c = GameLogic.Instantiate(testPrefab, GetRandVector(), Quaternion.identity);
    73.         Rigidbody2D rb = c.GetComponent<Rigidbody2D>();
    74.         testObjects.Add(c);
    75.       }
    76.     }
    77.  
    78.     // (2b) Destroy dynamic objects every even step
    79.     if (newStep && curStep % 2 == 0 && curStep <= maxSteps)
    80.     {
    81.       for (int i = 0; i < testObjects.Count; i++)
    82.       {
    83.         Destroy(testObjects[i].gameObject);
    84.       }
    85.       testObjects.Clear();
    86.     }
    87.  
    88.     // (2c) Move our dynamic objects around
    89.     foreach (GameObject c in testObjects) {
    90.       c.GetComponent<Rigidbody2D>().AddForce(new Vector2(UnityEngine.Random.Range(-3.0f, +3.0f), UnityEngine.Random.Range(-3.0f, +3.0f)));
    91.     }
    92.  
    93.     // (3) MAGICALLY PREVENT PERFORMANCE LOSS BY APPLYING RANDOM FORCES TO THE BODY
    94.     // If we start moving the objects in e.g. step 100, the performance will return no normal levels (as seen in step 1) again!
    95.     if (curStep == stopDegradePerformanceStep || Input.GetKeyDown(KeyCode.X))
    96.     {
    97.       Debug.Log("Applying force to non-moving objects.");
    98.       foreach (GameObject c in testObjectsStatic)
    99.       {
    100.         Rigidbody2D rb = c.GetComponent<Rigidbody2D>();
    101.         //rb.AddForce(new Vector2(0.001f,0.001f)); // Not enough to stop performance degradation?!
    102.         rb.AddForce(new Vector2(UnityEngine.Random.Range(-10.0f, +10.0f), UnityEngine.Random.Range(-10.0f, +10.0f)));
    103.       }
    104.     }
    105.  
    106.     // (4) Raycasts
    107.     List<GameObject> startRayList = (startRaysInDynamicObjects) ? testObjects : testObjectsStatic;
    108.     foreach (GameObject c in startRayList)
    109.     {
    110.       for (int i = 0; i < 15; i++)
    111.       {
    112.         Vector2 dir = new Vector2(Random.Range(-1.0f, +1.0f), Random.Range(-1.0f, +1.0f));
    113.         dir.Normalize();
    114.         Debug.DrawRay(c.transform.position, dir * 10, Color.red);
    115.         //int oldLayer = c.gameObject.layer;
    116.         //c.gameObject.layer = Physics2D.IgnoreRaycastLayer;
    117.         RaycastHit2D hit = Physics2D.Raycast(c.transform.position, dir, 10.0f);
    118.         //c.gameObject.layer = Physics2D.IgnoreRaycastLayer;
    119.       }
    120.     }
    121.  
    122.     // (5) Other tests
    123.     // (5.1) Wake up the objects
    124.     if (Input.GetKeyDown(KeyCode.Alpha1))
    125.     {
    126.       Debug.Log("Waking up objects");
    127.       foreach (GameObject c in testObjectsStatic)
    128.       {
    129.         Rigidbody2D rb = c.GetComponent<Rigidbody2D>();
    130.         rb.sleepMode = RigidbodySleepMode2D.NeverSleep; // Doesn't stop performance from getting worse
    131.       }
    132.     }
    133.  
    134.   }
    135.  
    136.  
    137. }
    138.  
     
    doarp likes this.
  2. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,474
    None of this makes sense to me either. Raycast has nothing whatsoever to do with movement of rigidbodies. If you have the whole test project then I'd be happy to take a look. Know that if you generate dynamic bodies overlapping then they need to be solved and will produce lots of contacts so they push apart which can take time and costs but performance will improve when the stop overlapping. Just guessing as to the cause.

    The code above doesn't profile the raycast specifically though and timing the whole function which contains a lot of non-physics code could mean it's anything in there so I can only presume you've got detail showing the raycast call itself is what's taking the time?

    I can't say it's this but maybe try turning off AutoSyncTransforms but I have no basis to say it's related to this, just another guess without more info.
     
  3. AdmiralOberstein

    AdmiralOberstein

    Joined:
    Dec 20, 2019
    Posts:
    10
    Thanks for your reply!

    Almost all of the time in the script is due to raycasts:

    Result 04 - Before Force Times.png Result 01 - Before Force Times.png Result 05 - After Force Times.png

    In the attached project, simply start the game and wait for about 300 steps (as counted in the console log). Raycast performance will get worse each frame (maybe this takes more or less steps depending on the CPU). After 300 steps, press "x" a few times (will apply forces) and watch the performance get better. The auto transform setting doesn't seem to make any difference.

    Edit: Added another image
     

    Attached Files:

    • Test.7z
      File size:
      13.3 KB
      Views:
      348
    Last edited: Dec 28, 2019
  4. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,474
    I've tried your project but I don't get what you see. All I see is the update/raycasts taking time then the objects are deleted which means it takes very little time. Pressing X doesn't seem to change this.

    Waiting for 300 steps takes a very very long time (many minutes).
    https://oc.unity3d.com/index.php/s/mLDgzfyJTnxt0ZY
     
  5. AdmiralOberstein

    AdmiralOberstein

    Joined:
    Dec 20, 2019
    Posts:
    10
    Thanks for trying out the project. It's right that 300 steps take a while, but the performance degrades very slowly, so it's necessary to wait. After the 300 steps, the objects stop spawning/despawning, which is when i created the screenshots in my previous posts. If i take a screenshot before then, it looks like this (this is after about 5 minutes, step ~100):
    result.png
    As you can see here, there's still a change when pressing "x" (I pressed it during the first few frames shown in the timeline, and performance gets better again), but not that noticeable. I will also try to reproduce this problem on another computer.
     
  6. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,474
    What i don't understand is that you're saying adding a force to static rigidbodies makes it go fast. The thing is, adding a force to a static bodies does nothing as the call checks to see if it's dynamic and just returns if it isn't so it's basically a NULL operation.

    Code (CSharp):
    1.     if (curStep == stopDegradePerformanceStep || Input.GetKeyDown(KeyCode.X))
    2.     {
    3.       Debug.Log("Applying force to non-moving objects.");
    4.       foreach (GameObject c in testObjectsStatic)
    5.       {
    6.         Rigidbody2D rb = c.GetComponent<Rigidbody2D>();
    7.         //rb.AddForce(new Vector2(0.001f,0.001f)); // Not enough to stop performance degradation?!
    8.         rb.AddForce(new Vector2(UnityEngine.Random.Range(-10.0f, +10.0f), UnityEngine.Random.Range(-10.0f, +10.0f)));
    9.       }
    10.     }
    Very confusing.
     
  7. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,474
    Hold on, by static you mean things that don't get added and removed not a static rigidbody.
     
  8. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,474
    btw, I'm not doubting you have an issue at all. I just need to reproduce it easily and dig into exactly what is causing it. I do see a rise in time to do 15000 raycasts though. I can add some nested profiler entries to see which part of the raycast that is but we offer a very very thin wrapper around the Box2D raycast so if anything it's going to be (potentially) some kind of issue with the Box2D broadphase after all it is a dynamic tree and maybe the fact that your world is large 5km it's not performing well with all the insertions and deletions. Again, another complete guess.
     
    AdmiralOberstein likes this.
  9. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,474
    So using your project I can see that the quality of the dynamic tree in the Box2D broadphase is slowly getting worse which reduces performance as there's a lot of non-leaf nodes to traverse when it's searching for leafs in the tree (colliders) during the raycast: b2DynamicTree:Raycast.

    It's actually hard to see what's going on here to figure out what the weakness is. Forcing a rebuild/balance of the tree solves it but that's an expensive operation to perform. Also, when you have moving objects then they tend to get removed and reinserted and the tree branch gets rebalanced. Very slow movements mean they don't get removed/reinserted as that is a performance feature. This is probably why when a force is added, they move and the tree quality improves.

    Over time the tree is getting more complex as you can see here:
    Start: https://oc.unity3d.com/index.php/s/wzomh0WPhjdgsW0
    Later: https://oc.unity3d.com/index.php/s/SCNbPWK6HijHfTf

    Here you can see what happens when you start moving your bodies (Press X). Note the tree gets much simpler resulting in far better performance:
     
    Last edited: Dec 29, 2019
    AdmiralOberstein likes this.
  10. AdmiralOberstein

    AdmiralOberstein

    Joined:
    Dec 20, 2019
    Posts:
    10
    Wow, thanks for the detailed analysis! Is there any way one can rebalance the tree manually?

    Also, is there a way to somehow do the raycasts in parallel to speed things up? I found a few old threads and the RaycastCommand class seems to do something like that, but for 3D only.
     
  11. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,474
    I prototyped the original batched queries back in an old Hackweek and whilst a few 3D queries were added by a 3D physics dev, nothing else was added so unfortunately no, they can't be currently run in jobs.

    Rebalancing the tree is expensive and it's just not needed typically. It could be exposed I suppose but it's not currently. To be honest, your set-up isn't typical and this is the very first time I've come across this, even searching similar topics of Box2D which is where the issue is. To be honest, this is potentially a bug too as opposed to an inefficiency of the Box2D dynamic tree.
     
    AdmiralOberstein likes this.