# Multiplayer networking

Connect players directly with peer-to-peer WebRTC networking

Source: https://docs.wavedash.com/multiplayer/networking

When players join a lobby, the SDK automatically establishes direct WebRTC connections between **all** members in a fully-mesh topology, sets up reliable and unreliable data channels, and handles NAT traversal with TURN servers. There is no relay or dedicated server — peers talk directly to each other.

All P2P payloads are **binary**: `Uint8Array` in JavaScript, `byte[]` or `ArraySegment<byte>` in Unity, `PackedByteArray` in Godot, and the Defold binding's binary message buffer type in Lua.

## Sending messages

### Broadcast to all peers

<CodeGroup>
```gdscript Godot
func broadcast_to_all(data: PackedByteArray, reliable: bool = true):
    WavedashSDK.send_p2p_message("", data, 0, reliable)
```

```csharp Unity
Wavedash.SDK.BroadcastP2PMessage(data, channel: 0, reliable: true);
```

```lua Defold
wavedash.broadcast_p2p_message(0, true, data)
```

```javascript JavaScript
Wavedash.broadcastP2PMessage(0, true, new Uint8Array([1, 2, 3]));
```
</CodeGroup>

#### Broadcast is best-effort

Broadcast only sends to peers whose data channels are currently `open`. Peers that are still connecting, mid-reconnect, or gone are silently skipped — no per-peer error, no `P2P_PACKET_DROPPED` event. The return value tells you whether the SDK had any open channels at all, not whether each peer received the message.

This keeps the broadcast fast path allocation-free and means a flaky peer won't spam your game with drop events every tick. The trade-off is that if you need to know which peers actually received the message, you have to track reachability yourself by listening for the P2P events. For example, maintain a "reachable peers" set:

- Add on `P2P_CONNECTION_ESTABLISHED` and `P2P_PEER_RECONNECTED`
- Remove on `P2P_PEER_RECONNECTING`, `P2P_PEER_DISCONNECTED`, and `P2P_CONNECTION_FAILED`

For unicast (`sendP2PMessage` with a target user ID), the SDK **does** emit `P2P_PACKET_DROPPED` with reason `PEER_NOT_READY` if the specific peer's channel isn't open. See [Events: P2P events](/sdk/events#p2p-events) for the full lifecycle.

### Send to a specific peer

<CodeGroup>
```gdscript Godot
func send_to_peer(user_id: String, data: PackedByteArray):
    WavedashSDK.send_p2p_message(user_id, data, 0, true)
```

```csharp Unity
Wavedash.SDK.SendP2PMessage(userId, data, channel: 0, reliable: true);
```

```lua Defold
wavedash.send_p2p_message(user_id, 0, true, data)
```

```javascript JavaScript
Wavedash.sendP2PMessage(targetUserId, 0, false, payload);
```
</CodeGroup>

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `userId` | `Id<"users">` | For unicast | Target peer's user ID (omit or `undefined` to broadcast) |
| `channel` | number | Yes | Channel number (0-7) |
| `payload` | Uint8Array | Yes | Binary data to send |
| `reliable` | boolean | No | Use reliable channel (default: true) |
| `payloadSize` | number | No | Bytes to send from payload (default: `payload.length`) |

## Receiving messages

<CodeGroup>
```gdscript Godot
func _process(_delta):
    var messages = WavedashSDK.drain_p2p_channel(0)
    for msg in messages:
        var from_user = msg["identity"]
        var payload: PackedByteArray = msg["payload"]
```

```csharp Unity
private List<Wavedash.P2PMessage> messageBuffer = new();

void Update()
{
    int count = Wavedash.SDK.DrainP2PChannel(0, messageBuffer);
    for (int i = 0; i < count; i++)
        HandleMessage(messageBuffer[i]);
}
```

```lua Defold
local message = wavedash.read_p2p_message_from_channel(0)
if message then
    print("From:", message.fromUserId, "Data:", message.payload)
end
```

```javascript JavaScript
const message = Wavedash.readP2PMessageFromChannel(0);
if (message) {
  console.log(`From: ${message.fromUserId}, Data: ${message.payload}`);
}
```
</CodeGroup>

## Channels

The SDK supports up to 8 channels (0-7):

| Channel | Suggested use |
|---------|---------------|
| 0 | Game state updates |
| 1 | Player input |
| 2 | Chat messages |
| 3 | Voice data |
| 4-7 | Custom use |

## Reliable vs unreliable

| Type | Description | Best for |
|------|-------------|----------|
| **Reliable** | Guaranteed delivery, ordered | Important state, chat, game events |
| **Unreliable** | Best-effort, faster | Position updates, input, frequent data |

<CodeGroup>
```gdscript Godot
WavedashSDK.send_p2p_message("", data, 0, true)   # reliable
WavedashSDK.send_p2p_message("", data, 1, false)  # unreliable
```

```csharp Unity
Wavedash.SDK.BroadcastP2PMessage(data, channel: 0, reliable: true);
Wavedash.SDK.BroadcastP2PMessage(data, channel: 1, reliable: false);
```

```lua Defold
wavedash.broadcast_p2p_message(0, true, game_state_data)
wavedash.broadcast_p2p_message(1, false, position_data)
```

```javascript JavaScript
Wavedash.broadcastP2PMessage(0, true, gameStateData);
Wavedash.broadcastP2PMessage(1, false, positionData);
```
</CodeGroup>

## Connection state

<CodeGroup>
```gdscript Godot
func _ready():
    WavedashSDK.p2p_connection_established.connect(_on_p2p_connected)
    WavedashSDK.p2p_peer_reconnecting.connect(_on_p2p_reconnecting)
    WavedashSDK.p2p_peer_reconnected.connect(_on_p2p_reconnected)
    WavedashSDK.p2p_peer_disconnected.connect(_on_p2p_disconnected)
    WavedashSDK.p2p_connection_failed.connect(_on_p2p_failed)
    WavedashSDK.p2p_packet_dropped.connect(_on_p2p_packet_dropped)
```

```csharp Unity
void Awake()
{
    Wavedash.SDK.OnP2PConnectionEstablished += data => Debug.Log($"Connected to: {data["userId"]}");
    Wavedash.SDK.OnP2PPeerReconnecting     += data => Debug.Log($"Peer unreachable, retrying: {data["userId"]}");
    Wavedash.SDK.OnP2PPeerReconnected      += data => Debug.Log($"Peer back online: {data["userId"]}");
    Wavedash.SDK.OnP2PPeerDisconnected     += data => Debug.Log($"Disconnected: {data["userId"]}");
    Wavedash.SDK.OnP2PConnectionFailed     += data => Debug.Log($"Connection failed: {data["error"]}");
    Wavedash.SDK.OnP2PPacketDropped        += data => Debug.LogWarning(
        $"Dropped packet on ch{data["channel"]} ({data["direction"]}, {data["reason"]}) " +
        $"x{data["droppedCount"]} (total {data["droppedTotal"]})");
}
```

```lua Defold
function init(self)
    wavedash.init({}, function(_, event, payload)
        if event == wavedash.EVENT_P2P_CONNECTION_ESTABLISHED then
            print("Connected to:", payload.userId)
        elseif event == wavedash.EVENT_P2P_PEER_RECONNECTING then
            print("Peer unreachable, retrying:", payload.userId)
        elseif event == wavedash.EVENT_P2P_PEER_RECONNECTED then
            print("Peer back online:", payload.userId)
        elseif event == wavedash.EVENT_P2P_PEER_DISCONNECTED then
            print("Disconnected:", payload.userId)
        elseif event == wavedash.EVENT_P2P_CONNECTION_FAILED then
            print("Connection failed:", payload.error)
        elseif event == wavedash.EVENT_P2P_PACKET_DROPPED then
            print(("Dropped packet on ch%d (%s, %s) x%d (total %d)"):format(
                payload.channel,
                payload.direction,
                payload.reason,
                payload.droppedCount,
                payload.droppedTotal
            ))
        end
    end)
end
```

```javascript JavaScript
Wavedash.on(Wavedash.Events.P2P_CONNECTION_ESTABLISHED, (payload) => {
  console.log("Connected to peer:", payload.username);
});
Wavedash.on(Wavedash.Events.P2P_PEER_RECONNECTING, (payload) => {
  console.warn("Peer unreachable, retrying:", payload.username);
});
Wavedash.on(Wavedash.Events.P2P_PEER_RECONNECTED, (payload) => {
  console.log("Peer back online:", payload.username);
});
Wavedash.on(Wavedash.Events.P2P_PACKET_DROPPED, (payload) => {
  const { direction, channel, reason, droppedCount, droppedTotal } = payload;
  console.warn(`Dropped ${direction} on ch${channel}: ${reason} x${droppedCount} (total ${droppedTotal})`);
});
```
</CodeGroup>

### Events

| Event | Description |
|-------|-------------|
| `P2P_CONNECTION_ESTABLISHED` | Both data channels to a peer are open and ready |
| `P2P_PEER_RECONNECTING` | Peer's ICE connection failed; SDK is attempting an ICE restart. Treat as "unreachable, but keep state" — no re-handshake happens |
| `P2P_PEER_RECONNECTED` | Peer that was reconnecting is back online; resume sending |
| `P2P_PEER_DISCONNECTED` | Peer's data channel closed (left the lobby, tab closed, or ICE restarts gave up) |
| `P2P_CONNECTION_FAILED` | Terminal failure — no TURN credentials, data-channel error, or ICE restart budget exhausted |
| `P2P_PACKET_DROPPED` | SDK dropped a packet; payload tells you the channel, direction, reason, and coalesced count |

## Advanced: batch reading

For game engines, `drainP2PChannelToBuffer` reads all queued messages into a tightly packed binary buffer:

```javascript
const packed = Wavedash.drainP2PChannelToBuffer(0);
const view = new DataView(packed.buffer);
let offset = 0;
while (offset < packed.byteLength) {
  const size = view.getUint32(offset, true);
  offset += 4;
  const message = packed.subarray(offset, offset + size);
  offset += size;
}
```

<Note>
TURN credentials are managed automatically. No configuration required.
</Note>

## Configuration

Pass a `p2p` field on your `Wavedash.init(config)` call to tune the P2P runtime. All fields are optional — the defaults below are used if you omit the block entirely.

<Note>
The current Defold binding does not expose `init(config)` options; it uses the runtime defaults.
</Note>

| Field | Default | Description |
|-------|---------|-------------|
| `enableReliableChannel` | `true` | Allocate the reliable (ordered, guaranteed) channel. |
| `enableUnreliableChannel` | `true` | Allocate the unreliable (best-effort) channel. |
| `messageSize` | `2048` | Max bytes per message slot. Must be `> 44` and is capped at `65536`. |
| `maxIncomingMessages` | `1024` | Max queued incoming messages per channel. |

```javascript
Wavedash.init({
  debug: false,
  p2p: {
    messageSize: 4096,
    maxIncomingMessages: 512
  }
});
```

### Reading the runtime limits

Query the resolved limits after `init()` to validate outgoing payloads or size receive buffers:

<CodeGroup>
```javascript JavaScript
const maxPayload = Wavedash.getP2PMaxPayloadSize();
const maxQueued  = Wavedash.getP2PMaxIncomingMessages();

// Pre-allocated scratch buffer for zero-copy outgoing messages. Write your
// payload bytes into the buffer, then pass it to sendP2PMessage with a
// payloadSize. Engine SDKs use this internally.
const scratch = Wavedash.getP2POutgoingMessageBuffer();
scratch[0] = 0x01;
scratch[1] = 0x02;
Wavedash.sendP2PMessage(peerId, 0, true, scratch, 2);
```

```csharp Unity
int maxPayload = Wavedash.SDK.MAX_PAYLOAD_SIZE;
```

```lua Defold
local max_payload = wavedash.get_p2p_max_payload_size()
local max_queued = wavedash.get_p2p_max_incoming_messages()

local scratch = wavedash.get_p2p_outgoing_message_buffer()
-- Fill `scratch` with your payload bytes, then pass it with an explicit size.
wavedash.send_p2p_message(peer_id, 0, true, scratch, payload_size)
```
</CodeGroup>

In Godot the same values are read internally — games typically don't need them directly, since `drain_p2p_channel` allocates the buffer for you.
