Repository navigation
Making Your First Multiplayer Game
This tutorial records source-level design patterns for a multiplayer FPS prototype using SparkEngine networking surfaces. It is not a turnkey two-client/server walkthrough or an operational deployment guide.
Stable-v1 boundary: Multiplayer is experimental, outside stable-v1, and blocked. Stable-v1 is limited to an uncertified Windows 11 x64/MSVC v143/D3D11 (or Windows NullRHI) C++-module product shape with one first-party single-player
SparkGameFPSvertical slice. The networking examples below do not certify a supported backend, host, package, protocol, or production service.
- SparkEngine built from source (see Build System and CMake Modules)
- Completed the single-player Getting Started tutorial
- Basic understanding of client/server architecture
The code patterns below use an authoritative-server model over UDP. They describe a development direction, not a verified end-to-end transport contract:
- Server owns the game state. It validates all player actions and broadcasts the canonical world state.
- Clients send input to the server and render the state they receive.
- Tick rate: The server runs its simulation at a fixed 60 Hz tick rate. Clients send input every frame and interpolate between received snapshots.
- Reliability: Critical messages (connect, disconnect, RPC) use reliable ordered delivery. State updates use unreliable channels for low latency.
┌──────────┐ UDP ┌──────────┐
│ Client │ ──── Input ───────► │ Server │
│ │ ◄── State Sync ──── │ │
└──────────┘ └──────────┘
Use NetworkManager to start a server on a port. The server listens for incoming connections and drives the simulation loop.
#include "Engine/Networking/NetworkManager.h"
using namespace Spark::Net;
void StartGameServer()
{
auto& net = NetworkManager::GetInstance();
net.Initialize();
// Start listening on port 27015, max 16 clients
if (!net.StartServer(27015, 16))
{
// Handle error
return;
}
// Register message handlers
net.RegisterHandler(MessageType::ClientInput,
[](const NetworkMessage& msg)
{
// Process player input (see Section 6)
});
}Call net.Update(deltaTime) every frame in your server game loop to process incoming packets and send state updates.
On the client side, connect to the server's IP address:
void ConnectToServer(const std::string& serverAddress)
{
auto& net = NetworkManager::GetInstance();
net.Initialize();
if (!net.Connect(serverAddress, 27015, "PlayerOne"))
{
// Connection failed
return;
}
// Register handlers for state updates
net.RegisterHandler(MessageType::EntityStateUpdate,
[](const NetworkMessage& msg)
{
// Apply remote entity state (see Section 5)
});
net.RegisterHandler(MessageType::EntitySpawn,
[](const NetworkMessage& msg)
{
// Spawn a remote player entity
});
}Every networked entity needs a ReplicatedEntity registration. The server assigns a unique networkID and tracks which properties are dirty.
void RegisterPlayerEntity(uint32_t localEntityId, ClientID ownerClient)
{
auto& net = NetworkManager::GetInstance();
ReplicatedEntity entity;
entity.networkID = 0; // Assigned by RegisterReplicatedEntity
entity.ownerID = ownerClient;
entity.entityType = "Player";
// Register position property
entity.properties.push_back({
"position",
ReplicatedProperty::Type::Vector3,
[localEntityId](NetBuffer& buf)
{
// Serialize current position into buffer
XMFLOAT3 pos = GetEntityPosition(localEntityId);
buf.WriteVector3(pos);
},
[localEntityId](NetBuffer& buf)
{
// Deserialize and apply position
XMFLOAT3 pos = buf.ReadVector3();
SetEntityPosition(localEntityId, pos);
}
});
// Register health property
entity.properties.push_back({
"health",
ReplicatedProperty::Type::Float,
[localEntityId](NetBuffer& buf)
{
buf.WriteFloat(GetEntityHealth(localEntityId));
},
[localEntityId](NetBuffer& buf)
{
float hp = buf.ReadFloat();
SetEntityHealth(localEntityId, hp);
}
});
uint32_t netId = net.RegisterReplicatedEntity(entity);
}Clients sample input each frame and send it as a ClientInputState:
void SendInput()
{
auto& net = NetworkManager::GetInstance();
ClientInputState input;
input.inputSequence = nextSequence++;
input.moveForward = GetAxis("MoveForward"); // -1 to 1
input.moveRight = GetAxis("MoveRight"); // -1 to 1
input.lookYaw = GetMouseDeltaX();
input.lookPitch = GetMouseDeltaY();
input.fire = IsButtonDown("Fire");
input.jump = IsButtonPressed("Jump");
input.sprint = IsButtonDown("Sprint");
input.deltaTime = frameDeltaTime;
input.timestamp = localTime;
net.SendClientInput(input);
// Store for client-side prediction reconciliation
pendingInputs.push_back(input);
}The server processes each client's input, validates it, and applies movement:
void ServerProcessInputs(float deltaTime)
{
auto& net = NetworkManager::GetInstance();
const auto& inputs = net.GetPendingInputs();
for (const auto& input : inputs)
{
// Find the player entity for this client
auto* entity = FindPlayerByClient(input.senderID);
if (!entity) continue;
// Validate input (anti-cheat: clamp values, check timing)
float forward = std::clamp(input.moveForward, -1.0f, 1.0f);
float right = std::clamp(input.moveRight, -1.0f, 1.0f);
// Apply movement
float speed = input.sprint ? 8.0f : 5.0f;
XMFLOAT3 movement;
movement.x = right * speed * input.deltaTime;
movement.y = 0.0f;
movement.z = forward * speed * input.deltaTime;
ApplyMovement(entity, movement);
// Mark position dirty for replication
net.MarkPropertyDirty(entity->networkID, "position");
}
}To avoid input lag, the client predicts its own movement locally and reconciles when the server confirms:
void ClientReconcile(uint32_t confirmedSequence, const XMFLOAT3& serverPosition)
{
// Remove all inputs up to the confirmed sequence
while (!pendingInputs.empty() &&
pendingInputs.front().inputSequence <= confirmedSequence)
{
pendingInputs.erase(pendingInputs.begin());
}
// Snap to server position
SetLocalPlayerPosition(serverPosition);
// Re-apply remaining unconfirmed inputs
for (const auto& input : pendingInputs)
{
float speed = input.sprint ? 8.0f : 5.0f;
XMFLOAT3 movement;
movement.x = input.moveRight * speed * input.deltaTime;
movement.y = 0.0f;
movement.z = input.moveForward * speed * input.deltaTime;
ApplyLocalMovement(movement);
}
}When a player fires, the server validates and broadcasts:
// Client: send fire request
void ClientFire(const XMFLOAT3& origin, const XMFLOAT3& direction)
{
NetworkMessage msg;
msg.type = MessageType::EntityRPC;
msg.channel = ChannelType::ReliableOrdered;
NetBuffer buf;
buf.WriteVector3(origin);
buf.WriteVector3(direction);
msg.payload = buf.GetData();
NetworkManager::GetInstance().SendMessage(msg);
}
// Server: validate and broadcast
void ServerHandleFire(ClientID shooter, const XMFLOAT3& origin,
const XMFLOAT3& direction)
{
auto* player = FindPlayerByClient(shooter);
if (!player) return;
// Validate: is origin close to player's actual position?
float dist = Distance(player->position, origin);
if (dist > 2.0f) return; // Reject suspicious fire
// Spawn projectile server-side
uint32_t projId = SpawnProjectile(origin, direction, shooter);
// Broadcast to all clients
NetworkMessage spawnMsg;
spawnMsg.type = MessageType::EntitySpawn;
NetBuffer buf;
buf.WriteUint32(projId);
buf.WriteVector3(origin);
buf.WriteVector3(direction);
spawnMsg.payload = buf.GetData();
NetworkManager::GetInstance().SendToAll(spawnMsg);
}Register timeout and disconnect handlers:
void SetupDisconnectHandling()
{
auto& net = NetworkManager::GetInstance();
// Server-side: handle client timeout
net.SetTimeoutHandler([](ClientID client)
{
// Remove player entity, notify other clients
RemovePlayerEntity(client);
BroadcastPlayerLeft(client);
});
// Client-side: enable auto-reconnect
NetworkManager::AutoReconnectConfig config;
config.enabled = true;
config.baseDelay = 2.0f;
config.maxAttempts = 5;
net.SetAutoReconnect(config);
net.SetReconnectFailedCallback([]()
{
ShowDisconnectedUI();
});
}The server rewinds entity positions to the time the shot was fired on the client:
void ServerHitscan(ClientID shooter, const XMFLOAT3& origin,
const XMFLOAT3& direction, float clientTimestamp)
{
auto& net = NetworkManager::GetInstance();
auto& lagComp = net.GetLagCompensator();
// Estimate client's view of the world at their fire time
float rtt = net.GetEstimatedRTT() / 1000.0f;
float rewindTime = clientTimestamp - (rtt * 0.5f);
// Rewind and test
// LagCompensator stores historical snapshots for rewinding
// Perform raycast against rewound hitboxes
// If hit: apply damage, mark health dirty, broadcast
}Track kills and deaths per client, replicate via game state sync:
// Server broadcasts score updates
void BroadcastScoreUpdate(ClientID player, int kills, int deaths)
{
NetworkMessage msg;
msg.type = MessageType::ScoreUpdate;
NetBuffer buf;
buf.WriteUint32(player);
buf.WriteUint32(static_cast<uint32_t>(kills));
buf.WriteUint32(static_cast<uint32_t>(deaths));
msg.payload = buf.GetData();
NetworkManager::GetInstance().SendToAll(msg);
}
// Client renders scoreboard from received data
void RenderScoreboard(const std::vector<PlayerScore>& scores)
{
// Use ImGui or engine UI to render a table
for (const auto& score : scores)
{
DrawText(score.name + ": " +
std::to_string(score.kills) + "K / " +
std::to_string(score.deaths) + "D");
}
}Do not use the old SparkEngine.exe --server, --connect, --port, or --name commands: the engine executable does not implement those options. Its runtime selectors are -game <module>, -manifest <path>, and -headless/-dedicated; those selectors do not configure a complete client/server session for this tutorial.
For source development, implement and test the NetworkManager calls in the selected module or use the separate server surface with its own documented configuration. Keep experiments on loopback unless a development LAN endpoint explicitly names a canonical RFC1918 interface and prefix. The current UDP protocol is unauthenticated and unencrypted, so do not send credentials, expose it to hostile networks, open firewall rules, or configure port forwarding from this guide. See SECURITY.md for the containment boundary.
This page does not provide a LAN or Internet deployment procedure, supported host matrix, or bandwidth estimate. Remote/production operations, hostile multi-client testing, protocol compatibility, authentication, encryption, and lifecycle evidence remain open. The headless/NullRHI delivery evidence is separately blocked by HEAD-220; -headless alone is not proof of a configured NullRHI server deployment.
- Read the Networking wiki page for full API details
- See Area Server Architecture for scaling to larger worlds
- Explore the Networking Wire Format specification
- Inspect
GameModules/SparkGameFPS/as the blocked, uncertified first-party single-player vertical-slice reference; it is not multiplayer or service evidence
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers