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.