SKWOLETS / DEV LOG / 002

BUILDING A
SHOOTING SYSTEM

Server-authoritative shooting with prediction, rewind and realistic projectile simulation

THE PROJECTILE

I use the term "projectile" as a general term for any damaging object fired from a weapon's muzzle, whether it's a bullet from a Kalashnikov rifle or a grenade from an underbarrel grenade launcher.

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.

SIMULATION CENTRALIZED
RUNTIME DATA ARRAY
PROJECTILE OBJECTS NONE REQUIRED
ACTIVE PROJECTILES SCALABLE

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.

300 M
↓ GRAVITY
← DRAG
↘ WIND

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

GRAVITY

The projectile is affected by gravity throughout its flight.

DRAG

Air resistance gradually reduces the projectile's velocity as it travels through the air.

WIND

Wind velocity and direction can influence the projectile's trajectory throughout its flight.

VELOCITY

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.

CLIENT Predicted Shot immediate feedback
SAME SIMULATION
SERVER Authoritative Shot damage authority
REPLICATION
OTHER CLIENTS Visual Shot replicated state

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.

TIMESTAMP

Prevents the same shot from being processed more than once.

MUZZLE POSITION

The server reconstructs the expected world-space position.

INTERVAL

The shot must respect the weapon's configured fire interval.

ANGLE

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.

PROJECTILE START → END trajectory segment
GRID QUERY
SPATIAL GRID CANDIDATES nearby hitboxes only
FILTERED
COLLISION TRACE actual intersection

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.

SERVER HIT REGISTRATION TARGET POSE AT SHOT TIME −150 ms SNAPSHOT NOW
CURRENT
SHOT
HISTORICAL
HIT
PAST SHOT NOW

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.

PRE-ALPHA

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.