In the early stages of the development of our 2D run’n gun game, Trespassers, we started to get some performance problems. One of our design goals was having tons of enemies on screen at once, and shooting them without thinking twice. So, tons of bullets too, specially when you get machine guns. We were using 2D physics, but we think this approach may be used in 3D physics with good results too.
Our first attempt using Unity, the naive one, was setting a GameObject with its sprites, scripts and a rigidbody with its collider acting as trigger. It turned out that, as the trigger had to detect OnTriggerEnter events to know when something was hit, the physics system had to consider another object which could collide with lots of things (walls and enemies mainly, which were so many), so having many of these at a time hurted the CPU.
This turned out really intensive so, following the suggestion of our fellow developers from Milkstone Studios, we switched to a more efficient approach: simulate the impact detection on the Update, getting rid of the rigidbody and collider all together. It is a really simple approach with some flavors depending on the implementation, but to summarize, those are the steps:
- The weapon calls a “Shoot” method where the direction, starting position, and speed are passed in to the projectile. With those parameters (or any others you may need), the projectile updates the current position at each frame, manually, not relying on a rigidbody.
- Show a public LayerMask variable (maybe two) to filter what layers represents enemies and things that can be damaged, and what layers represent things that stop the projectile (as walls for a normal bullet). Why? because we will be …
- Ray/Circlecasting each Update from the previous position stored to the current one. As Ray/Circlecast methods (and their “***All” variants) return the impacts sorted from origin to end, it is easy to check if the projectile hit something damageable or something that could break the projectile, in case you set two LayerMasks before. Then, update the previous position.
- An overridable method resolves the impact. This is useful if you want to create explosive bullets, for example, although you could use composition instead, with the proper delegates/interfaces or the OO method you consider. Just switch to whatever feels right for your needs.
Here is a GIF (programmer “art”), explaining the simple process:
- Some projectiles could go through many targets. In that case, the “Ray/CirclecastNonAlloc” variants were used, to avoid getting garbage. The array those methods need as out parameter was initialized with the number of targets that, by design, a projectile could go through. If there were more than those impacts, we didn’t care, as the projectile would have already been destroyed.
- We needed rigidbodies and colliders in some cases. Grenades were considered projectiles in our design, as they could bounce and roll. The bullets shot from the shotgun had them too, as they could bounce several times, in the same way the Unreal Tournament’s Flak Cannon work.
We didn’t capture any screenshots of the profiler at that time, but we managed to free some CPU cycles.
Hope all of this eases your development. See ya!