Search documentation

Find pages, sections, and content across all docs.

WavedashDocs

Multiplayer networking

Connect players directly with peer-to-peer WebRTC 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, and PackedByteArray in Godot.

Sending messages

Broadcast to all peers

func broadcast_to_all(data: PackedByteArray, reliable: bool = true):
    WavedashSDK.send_p2p_message("", data, 0, reliable)
Wavedash.SDK.BroadcastP2PMessage(data, channel: 0, reliable: true);
Wavedash.broadcastP2PMessage(0, true, new Uint8Array([1, 2, 3]));

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 for the full lifecycle.

Send to a specific peer

func send_to_peer(user_id: String, data: PackedByteArray):
    WavedashSDK.send_p2p_message(user_id, data, 0, true)
Wavedash.SDK.SendP2PMessage(userId, data, channel: 0, reliable: true);
Wavedash.sendP2PMessage(targetUserId, 0, false, payload);

Parameters

ParameterTypeRequiredDescription
userIdId<"users">For unicastTarget peer's user ID (omit or undefined to broadcast)
channelnumberYesChannel number (0-7)
payloadUint8ArrayYesBinary data to send
reliablebooleanNoUse reliable channel (default: true)
payloadSizenumberNoBytes to send from payload (default: payload.length)

Receiving messages

func _process(_delta):
    var messages = WavedashSDK.drain_p2p_channel(0)
    for msg in messages:
        var from_user = msg["identity"]
        var payload: PackedByteArray = msg["payload"]
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]);
}
const message = Wavedash.readP2PMessageFromChannel(0);
if (message) {
  console.log(`From: ${message.fromUserId}, Data: ${message.payload}`);
}

Channels

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

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

Reliable vs unreliable

TypeDescriptionBest for
ReliableGuaranteed delivery, orderedImportant state, chat, game events
UnreliableBest-effort, fasterPosition updates, input, frequent data
WavedashSDK.send_p2p_message("", data, 0, true)   # reliable
WavedashSDK.send_p2p_message("", data, 1, false)  # unreliable
Wavedash.SDK.BroadcastP2PMessage(data, channel: 0, reliable: true);
Wavedash.SDK.BroadcastP2PMessage(data, channel: 1, reliable: false);
Wavedash.broadcastP2PMessage(0, true, gameStateData);
Wavedash.broadcastP2PMessage(1, false, positionData);

Connection state

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)
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"]})");
}
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})`);
});

Events

EventDescription
P2P_CONNECTION_ESTABLISHEDBoth data channels to a peer are open and ready
P2P_PEER_RECONNECTINGPeer's ICE connection failed; SDK is attempting an ICE restart. Treat as "unreachable, but keep state" — no re-handshake happens
P2P_PEER_RECONNECTEDPeer that was reconnecting is back online; resume sending
P2P_PEER_DISCONNECTEDPeer's data channel closed (left the lobby, tab closed, or ICE restarts gave up)
P2P_CONNECTION_FAILEDTerminal failure — no TURN credentials, data-channel error, or ICE restart budget exhausted
P2P_PACKET_DROPPEDSDK 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:

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

TURN credentials are managed automatically. No configuration required.

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.

FieldDefaultDescription
enableReliableChanneltrueAllocate the reliable (ordered, guaranteed) channel.
enableUnreliableChanneltrueAllocate the unreliable (best-effort) channel.
messageSize2048Max bytes per message slot. Must be > 44 and is capped at 65536.
maxIncomingMessages1024Max queued incoming messages per channel.
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:

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);
int maxPayload = Wavedash.SDK.MAX_PAYLOAD_SIZE;

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