Search Unity

Question RayCast bullets with Job System?

Discussion in 'C# Job System' started by RogueStargun, Apr 28, 2023.

  1. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    I have a VR game that runs on very limited hardware (the Oculus Quest2)
    I have space battle scenes where upwards of 400 raycast bullets are flying at any given moment.

    The problem I'm encountering is fps is hovering around 50 fps with >100 raycast bullets (which update in FixedUpdate) and it seems the only way to get the performance bump I need to ship my game is to start multithreading the bullet logic.

    I'm struggling to find the best way to do this with Job system or whether its prudent to switch the bullet logic into DOTS. I've played with DOTs previously and am familiar with ECS (I've made a game using the Rust Bevy game engine), but I'd much rather do it with Jobs since I have no idea how to link ECS to the vanilla Unity physics colliders. I already have hundreds of prefabs with colliders setup, so switching to ECS physics might be out of the question?

    What exactly is the best way to handle raycast bullets?

    I imagine the bullet payload looks something like this, but I'm not sure whether this works with GameObjects within a struct within a NativeArray????

    Code (CSharp):
    1.     struct BulletData
    2.     {
    3.         public RaycastCommand raycastCommand;
    4.         public RaycastHit rayHit;
    5.         public float timeToLive;
    6.         public LayerMask hitMask;
    7.         public GameObject ttlExplosionPrefab;
    8.         public int maxNumPooledExplosions;
    9.         public float impactForce;
    10.         public int baseDamage;
    11.         public int ionDamage;
    12.         public int damageDecayPerSec;
    13.  
    14.     }
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    Have you tried to reduce the timestep for FixedUpdate? You may not need to update it every 0.02 seconds, it may suffice to do so every 0.04 seconds and that may be all you need to do to go above 60/75 Hz.

    Next, have you tried RaycastCommand.ScheduleBatch yet? I see it's part of your data.

    From the example:
    Code (CSharp):
    1.         // Schedule the batch of raycasts
    2.         JobHandle handle = RaycastCommand.ScheduleBatch(commands, results, 1, default(JobHandle));
    3.  
    4.         // Wait for the batch processing job to complete
    5.         handle.Complete();
    You may not want to Complete() right away. For example if a 1-frame lag is okay for your game, you could schedule the batch during Update() and put the handle in a field, and during FixedUpdate you complete the handle and process the collisions. Alternatively schedule in FixedUpdate() and complete during LateUpdate().

    This gives the batched raycasts ample time during a frame to complete while the rest of the game logic keeps running.
     
  3. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    I have reduced the time step to 0.025. will look into reducing it further.

    I was wondering if I could do the raycastcommand and transform update in a single job
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    You cannot access the Transform component (or any managed object) in a job.
    RaycastCommand.ScheduleBatch is already jobified though. If you already use that and it's still heavy you probably can't fix that besides writing your custom, bursted collision detection.
     
  5. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
  6. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    Right but it‘s technically not a Transform object but a TransformAccess struct. ;)
    But the question is, are the transform changes or the raycasts taking more cpu time? Did you profile? Focus on the big one first.
     
  7. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    How do I separate the transform cost from the raycast cost when viewing the timeline in the profiler?

    Both are performed in the same Move() method within the same FixedUpdate update call

    My intuition is that transform updates for 300 bullets on a Quest 2 should not be that significant compared to the 300 raycast queries

    I'm starting a refactor to move all my bullet updates to a Bullet manager singleton that uses a RaycastCommand

    There are also some other solutions... My test scene is actually two large capital ships firing massive amounts of lasers at each other. I could also simply reduce the number of lasers to fewer bigger deadlier lasers, but I find that (much like in the Star wars movies and games) it's way to easy to dodge these things!
     
  8. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
  9. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    If you don't see more detail than the Move() method I assume you haven't enabled "Deep Profiling". That will break it down into even the smallest internal methods.
    upload_2023-4-29_10-0-42.png
     
  10. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    Deep profile is showing the raycast cost for the roughly 300 on-screen bullets should be negligible
    I am seeing a weird situation where only 1 or 2 bullets are taking up a huge percentage of processing and its registering as from some sort of log string?

    Am I reading this incorrectly??
     

    Attached Files:

  11. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    The LogString is taking 0.17 of the 1.8 time for the entire function. Since that comes on top it would be the first thing to remove but it shouldn't make a huge difference.

    Of the 1.8 only 0.28 are accounted for which usually means the rest of the time is spent on something inside that method that isn't a method call. It could be a for loop that just does some simple math for instance. Feel free to post the code of that method.
     
  12. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    This is probably going to be somewhat embarassing.

    Move() is called in FixedUpdate()

    I suspect TryGetComponentInParent() (which is a custom extension method) can be quite expensive... it just wraps GetComponentInParent()

    Code (CSharp):
    1.        protected override void Move(float dT)
    2.         {
    3.             // Perform the raycast. Shoots a ray forwards of the bullet that covers all the distance
    4.             // that it will cover in this frame. This guarantees a hit in all but the most extenuating
    5.             // circumstances (against other extremely fast and small moving targets it may miss) and
    6.             // works at practically any bullet speed.
    7.  
    8.             if (Physics.Raycast(transform.position, velocity, out RaycastHit rayHit, velocity.magnitude * kVelocityMult * dT, hitMask))
    9.             {
    10.                 Transform rayHitTransform = rayHit.transform;
    11.                 // SHIELD COLLISION LOGIC
    12.                 // if hit shield
    13.                 if (rayHit.collider.gameObject.layer == ShieldLayer && ((hitMask.value | (1 << ShieldLayer)) == hitMask.value) && baseDamage > 0)
    14.                 {
    15.  
    16.                     // Note: The BaseShield should be on the same gameobject as the Rigidbody and will take damage
    17.                     // regardless of whether the ship is flying beneath the shield or not
    18.                     if (rayHitTransform.TryGetComponentInParent<BaseShield>(out BaseShield shield))
    19.                     {
    20.                         if (shield.enabled && shield.shieldHealth > 0)
    21.                         {
    22.                             shield.ApplyDamageAndVfx(currentDamage, rayHit.point);
    23.                         }
    24.                     }
    25.                 }
    26.                 else //if hit not a shield
    27.                 {
    28.                     // Note that shields protect a ship from tractor beams or anti-tractor beams
    29.                     // Note that tractorbeams still work if base damage is 0
    30.                     if (Mathf.Abs(impactForce) > 0 && rayHitTransform.TryGetComponent<Rigidbody>(out Rigidbody targetRb))
    31.                     {
    32.                         // Note we have to try finding the rigidbody in the parent
    33.                         // as most rigidbodies are nearer the root of the object transform
    34.                         // hierarchy
    35.                         // Apply impact/impulse forces
    36.                         targetRb.AddForce(velocity.normalized * impactForce, ForceMode.Impulse);
    37.                     }
    38.                     if (baseDamage > 0)
    39.                     {
    40.                         HealthDamage targetHealthDamage;
    41.                         if (rayHitTransform != rayHit.collider.transform)
    42.                         {
    43.                             // Get the nearest healthdamage to the impact collider if the impact collider
    44.                             // is not on the same GameObject as the main rigidbody
    45.                             targetHealthDamage = rayHit.collider.GetComponentInParent<HealthDamage>();
    46.                         }
    47.                         else
    48.                         {
    49.                             // Get the root health damage otherwise (the one on the same GameObject as the rigidbody)
    50.                             targetHealthDamage = rayHitTransform.GetComponent<HealthDamage>();
    51.                         }
    52.  
    53.                         if (targetHealthDamage != null && rayHit.point != null)
    54.                         {
    55.                             // Traverse up the transform hierarchy for the hit collider to find the HealthDamage
    56.                             // component. Note that this allows us to avoid nesting rigidbodies which can screw up
    57.                             // physics! For example, you can destroy a turret parented to a rigidbody effectively
    58.                             // Apply bullet damage and tell the targetHealthSystem where to place damage VFX
    59.                             //Vector3 toCenter = (rayHit.transform.position - transform.position).normalized;
    60.                             //targetHealth.ApplyBulletDamage(damage, transform.position + 3.5f * toCenter);
    61.                             #region
    62. #if UNITY_EDITOR
    63.                             if (allegiance == null)
    64.                             {
    65.                                Debug.LogError($"Missing allegiance in {this.gameObject.name}");
    66.                             }
    67. #endif
    68.                             #endregion
    69.                             targetHealthDamage.ApplyBulletDamage(currentDamage, rayHit.point, rayHit.normal, allegiance);
    70.  
    71.                         }
    72.  
    73.                         // The rayHitTransform gives you the rigidbody transform not the collider transform
    74.                         if (rayHitTransform.TryGetComponent<PhysicsBullet>(out PhysicsBullet targetBullet))
    75.                         {
    76.                             // If you hit a physics bullet, detonate the physics bullet
    77.                             targetBullet.DefaultProjectileDestruction(rayHit.point);
    78.                         }
    79.                     }
    80.                 }
    81.  
    82.                 // Destroy the bullet.
    83.                 ProjectileDestruction(rayHit.point, explosionPrefab);
    84.             }
    85.             else
    86.             {
    87.                 // Bullet didn't hit anything, continue moving.
    88.                 transform.Translate(velocity * dT, Space.World);
    89.  
    90.                 // Account for bullet drop.
    91.                 if (gravityModifier != 0)
    92.                 {
    93.                     velocity += Physics.gravity * gravityModifier * dT;
    94.                 }
    95.  
    96.                 // Align to velocity
    97.                 if (alignToVelocity == true && velocity != Vector3.zero)
    98.                     transform.rotation = Quaternion.LookRotation(velocity);
    99.             }
    100.         }

    Code (CSharp):
    1.         /// <summary>
    2.         /// Destroy the projectile and play the necessary events.
    3.         /// </summary>
    4.         /// <param name="position">Where the event that destroyed the bullet happened.</param>
    5.         public virtual void ProjectileDestruction(Vector3 position, GameObject prefab)
    6.         {
    7.             if (prefab != null)
    8.             {
    9.                 GameObject pooledExplosion = objectPool.SpawnFromPool(
    10.                     prefab.name,
    11.                     position,
    12.                     transform.rotation
    13.                 );
    14.                 objectPool.TimedDestroy(pooledExplosion, 5f);
    15.                 //GameObject explosion = Instantiate(explosionPrefab, transform.position, transform.rotation);
    16.                 //Destroy(explosion, 5f);
    17.             }
    18.             if (TryGetComponent<ExplosiveShell>(out ExplosiveShell explosiveShell))
    19.             {
    20.                 // Set some parameters on the explosive shell
    21.                 explosiveShell.allegiance = allegiance;
    22.                 explosiveShell.hitMask = hitMask;
    23.                 explosiveShell.initialVelocity = GetVelocity();
    24.                 explosiveShell.Explode();
    25.             }
    26.  
    27.             //Destroy(gameObject);
    28.             Disable();
    29.         }
    30.  
     
  13. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    After running some android builds and doing more testing, it's actually rather ambiguous whether multithreading raycasts would actually help performance.

    The game actually runs at 72 FPS when removing all usage of Unity's Terrain system, so I think unity terrain is somehow severely harming performance.
     
  14. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,937
    Since there is not insignificant GC alloc every update (6.5 kb) you may want to try the non-alloc version of raycast first: https://docs.unity3d.com/ScriptReference/Physics.RaycastNonAlloc.html
    And generally look into optimizing GC allocations and reducing the null / object equality checks since they aren‘t free for Unity objects.

    But I think that GetComponentInParent is what‘s really hurting because this won‘t just check transform.parent but it will walk the entire scene hierarchy up to the root object. Rather each object that is hittable should deal with the impact itself. A single IsHittable component should handle what happens to the object being hit, and it will have cached the necessary components.The projectile itself only needs to deal with its own matters, like destroying itself or spawning an explosion fx.
     
  15. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    Ok those seem like reasonable optimizations. I really have very few collider objects that actually need GetComponentInParent (mainly turrets with collider bases).
     
  16. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    Ok, there were several suboptimal bits of code I uncovered while going through this including uncovering some redundant worldspace to localspace transformations in the shield system.

    Ultimately I completely eliminated usage of TryGetComponentInParent<>, instead caching all components in a new "Hittable" component for which I added a edit button to automatically add to all colliders in the game for anything with health or shields. Raycasting in the hundreds is absolutely negligible in performance compared to doing queries up the transform hierarchy.

    However, I realized while deep profiling that ultimately, the raycasts and bullets are rather negligible compared to the massive performance hit wrought by using Unity's terrain system. Simply disabling terrains entirely gives me 72 fps space battles with massive hordes of enemy ships.

    Honestly this exercise has made me wonder why Unity invested so much into things like DOTS and ECS when the terrain system sucks out such a massive amount of performance. Querying up a transform hierarchy is no better with ECS than with regular ol' gameobjects!

    I actually slogged through terrain optimizations last year, but working around all the performance issues with UnityTerrains was so tiresome, I ultimately opted to buy the Terrain To Mesh asset, and that'll be the final major performance optimization I take a crack at.
     
  17. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    296
    Update:
    - Optimizing the raycasts and transform traversal led to some barely noticable improvement in the profiler
    - The real hog killing performance of the game was Unity's Terrain system which seems to run terribly on the Oculus Quest2. Went from 40 fps -> a consistent 72 fps using this asset: https://assetstore.unity.com/packages/tools/terrain/terrain-to-mesh-195349
     
  18. pan4ezzz

    pan4ezzz

    Joined:
    Jul 9, 2017
    Posts:
    19