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_ESTABLISHEDandP2P_PEER_RECONNECTED - Remove on
P2P_PEER_RECONNECTING,P2P_PEER_DISCONNECTED, andP2P_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
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | UserId | undefined | Yes | Target peer's user ID; pass undefined to broadcast |
channel | number | Yes (positional; default 0) | Channel number (0-7) |
reliable | boolean | Yes (positional; default true) | Use the reliable (ordered, guaranteed) channel |
payload | Uint8Array | Yes | Binary data to send |
payloadSize | number | No | Bytes 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)
endconst message = Wavedash.readP2PMessageFromChannel(0);
if (message) {
console.log(`From: ${message.fromUserId}, Data: ${message.payload}`);
}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 |
WavedashSDK.send_p2p_message("", data, 0, true) # reliable
WavedashSDK.send_p2p_message("", data, 1, false) # unreliableWavedash.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)
endWavedash.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
| Event | Description |
|---|---|
P2P_CONNECTION_ESTABLISHED | Both data channels to a peer are open and ready |
P2P_PEER_RECONNECTING | Peer's connection dropped and the SDK is trying to restore it. Treat as "unreachable, but keep state" |
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 the connection failed) |
P2P_CONNECTION_FAILED | Terminal failure — the SDK couldn't reconnect, or couldn't connect at all |
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. 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).
| 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. |
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.