THE PROJECTILE
In a networked multiplayer game, determinism is crucial. The client needs immediate feedback, while the server must remain authoritative. Players have different latency levels, targets are constantly moving, and projectiles must behave consistently regardless of network conditions.
I am not approaching this as someone who already knows all the answers. Most of what is described here came from building the system, breaking it, testing it, measuring it, and then rebuilding parts of it when they did not behave the way I expected.
I learn this way because it is the way that works for me. I spend a lot of time testing the actual system under different conditions, looking at what fails, and trying to understand why. This shooting system is the result of that process.
In SKWOLETS, I don't want to create a separate gameplay object for every projectile. The goal is to build a centralized ballistic system where simulation, validation, and hit registration work as separate but interconnected systems.
ONE SYSTEM FOR EVERY PROJECTILE
Projectiles are not individual objects.
The system does not create a separate game object for every projectile flying through the world.
Instead, active projectiles are processed together as part of a centralized simulation.
Each projectile keeps only the state required for its ballistic calculation, while the
simulation advances them in a single pass.
This significantly reduces the amount of per-projectile overhead and makes it possible
to handle a large number of airborne projectiles without turning every projectile into an
independent gameplay object.
With a few hundred active projectiles, iterating through a native array is extremely inexpensive compared with maintaining hundreds of independent gameplay objects.
BALLISTIC SIMULATION
The projectile does not simply move from A to B.
Every simulation step updates the projectile's velocity before calculating its next position.
The physics are intentionally lightweight and implemented directly in native C++. At each simulation step, the projectile's velocity is calculated from its current velocity, gravity, drag and wind. That velocity is then used to calculate the projectile's new position.
Formula: velocity + gravity + drag + wind → new velocity → new position
The projectile is affected by gravity throughout its flight.
Air resistance gradually reduces the projectile's velocity as it travels through the air.
Wind velocity and direction can influence the projectile's trajectory throughout its flight.
The projectile's position is continuously calculated from its simulated velocity.
DATA-DRIVEN APPROACH
Ammunition parameters are not hardcoded directly into the projectile simulation.
Instead, each ammunition type has its own ballistic characteristics, lifetime, and visual behaviour, all defined through external configuration.
This also allows the system to be modified without changing its internal logic. New ammunition types can be added by specifying their parameters in the configuration .
AMMUNITION CONFIGURATION JSON
[
{
"Type": "Ammo_762x39",
"Velocity": 71800,
"BulletWeight": 7.9,
"BallisticCoefficient": 0.294,
"Lifetime": 3.0,
"ProjectileCount": 1
},
{
"Type": "Ammo_762x39Tracer",
"Parent": "Ammo_762x39",
"Particle": {
"ParticleSystem": "Tracer",
"SpawnDelayDistance": 100
}
},
{
"Type": "Ammo_762x39Subsonic",
"Parent": "Ammo_762x39",
"Velocity": 28500,
"BulletWeight": 12.5,
"Lifetime": 4.0
}
]
CLIENT PREDICTION
Waiting for the server before showing a projectile would make shooting feel slow.
The firing client therefore runs the same projectile simulation locally as soon as the shot is created.
The important part is that these are not three independent gameplay projectiles. They are three executions of the same deterministic projectile simulation in different contexts.
SERVER VALIDATION
Prediction is for responsiveness. The server remains authoritative.
Before the authoritative projectile is allowed to produce damage, the server validates the shot.
Prevents the same shot from being processed more than once.
The server reconstructs the expected world-space position.
The shot must respect the weapon's configured fire interval.
Impossible deviations from the synchronized camera direction can be rejected.
I originally used fire rate in RPM, for example 600 RPM for an AK-style rifle.
Later, I switched to storing the fire interval directly in seconds.
So 600 RPM becomes a 0.1 second interval between shots.
The fire interval can also be configured individually for each weapon. I will cover the weapon configuration system in a separate article.
For the angle check, I did not have to invent another system just to replicate
the camera. Unreal Engine's movement system already carries the relevant view
information through its existing network movement flow.
I could simply build the shooting validation around data the server was already receiving.
[12:41:08] shot received
[12:41:08] timestamp accepted
[12:41:08] muzzle position validated
[12:41:08] fire interval validated
[12:41:08] camera angle validated
[12:41:08] authoritative shot accepted
SPATIAL GRID
A projectile does not need to test every player in the world.
Before performing the actual collision query, the projectile system asks the Spatial Grid for potential hitbox candidates along the projectile segment.
The Spatial Grid is therefore used as a broad-phase candidate system. The expensive part of the process only runs against objects that can actually be near the projectile's trajectory.
I started calling this part of the system "Vibrissae" because the principle feels surprisingly similar. A mammals does not need to inspect everything around it to know what is nearby. Its whiskers help it detect what is close enough to matter.
The projectile system works in a similar way: first find what is nearby, then perform the expensive collision check only on those candidates.
REWIND
The server does not only need to know where the target is. It needs to know where the target was.
By the time a shot reaches the server, the target may have already moved. Rewind allows the hit test to use the target's historical state from the moment the shot was fired.
The server keeps a short history of recent target poses.
When a shot is evaluated, the relevant historical state is selected
and the hit test is performed against it.
A benchmark of a complete rewind-and-restore scope averaged roughly
0.65 microseconds per operation in the tested setup,
with a P95 around 0.9 microseconds.
All materials shown are from an early stage of development. Visual elements, interface, mechanics and game systems may change during development.
The demonstration below was recorded at approximately 160 m distance with 250 ms network latency.
HIT REGISTRATION
There is a golden rule in multiplayer game development: never trust the client.
Client-side simulation exists to ensure responsive controls and provide visual feedback. However, the server always has the final say on whether damage is applied.
Only a hit confirmed by the server can trigger damage processing.
The damage calculation itself is straightforward: calculate the bullet's kinetic energy and determine the final damage based on the body part hit.
CONCLUSION
This system did not appear all at once. It started with a much simpler approach where the server trusted the client more than it should. Later, I moved to a fully server-side approach and gradually built the missing pieces around it.
For about a year, I kept putting this puzzle together until all these pieces could work together as one model.