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_ESTABLISHEDandP2P_PEER_RECONNECTED - Remove on
P2P_PEER_RECONNECTING,P2P_PEER_DISCONNECTED, andP2P_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
| 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
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):
| 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) # 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
| 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:
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.
| 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;
In Godot the same values are read internally — games typically don't need them directly, since drain_p2p_channel allocates the buffer for you.