Forge Engine's networking is built on ECS-native replication. Components are automatically synchronized across the network based on attributes, making multiplayer "just work" for most cases.
┌─────────────────────────────────────────────────────────────┐
│ Game Logic │
│ Same code runs on client and server │
├─────────────────────────────────────────────────────────────┤
│ Replication Layer │
│ Component sync, interest management, delta compression │
├─────────────────────────────────────────────────────────────┤
│ Network Protocol │
│ Reliable/unreliable channels, ordering, fragmentation │
├─────────────────────────────────────────────────────────────┤
│ Transport Layer │
│ UDP │ QUIC │ WebRTC │ WebSocket │
└─────────────────────────────────────────────────────────────┘
// Automatically synced to all relevant clients
#[derive(Component, Replicated)]
pub struct Position(pub Vec3);
// Server owns this - clients receive updates
#[derive(Component, Replicated, ServerAuthority)]
pub struct Health {
pub current: i32,
pub max: i32,
}
// Client can predict, server will correct
#[derive(Component, Replicated, ClientPredicted)]
pub struct Velocity(pub Vec3);
// Owning client has authority
#[derive(Component, Replicated, OwnerAuthority)]
pub struct PlayerInput {
pub direction: Vec2,
pub actions: InputActions,
}// Spawn a networked entity
commands.spawn((
NetworkId::new(),
Replicated,
Transform::default(),
Health { current: 100, max: 100 },
// Server owns by default
));
// Spawn with client ownership
commands.spawn((
NetworkId::new(),
Replicated,
Owner(client_id),
Transform::default(),
PlayerController,
));// Server configuration
NetworkConfig {
topology: NetworkTopology::ServerAuthoritative {
tick_rate: 60,
client_prediction: true,
server_reconciliation: true,
},
..
}The server is the source of truth:
- Clients send inputs to server
- Server simulates and broadcasts state
- Clients predict locally, server corrects
NetworkConfig {
topology: NetworkTopology::ClientAuthoritative,
..
}Simpler model where clients trust each other. Good for:
- Single-player with optional co-op
- Cooperative games between friends
- When security isn't critical
NetworkConfig {
topology: NetworkTopology::PeerToPeer {
max_peers: 8,
relay_fallback: true, // Use relay if P2P fails
},
..
}Clients connect directly. Good for:
- Small session games
- Fighting games (minimal latency)
- When no dedicated servers
NetworkConfig {
topology: NetworkTopology::HostMigration {
host_selection: HostSelectionStrategy::BestConnection,
},
..
}One player hosts, others connect. If host leaves, another player takes over.
let transport = UdpTransport::new()
.with_reliability(true)
.with_ordering(ChannelOrdering::Sequenced)
.build();Raw UDP with custom reliability layer. Best for:
- Desktop games
- When you control the network
let transport = QuicTransport::new()
.with_certificate(cert)
.build();Modern protocol with built-in encryption. Best for:
- Mobile games (handles network switches)
- When encryption is needed
- General purpose
let transport = WebRtcTransport::new()
.with_signaling_server("wss://signal.example.com")
.build();Browser-compatible with NAT traversal. Best for:
- Web games
- Cross-platform (browser + native)
- P2P with NAT traversal
let transport = WebSocketTransport::new("wss://game.example.com")
.build();Fallback when UDP is blocked. Slower but reliable.
For responsive gameplay despite latency:
#[derive(Component, Replicated, ClientPredicted)]
pub struct CharacterState {
pub position: Vec3,
pub velocity: Vec3,
pub grounded: bool,
}
// Client-side prediction system
fn predict_movement(
input: Res<LocalInput>,
mut query: Query<&mut CharacterState, With<LocalPlayer>>,
) {
for mut state in &mut query {
// Apply input immediately
state.velocity += input.direction * MOVE_SPEED;
state.position += state.velocity * DELTA_TIME;
}
}
// Server reconciliation happens automatically
// Engine compares server state with predicted state
// Smoothly corrects if they divergeOnly send relevant data to each client:
// Only replicate entities within distance
InterestManagement {
spatial: SpatialInterest {
radius: 100.0, // Units around player
update_frequency: 0.1, // Seconds
},
}// Team-based: only see teammates fully
#[derive(Component)]
pub struct Team(pub u8);
fn interest_filter(
observer: Entity,
observed: Entity,
world: &World,
) -> InterestLevel {
let observer_team = world.get::<Team>(observer);
let observed_team = world.get::<Team>(observed);
if observer_team == observed_team {
InterestLevel::Full // All components
} else {
InterestLevel::Minimal // Position only
}
}WorldSharding {
// Divide world into zones
zones: vec![
Zone { name: "starting_area", bounds: Aabb::new(...) },
Zone { name: "forest", bounds: Aabb::new(...) },
Zone { name: "dungeon_1", bounds: Aabb::new(...) },
],
// Each zone can run on different server
zone_assignment: ZoneAssignment::LoadBalanced,
// Seamless transition between zones
handoff: ZoneHandoff::Seamless {
overlap: 50.0, // Units of overlap for smooth transition
},
}Only send what changed:
// Frame N: Position(0, 0, 0), Health(100)
// Frame N+1: Position(1, 0, 0), Health(100)
// Sent: Position delta only (1, 0, 0), Health unchangedAutomatic for all Replicated components.
Smooth display between network updates:
NetworkConfig {
snapshot_interpolation: SnapshotInterpolation {
delay: 100.0, // ms of interpolation buffer
extrapolation_limit: 50.0, // ms max extrapolation
},
..
}Server rewinds time for hit detection:
#[derive(Component)]
pub struct LagCompensated;
// When checking hits on server
fn check_hit(
lag_compensation: Res<LagCompensation>,
attacker: Entity,
target: Entity,
) -> bool {
// Get target position at time attacker fired
let compensated_position = lag_compensation.position_at(
target,
attacker_fire_time,
);
// Check hit against historical position
ray_intersects(attack_ray, compensated_position)
}For one-off messages that don't fit component replication:
#[derive(ServerRpc)]
pub struct UseAbilityRpc {
pub ability_id: u32,
pub target: Option<Entity>,
}
// Client sends
network.send_server_rpc(UseAbilityRpc {
ability_id: 1,
target: Some(enemy),
});
// Server handles
fn handle_use_ability(
mut events: EventReader<UseAbilityRpc>,
mut query: Query<(&mut Abilities, &mut Mana)>,
) {
for event in events.read() {
// Validate and execute
}
}#[derive(ClientRpc)]
pub struct ShowDamageNumberRpc {
pub position: Vec3,
pub amount: i32,
pub crit: bool,
}
// Server sends to specific client
network.send_client_rpc(client_id, ShowDamageNumberRpc { ... });
// Or broadcast to all
network.broadcast_rpc(ShowDamageNumberRpc { ... });// Server: handle connections
fn on_client_connected(
mut events: EventReader<ClientConnected>,
mut commands: Commands,
) {
for event in events.read() {
// Spawn player entity for new client
commands.spawn((
NetworkId::new(),
Owner(event.client_id),
Transform::default(),
Player,
));
}
}
fn on_client_disconnected(
mut events: EventReader<ClientDisconnected>,
mut commands: Commands,
query: Query<(Entity, &Owner)>,
) {
for event in events.read() {
// Clean up player entity
for (entity, owner) in &query {
if owner.0 == event.client_id {
commands.entity(entity).despawn();
}
}
}
}// Start a network server
{
"method": "network.startServer",
"params": {
"port": 7777,
"max_clients": 64,
"tick_rate": 60
}
}
// Stop the server
{
"method": "network.stopServer"
}
// Get server status
{
"method": "network.getServerStatus"
}
// Returns: is_server, is_client, role, tick_rate, max_clients, stats
// List connected clients
{
"method": "network.listClients"
}
// Kick a client
{
"method": "network.kickClient",
"params": {
"client_id": 123,
"reason": "AFK"
}
}// Connect to a server
{
"method": "network.connect",
"params": {
"address": "game.example.com:7777"
}
}
// Disconnect from server
{
"method": "network.disconnect"
}
// Get connection status
{
"method": "network.getConnectionStatus"
}
// Returns: connected, state, client_id, ping_ms, server_tick// Spawn a networked entity
{
"method": "network.spawnEntity",
"params": {
"name": "Player1",
"owner": 123,
"authority": "owner"
}
}
// Returns: entity_id, network_id
// Despawn a networked entity
{
"method": "network.despawnEntity",
"params": {
"entity_id": 456
}
}
// Set entity authority
{
"method": "network.setAuthority",
"params": {
"entity_id": 456,
"authority": "server"
}
}
// Authority modes: "server", "owner", "predicted"
// Register a component for replication
{
"method": "network.registerComponent",
"params": {
"entity_id": 456,
"component": "Transform"
}
}// Get network statistics
{
"method": "network.getStats"
}
// Returns: bytes_sent_per_sec, bytes_recv_per_sec, packets_sent_per_sec,
// packets_recv_per_sec, ping_ms, rtt_ms, packet_loss, jitter_ms
// Get network configuration
{
"method": "network.getConfig"
}
// Returns: mode, tick_rate, client_prediction, server_reconciliation,
// max_clients, timeout_ms
// Set network configuration
{
"method": "network.setConfig",
"params": {
"tick_rate": 60,
"client_prediction": true,
"server_reconciliation": true,
"max_clients": 64,
"timeout_ms": 5000
}
}
// Get interest management config
{
"method": "network.getInterestConfig"
}
// Set interest management config
{
"method": "network.setInterestConfig",
"params": {
"full_interest_radius": 100.0,
"minimal_interest_radius": 200.0,
"update_interval": 0.5,
"grid_cell_size": 50.0
}
}# Terminal 1: Start server
forge run --server --port 9000
# Terminal 2: Connect client
forge run --connect localhost:9000// Simulate bad network conditions
NetworkSimulation {
latency: Duration::from_millis(100),
latency_variance: Duration::from_millis(20),
packet_loss: 0.02, // 2%
packet_duplication: 0.01,
}- Architecture Overview
- ECS Documentation
- Physics System (for networked physics)