Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
Dismiss Notice
Join us now in the Performance Profiling Dev Blitz Day 2023 - Q&A forum where you can connect with our teams behind the Memory and CPU Profilers and the Frame Debugger.

DOTS for a projectile system?

Discussion in 'Physics for ECS' started by MadboyJames, May 8, 2022.

  1. MadboyJames


    Oct 28, 2017
    I am thinking of making a projectile system for a game. It would be entirely based on raycasts, even for slow moving projectiles. It would work by each "projectile" (a projectileData class) being added to a Manager game object (or more likely a Scriptable Object system... or even just a static class) and each frame the Manager iterates through it's list and raycasts from "bullet current location" to "bullet destination", which is just Time.DeltaTime*projectilespeed. Additionally, this would be implemented in a mostly OOP game, rather than a mostly DOTS game.

    Here the requirements I was thinking of (just copied over from my google doc)
    1. Trigger is pulled

    2. Accuracy drift determined. (the degree away from the barrel’s “forward” that the bullet is heading.)

    3. Spawn offset determined (the distance away from the barrel that the bullet spawns. Usually used on guns with high RoF to simulate barrel jerk from recoil)

    4. ProjectileData object created, added to global "traveling hitscan bullets" manager. (or scriptable object system thing?)

    5. Each frame, for each bullet in manager, raycast from their "current pos" to "dist traveled in frame". Then update the bullet pos to the traveled pos. Then apply new angle to adjust for bullet drop. Apply damage if target hit, and subtract peirce remaining. If peirce is less than 0, remove bullet.

    -bullet drop should be represented by an animation curve, and should have a min, and max.

    -bullet drop should also be nothing, or be very very slight for a good 50m-100m.

    -bullet drop should have a gravity modifier, which is calculated from a global “gravity” value. (real gravity acc is 9.8. We should have 10 be the baseline. 10m/s = multiplier of 1. This means that areas with less gravity have less projectile drop.)

    -bullet drop should be optional (missiles, magic, tracking-projectiles, and many self-propelled “smart” projectiles will not drop. however, they may run out of fuel/ duration, which should be represented elsewhere and may cause them to obey bullet drop and velocity deceleration).

    -projectile velocity should be represented by an animation curve, and should have a min, and max.

    -projectile velocity should not change for a good 50m-100m.

    -projectile velocity should have an “air density” modifier, which is calculated from a global “atmosphere thickness” value, and modified by regional atmosphere thickness, such as water. (real density is 1.2kg/ m^3. We should have 1 be the baseline. So 1kg/ m^3 = multiplier of 1. This means that areas with less density will have less bullet slowdown (such as vacuum))

    -in addition to modifying the velocity deceleration curve, the atmosphere thickness should also modify the real speed (for multipliers above 1, the bullet velocity is cut by a percent after all modifiers. If air at 1kg is 1, then water at 1000kg is a multiplier of 10. Realistically, the bullet in water would only travel a meter or so, but since that’s not fun, we’re just making it move slow and allowing it to decelerate much slower than it normally would. Use “velocity” and “multiplier*10” into "divide by bonus sum" to get the throttled bullet velocity.

    -bullet velocity should be optional (missiles, magic, tracking-projectiles, and many self-propelled “smart” projectiles will not decelerate. however, they may run out of fuel/ duration, which should be represented elsewhere and may cause them to turn into/ obey regular projectile system).

    -projectiles can have a “target” and a “turn degrees per millisecond”. “Turn degrees” is how fast or how many degrees per frame the projectile can turn left, right, up or down to orient towards a target.

    -the projectile can have an optional “lift gain per millisecond”. If this is disabled, then the projectile will obey “turn degrees” and ignore any projectile drop. If this is enabled, the bullet will obey bullet drop, and will only be able to orient up by the “lift gain” amount. This would work by first applying bullet drop angle, then orienting the projectile, then throttling the upwards angle if it exceeds the “lift gain” amount.

    -projectiles can have a list of “stages”. A stage represents a change in state of a projectile, and has a timer (time till the stage executes the change) which starts counting down as soon as the projectile is created. The stage also has an event trigger (like buttons do) which will fire all delegates when it triggers. This can be used to represent missiles running out of fuel, or something like “parachute” or “drag” bullets. Each projectile can have a list of stages

    -projectiles will have a maxDistance and lifetime variable. If either conditions are reached then the projectile is destroyed (removed from the “projectile system”).

    -have an onInit event, an onContact event, and onDestroyEvent. Add other events as needed.

    -projectiles would not deal damage or force on their own. The weapon that fired them would add a “deal damage” delegate to the projectile onContact event.

    -tie a particle to each projectile, and have it move each frame? (have it “teleport to end of ray” each frame? Need to research to see if that’s possible)"

    Now, I am aware that I am taking a data-oriented approach to this projectile system, and I was thinking that DOTS may be a good way to go for maximum performance. I don't have a specific, high projectile use case, but I would like to have ludicrously sized warships shoot thousands of projectiles at each other... at some point... or at least I'd like the option.

    So my questions are:

    -What challenges will I face with these requirements if I try to build it in DOTS?

    -Should I just go partway, using Burst Compiler and Jobs, but not ECS? (or burst compiler and ECS, but not jobs?)

    -If a player does something like "detonate all shots from their gun" or "retarget seeking projectiles", can I get those projectile references in an ECS system?

    -What networking challenges will be faced using DOTS if I make a multiplayer shooter where bullets may be redirected mid-flight?

    -Has someone already made something like this? I haven't looked in the asset store yet, but if someone knows of a good projectile system that either fits the criteria, or fits some of it and is easy to extend, I'd pay for that.

    I can make the non-DOTS version of this fairly easily. The physics of it is not too much of an issue, but if there is a uber-performant solution with SOLID architecture and multilayer capable with not-too-much modification, I'm all ears. (I don't expect a "golden bullet" to exist though, and I welcome the option to build it myself if I can figure out how I'll never have to build it again)
  2. MadboyJames


    Oct 28, 2017
    In doing a bit more research, it looks like there is not an already built projectile system that handels both pseudo-accurate physics, can be customized for target seeking, and is super performant.
    However, it looks like raycastCommand could help push a couple hundred raycast calls off the main thread.
  3. hippocoder


    Digital Ape Moderator

    Apr 11, 2010
    I do all my projectiles with raycasts in ECS and I have thousands. It doesn't register on profiler. Cast from the previous position to the current each frame and it won't miss anything.
  4. MadboyJames


    Oct 28, 2017
    What issues do you run into when creating such systems in ECS? I'm aware of the one issue where batched raycasts store the ID of the collider they hit, but not the reference, because you can't store a reference in a job. Any other things I could be aware of?
  5. hippocoder


    Digital Ape Moderator

    Apr 11, 2010
  6. MadboyJames


    Oct 28, 2017
    Interesting. Lots of good stuff there. Thanks!
    My approach would have all projectiles be raycasts, and the "bullet" graphic is nothing more than a mesh illusion being dragged along. I haven't dived into the code references on the other thread yet (and likely won't get a chance for a week or so). Is that approach applicable with ECS? I suppose the graphic part is, but the raycast system would likely be its own thing.
    Basically, every frame, everything that needs a raycast will ask a raycast manager (or raycast system) to cast it, and the raycast system will cast all the rays in a job once per frame. Give or take a frame or two, because jobs.
    Or should I just have a "projectiles system" which handles both calculating the bullet trajectory per frame and casting a ray to detect collisions?
  7. hippocoder


    Digital Ape Moderator

    Apr 11, 2010
    You really need to reorient, I think. Hybrid Renderer is fast enough for 10s of thousands of projectiles without fancy ideas or schemes. Just brute force it and test :)

    You can see my tests learning here:

    So far I haven't managed to make it take long enough to ever care about and I filled half the world up with bullets.
  8. MadboyJames


    Oct 28, 2017
    Interesting! I'll try a couple things and test them out. Thanks for the references, they'll help me figure out what I need to learn!
  9. MadboyJames


    Oct 28, 2017
    Okay, I started making the "raycast system". It works... but it has one huge problem that I'm not sure how to fix: The rayhit may not be returned to the object that asked for a ray to be cast.

    Here's the code
    Code (CSharp):
    1. [BurstCompile]
    2.     public class JobRaycastSystem : ISystem
    3.     {
    4.         protected static List<RaycastRequestStruct> _raycastRequests;
    5.         protected static List<IJobbedRaycasts> _requesters;
    6.         protected static NativeArray<RaycastCommand> requests;
    7.         protected static NativeArray<RaycastHit> hits;
    8.         protected static bool addedToListenerFlag = false;
    9.         protected static JobHandle jobHandle;
    10.         protected static int maxRequestsPerBatch = 50000;
    12.         public static void Tick()
    13.         {
    14.             CastRays();
    15.         }
    17.         protected static void CastRays()
    18.         {
    19.             jobHandle.Complete();//forces the previous job to complete before creating another job (this may still block the main thread, but we gave it a frame before we did so)
    21.             //return rays to the requesters
    22.             if (hits.Length > 0)
    23.             {
    24.                 int max = hits.Length < _requesters.Count ? hits.Length : _requesters.Count;
    25.                 for (int i = 0; i < max; i++)
    26.                 {
    27.                     _requesters[i].ReciveRayhit(hits[i]);
    28.                 }
    29.             }
    32.             _requesters.Clear();
    33.             if (requests.IsCreated)
    34.                 requests.Dispose();
    35.             if (hits.IsCreated)
    36.                 hits.Dispose();
    38.             //start a new raycast job
    39.             requests = new NativeArray<RaycastCommand>(_raycastRequests.Count, Allocator.TempJob);
    40.             hits = new NativeArray<RaycastHit>(_raycastRequests.Count, Allocator.TempJob);
    41.             for (int i = 0; i < _raycastRequests.Count; i++)
    42.             {
    43.                 RaycastRequestStruct req = _raycastRequests[i];
    44.                 requests[i] = new RaycastCommand(req.Origin, req.Direction, req.Distance, req.LayerMask, 1);
    45.             }
    46.             jobHandle = RaycastCommand.ScheduleBatch(requests, hits, maxRequestsPerBatch);
    47.             _raycastRequests.Clear();
    48.         }
    51.         public static void RaycastRequest(RaycastRequestStruct request, IJobbedRaycasts requester)
    52.         {
    53.             if (_raycastRequests == null)
    54.                 _raycastRequests = new List<RaycastRequestStruct>();
    56.             if (_requesters == null)
    57.                 _requesters = new List<IJobbedRaycasts>();
    59.             _raycastRequests.Add(request);
    60.             _requesters.Add(requester);
    62.             if (!addedToListenerFlag)
    63.             {
    64.                 addedToListenerFlag = true;
    65.                 TickSystemsOnUpdate.Register(Tick);
    66.             }
    67.         }
    69.         protected static void DisposeJobVars()
    70.         {
    71.             jobHandle.Complete();
    72.             if (requests.IsCreated)
    73.                 requests.Dispose();
    74.             if (hits.IsCreated)
    75.                 hits.Dispose();
    76.         }
    78.     }
    80.     public struct RaycastRequestStruct
    81.     {
    82.         public Vector3 Origin;
    83.         public Vector3 Direction;
    84.         public int LayerMask;
    85.         public float Distance;
    87.         public RaycastRequestStruct(Vector3 origin, Vector3 direction, int layerMask, float distance)
    88.         {
    89.             Origin = origin;
    90.             Direction = direction;
    91.             LayerMask = layerMask;
    92.             Distance = distance;
    93.         }
    94.     }
    There is a very good chance that I don't have to code this system this way, and I simply misunderstand a basic principle of raycastcommand. Soo... suggestions welcome!