Server-authoritative multiplayer with client-side prediction, server reconciliation, lag compensation, and entity interpolation.
Astral Rift uses a listen-server model where one player hosts and all others connect as clients. The server is authoritative on all game state—positions, health, projectile hits. Clients predict their own movement locally for responsive controls, then reconcile when the server corrects them.
The owning client applies input locally before sending it to the server, eliminating the round-trip delay for movement. Every input frame is buffered with a sequence number so the client can replay unacknowledged inputs after a server correction.
struct FShipInputFrame {
uint32 SequenceNumber; // Monotonic, for ack tracking
float DeltaTime; // Frame delta (validated: 0 < dt < 0.5)
float RotationInput; // -1 to 1 (validated: |r| <= 1.1)
bool bThrusting;
bool bReversing;
bool bOverdrive;
};
The client sends this to the server via an unreliable RPC every tick (ServerReceiveInput). Unreliable is correct here—if an input is lost, the next one supersedes it. The server validates inputs (clamping delta time, rotation range) to prevent cheating.
When the server processes an input, it sends back a correction containing the authoritative position, velocity, and the last processed sequence number. The client reconciles by:
struct FShipStateCorrection {
FVector_NetQuantize Position; // Quantized for bandwidth
FVector_NetQuantize Velocity;
float Rotation;
uint32 LastProcessedSequence; // Which input was last applied
};
Input replay means most corrections are tiny (client and server ran the same deterministic physics). But when they diverge:
Correction < 50 units: SmoothingOffset absorbs the delta, decaying at 10.0/s. Ship visually glides to the corrected position over ~100ms. The player never notices.
Correction > 50 units: Hard snap. This only happens on teleport, respawn, or severe desync. No point smoothing a 500-unit correction.
This works because SimulateMovement is a pure static function—same inputs always produce the same outputs. Client and server run identical physics, so corrections are rare and small under normal conditions.
Other players' ships (SimulatedProxy) can't be predicted—we don't know their inputs. Instead, the server replicates their position and the client smoothly interpolates toward it.
// Each frame for SimulatedProxy ships:
CurrentPos = FMath::VInterpTo(CurrentPos, TargetPos, DeltaTime, 15.0f);
CurrentRot = FMath::RInterpTo(CurrentRot, TargetRot, DeltaTime, 15.0f);
The interpolation rate of 15.0 gives smooth movement without excessive lag. Higher values track more tightly but look jittery on lossy connections. 15.0 was tuned through PIE testing with simulated latency.
The hardest networking problem: when you shoot where an enemy was on your screen, the server needs to validate that hit against where they actually were at the time you fired, not where they are now.
Every server tick, each ship records a position snapshot into a 16-entry ring buffer. When a projectile hits, the server:
ServerTime - (Ping / 2)Bots (AIController) always have 0ms latency—no rewind needed. The 200ms max rewind supports up to 400ms round-trip time, which covers most internet connections.
| RPC | Direction | Reliability | Purpose |
|---|---|---|---|
| ServerReceiveInput | Client→Server | Unreliable | Send input frame every tick |
| MulticastWeaponFire | Server→All | Unreliable | Fire sound + muzzle flash |
| MulticastDeath | Server→All | Reliable | Death explosion VFX |
| MulticastRespawn | Server→All | Reliable | Respawn visual |
| MulticastHitFlash | Server→All | Unreliable | Damage feedback |
| MulticastKillFeed | Server→All | Unreliable | Kill notification |
Unreliable for cosmetic events (missing a fire sound is fine), reliable for state-changing events (a missed death would leave a ghost ship). This matches the standard UE5 networking pattern.
These are the same problems every multiplayer game solves—Valorant, Overwatch, Rocket League all use variations of this architecture. The tradeoffs are the interesting part: how much history to buffer, when to smooth vs snap, how much tolerance on lag-compensated hits. Each choice has a direct impact on how the game feels to play at different latencies.
The implementation is ~400 lines of C++, plus 12 automated behavior tests for prediction, reconciliation, and rewind correctness, and 49 multi-process headless networking tests that verify replication, input flow, and latency simulation with up to 3 simultaneous clients.