Test

arrow_backAll guides
Advanced·Game Development·30 min

Networking & Multiplayer

Implement multiplayer features — RPCs, networked properties, host/client architecture.

s&box's networking model is intentionally simple: one player is the host, the rest are clients, and the host has authority by default. You mark properties as replicated with [Sync], call cross-machine methods with [Rpc.Broadcast] / [Rpc.Owner] / [Rpc.Host], and spawn networked GameObjects with NetworkSpawn(). The Networking static class handles lobbies and connections. It is not a server-authoritative AAA stack — it's designed for small co-op and party games, which is exactly what game jams need.

Lobbies and connections

Use the static Networking class to create or join games. Networking.CreateLobby opens a lobby on Steam with a max player count and a privacy setting (Public, FriendsOnly, Private). Networking.QueryLobbies returns a list you can present in a server browser. Networking.Connect(lobbyId) joins one. The host's scene becomes the source of truth — joining clients receive a full snapshot of every networked GameObject.

// Host: open a lobby
Networking.CreateLobby( new LobbyConfig
{
    MaxPlayers = 8,
    Privacy = LobbyPrivacy.Public,
    Name = "Jam Game"
} );

// Client: list lobbies, join one
var lobbies = await Networking.QueryLobbies();
Networking.Connect( lobbies.First().LobbyId );

[Sync] for replicated state

[Sync] on a property of a Component automatically replicates the value from its owner to everyone else. Supported types are unmanaged value types, string, and a few engine-special references (GameObject, Component, GameResource). The owner of the object writes; everyone else reads. For collections, use the special NetList<T> and NetDictionary<K,V> types. Combine with [Change("OnNameChanged")] to fire a callback when the value updates on a remote machine.

using Sandbox;

public sealed class Player : Component
{
    [Sync] public int Kills { get; set; }
    [Sync] public string DisplayName { get; set; }
    [Sync, Change("OnHealthChanged")] public float Health { get; set; } = 100f;

    [Sync] public NetList<int> Inventory { get; set; } = new();

    private void OnHealthChanged( float oldValue, float newValue )
    {
        if ( newValue <= 0 ) PlayDeathFx();
    }

    void PlayDeathFx() { /* ... */ }
}

RPCs: Broadcast, Owner, Host

An RPC is a method that, when called locally, also executes on remote machines. [Rpc.Broadcast] runs everywhere (use it for visual/audio effects you want everyone to see). [Rpc.Owner] runs only on the owner of the networked object (or the host if it has no owner). [Rpc.Host] runs only on the host (good for authoritative actions like 'request damage'). RPCs can be static and can carry the same argument types as [Sync] properties. Pass NetFlags.Unreliable for cheap fire-and-forget messages, and use Rpc.Caller to learn who invoked the method.

using Sandbox;

public sealed class Bomb : Component
{
    public void Detonate()
    {
        // Local effect; ask everyone to play it too
        PlayExplosionFx( WorldPosition );
    }

    [Rpc.Broadcast( NetFlags.Unreliable )]
    public void PlayExplosionFx( Vector3 position )
    {
        Sound.Play( "explosion", position );
    }

    [Rpc.Host]
    public void RequestDamage( int amount )
    {
        // Only the host applies damage authoritatively
        var caller = Rpc.Caller;
        Log.Info( $"Damage requested by {caller.DisplayName}" );
    }
}

Spawning networked GameObjects and ownership

By default GameObjects in the scene are sent as part of the initial snapshot to joining clients (NetworkMode.Snapshot). To create a networked object at runtime, clone a prefab and call NetworkSpawn() — pass a Connection to assign that client as the owner. The owner simulates the object: their machine writes its transform and [Sync] properties, everyone else interpolates. Check IsProxy in your update loop to skip control logic on machines that aren't the owner. Ownership can be transferred with go.Network.TakeOwnership().

using Sandbox;

public sealed class GameNetworkManager : Component, Component.INetworkListener
{
    [Property] public GameObject PlayerPrefab { get; set; }
    [Property] public GameObject SpawnPoint { get; set; }

    public void OnActive( Connection connection )
    {
        var player = PlayerPrefab.Clone( SpawnPoint.WorldTransform );
        player.NetworkSpawn( connection ); // 'connection' becomes the owner
    }
}

public sealed class PlayerMovement : Component
{
    protected override void OnUpdate()
    {
        if ( IsProxy ) return; // only the owner reads input

        if ( !Input.AnalogMove.IsNearZeroLength )
            WorldPosition += Input.AnalogMove.Normal * Time.Delta * 250f;
    }
}

Test multiplayer locally

You don't need a friend to test. With your project running, click the network status icon in the editor's header bar and choose 'Join via new instance' — a second sbox-dev process spawns and joins your session. Code changes hot-reload to all connected clients, so you can iterate while two clients are live. You can also run 'connect local' in the console of a separately-launched s&box client.

Further reading