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 STUN/TURN. There is no game server in the loop — peers exchange game data directly, falling back to a TURN relay only when a direct path can't be established.

All P2P payloads are binary: Uint8Array in JavaScript, byte[] or ArraySegment<byte> in Unity, PackedByteArray in Godot, and a Lua string of raw bytes in Defold.

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.broadcast_p2p_message(0, true, data)
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 is false when P2P isn't connected, the channel type you're broadcasting on (reliable or unreliable) is disabled in the p2p config, no peer channels of that type exist yet, or the channel or payload is invalid. true only means the message was handed to the channels that existed — not that any of them were open, and not that a peer received it.

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 call returns false without emitting an event when that peer's channels aren't open or P2P isn't initialized, so check the return value. P2P_PACKET_DROPPED with reason PEER_NOT_READY only fires when a send passes that check but still fails — the channel type is disabled in the p2p config, or the send itself throws. 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.send_p2p_message(user_id, 0, true, data)
Wavedash.sendP2PMessage(targetUserId, 0, false, payload);

Parameters

ParameterTypeRequiredDescription
userIdUserId | undefinedYesTarget peer's user ID; pass undefined to broadcast
channelnumberYes (positional; default 0)Channel number (0-7)
reliablebooleanYes (positional; default true)Use the reliable (ordered, guaranteed) channel
payloadUint8ArrayYesBinary data to send
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]);
}
local message = wavedash.read_p2p_message_from_channel(0)
if message then
    -- message.payload is a table of byte values keyed "0".."n-1", not a string
    print("From:", message.fromUserId, "Channel:", message.channel)
end
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.broadcast_p2p_message(0, true, game_state_data)
wavedash.broadcast_p2p_message(1, false, position_data)
Wavedash.broadcastP2PMessage(0, true, gameStateData);
Wavedash.broadcastP2PMessage(1, false, positionData);

Connection state

func _ready():
    WavedashSDK.p2p_connection_established.connect(func(p): print("Connected to: ", p["userId"]))
    WavedashSDK.p2p_peer_reconnecting.connect(func(p): print("Peer unreachable, retrying: ", p["userId"]))
    WavedashSDK.p2p_peer_reconnected.connect(func(p): print("Peer back online: ", p["userId"]))
    WavedashSDK.p2p_peer_disconnected.connect(func(p): print("Disconnected: ", p["userId"]))
    WavedashSDK.p2p_connection_failed.connect(func(p): print("Connection failed: ", p["error"]))
    WavedashSDK.p2p_packet_dropped.connect(func(p): push_warning(
        "Dropped packet on ch%d (%s, %s) x%d (total %d)"
        % [p["channel"], p["direction"], p["reason"], p["droppedCount"], p["droppedTotal"]]))
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"]})");
}
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
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 connection dropped and the SDK is trying to restore it. Treat as "unreachable, but keep state"
P2P_PEER_RECONNECTEDPeer that was reconnecting is back online; resume sending
P2P_PEER_DISCONNECTEDPeer's data channel closed (left the lobby, tab closed, or the connection failed)
P2P_CONNECTION_FAILEDTerminal failure — the SDK couldn't reconnect, or couldn't connect at all
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. Each record is a 4-byte little-endian size followed by [fromUserId: 32 bytes][channel: u32 LE][dataLength: u32 LE][payload]. JavaScript games should normally use readP2PMessageFromChannel instead.

const packed = Wavedash.drainP2PChannelToBuffer(0);
const view = new DataView(packed.buffer, packed.byteOffset, packed.byteLength);
const decoder = new TextDecoder();
let offset = 0;
while (offset < packed.byteLength) {
  const size = view.getUint32(offset, true);
  offset += 4;
  const record = packed.subarray(offset, offset + size);
  const rv = new DataView(record.buffer, record.byteOffset, record.byteLength);
  const fromUserId = decoder.decode(record.subarray(0, 32)).replace(/\0+$/, "");
  const channel = rv.getUint32(32, true);
  const payload = record.subarray(40, 40 + rv.getUint32(36, true));
  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.

In Defold, pass the same table as the first argument to wavedash.init, e.g. wavedash.init({ p2p = { messageSize = 4096, maxIncomingMessages = 512 } }, on_event).

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;
local max_payload = wavedash.get_p2p_max_payload_size()
local max_queued = wavedash.get_p2p_max_incoming_messages()

-- In Defold, pass the payload as a Lua string (at most max_payload bytes);
-- its full length is sent.
wavedash.send_p2p_message(peer_id, 0, true, payload)

Godot doesn't expose these getters. send_p2p_message sizes its limit from the SDK's outgoing buffer and returns false (with a warning) for a larger payload, and drain_p2p_channel allocates the receive buffer for you.