Skip to content

Latest commit

 

History

History
594 lines (482 loc) · 12.8 KB

File metadata and controls

594 lines (482 loc) · 12.8 KB

Networking System

Overview

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.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    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                            │
└─────────────────────────────────────────────────────────────┘

Replication Basics

Marking Components for Replication

// 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,
}

Entity Ownership

// 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,
));

Network Topologies

Server-Authoritative (MMO/Competitive)

// Server configuration
NetworkConfig {
    topology: NetworkTopology::ServerAuthoritative {
        tick_rate: 60,
        client_prediction: true,
        server_reconciliation: true,
    },
    ..
}

The server is the source of truth:

  1. Clients send inputs to server
  2. Server simulates and broadcasts state
  3. Clients predict locally, server corrects

Client-Authoritative (Co-op/Casual)

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

Peer-to-Peer

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

Host Migration

NetworkConfig {
    topology: NetworkTopology::HostMigration {
        host_selection: HostSelectionStrategy::BestConnection,
    },
    ..
}

One player hosts, others connect. If host leaves, another player takes over.

Transport Layers

UDP (Default)

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

QUIC

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

WebRTC

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

WebSocket

let transport = WebSocketTransport::new("wss://game.example.com")
    .build();

Fallback when UDP is blocked. Slower but reliable.

Client Prediction

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 diverge

Interest Management

Only send relevant data to each client:

Spatial Interest

// Only replicate entities within distance
InterestManagement {
    spatial: SpatialInterest {
        radius: 100.0,  // Units around player
        update_frequency: 0.1,  // Seconds
    },
}

Custom Interest

// 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
    }
}

MMORPG-Scale Features

World Sharding

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
    },
}

Delta Compression

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 unchanged

Automatic for all Replicated components.

Snapshot Interpolation

Smooth display between network updates:

NetworkConfig {
    snapshot_interpolation: SnapshotInterpolation {
        delay: 100.0,  // ms of interpolation buffer
        extrapolation_limit: 50.0,  // ms max extrapolation
    },
    ..
}

Lag Compensation

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)
}

Remote Procedure Calls (RPC)

For one-off messages that don't fit component replication:

Server RPC (Client → Server)

#[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
    }
}

Client RPC (Server → Client)

#[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 { ... });

Connection Management

// 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();
            }
        }
    }
}

API Commands

Server Commands

// 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"
    }
}

Client Commands

// 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

Replication Commands

// 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"
    }
}

Statistics and Configuration

// 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
    }
}

Testing Multiplayer

Local Testing

# Terminal 1: Start server
forge run --server --port 9000

# Terminal 2: Connect client
forge run --connect localhost:9000

Network Simulation

// 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,
}

See Also