Multiplayer Hosting Guide
How join codes and the coturn relay flow work for players and developers.
NetCrafter Relay connects players through a coturn TURN/STUN relay — no dedicated server to provision, no router port forwarding, no CGNAT problems. This guide explains how the relay flow works and how to wire the connection layer in your game.
00 — Overview#
Every peer (host and joiners alike) opens one TURN allocation on the shared relay. The host registers its relayed address with a rendezvous service under a short join code; joiners resolve that code back to the host's address and connect through the relay. The host does the star fan-out itself — BroadcastAll/SenderOnly/TargetUser Host Actions run inside whichever player's Unity process is hosting, so there's no separate server process to deploy anywhere.
This replaces the two things every earlier multiplayer setup needed: a VPS to provision, or a router to configure. Neither is required with NetCrafter Relay.
01 — Hosting a session#
Hosting just means calling Connect() on the RelayConnectionManager with no join code set:
void Start()
{
RelayConnectionManager.Instance.Connect();
}
This allocates a relayed address on the configured relay endpoint (see Relay Settings in the editor window) and registers it with the rendezvous service, which returns a join code — dispatched as a RelayHosting event with the code as its payload. Show that code to the host so they can share it with friends (e.g. GAME-4892).
private void OnEnable()
{
NetCrafterEventDispatcher.Instance?.Subscribe("RelayHosting", OnHosting);
}
private void OnHosting(object joinCode)
{
joinCodeText.text = "Join code: " + (string)joinCode;
}
02 — Joining a session#
Joiners set the join code before connecting:
RelayConnectionManager.Instance.UpdateHost("GAME-4892");
RelayConnectionManager.Instance.Connect();
UpdateHost() resets the relay connection so the following Connect() resolves the new code via the rendezvous service, allocates a relayed address, and creates a TURN permission to the host — no IP address, no port forwarding, works behind CGNAT.
03 — Connection Architecture#
INetCrafterConnection
The generated RelayConnectionManager implements INetCrafterConnection, which lets NetCrafter Relay's built-in components (and your join-flow UI) talk to the connection manager without depending on your generated namespace:
public interface INetCrafterConnection
{
bool IsConnected { get; }
bool IsHost { get; }
int LocalUserId { get; }
string LocalUserName { get; }
void Send(string cmd, byte[] payload, bool reliable);
void UpdateHost(string joinCode);
void Connect();
}
IsHost is true only on the peer whose HostActionRouter logic actually runs — every other peer's HostActionRouter.Handle() calls are simply never invoked by their own RelayConnectionManager.
NetCrafterEventDispatcher
NetCrafterEventDispatcher is a singleton MonoBehaviour event bus. The generated connection manager dispatches:
| Event | When | Payload |
|---|---|---|
RelayHosting |
Hosting registration with the rendezvous service succeeds | string join code |
ConnectionLost |
Join-code lookup fails or the connection drops | string reason |
Subscribe in Start(), unsubscribe in OnDestroy():
private Action<object> _hostingHandler;
private void Start()
{
_hostingHandler = code => OnHosting((string)code);
NetCrafterEventDispatcher.Instance?.Subscribe("RelayHosting", _hostingHandler);
NetCrafterEventDispatcher.Instance?.Subscribe("ConnectionLost", OnConnectionLost);
}
private void OnDestroy()
{
NetCrafterEventDispatcher.Instance?.Unsubscribe("RelayHosting", _hostingHandler);
NetCrafterEventDispatcher.Instance?.Unsubscribe("ConnectionLost", OnConnectionLost);
}
UpdateHost() for joining a session
Call UpdateHost() before Connect() to point the connection manager at a join code instead of hosting a new session:
var all = FindObjectsByType<MonoBehaviour>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
INetCrafterConnection cm = null;
foreach (var mb in all)
if (mb is INetCrafterConnection c) { cm = c; break; }
cm?.UpdateHost("GAME-4892");
cm?.Connect();
04 — Building a Join Flow#
A minimal join panel — join-code input, Connect button, status text, and a UnityEvent onJoinSuccess:
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using TMPro;
using PivecLabs.NetCrafterRelay;
public class JoinPanel : MonoBehaviour
{
public TMP_InputField joinCodeInput;
public Button connectButton;
public TMP_Text statusText;
public UnityEvent onJoinSuccess;
private void Start()
{
connectButton.onClick.AddListener(OnConnect);
NetCrafterEventDispatcher.Instance?.Subscribe("ConnectionLost", OnConnectionLost);
}
private void OnDestroy()
{
NetCrafterEventDispatcher.Instance?.Unsubscribe("ConnectionLost", OnConnectionLost);
}
private void OnConnect()
{
string code = joinCodeInput.text.Trim();
if (string.IsNullOrEmpty(code))
{
statusText.text = "Enter a join code.";
return;
}
var cm = FindConnectionManager();
if (cm == null) { statusText.text = "ConnectionManager not found."; return; }
statusText.text = "Connecting to " + code + "...";
cm.UpdateHost(code);
cm.Connect();
// IsConnected flips once the relay allocation + permission succeed.
StartCoroutine(WaitForConnection(cm));
}
private System.Collections.IEnumerator WaitForConnection(INetCrafterConnection cm)
{
float timeout = Time.time + 10f;
while (!cm.IsConnected && Time.time < timeout)
yield return null;
if (cm.IsConnected)
onJoinSuccess?.Invoke();
else
statusText.text = "Connection timed out.";
}
private void OnConnectionLost(object reason) { statusText.text = "Connection failed: " + reason; }
private INetCrafterConnection FindConnectionManager()
{
var all = FindObjectsByType<MonoBehaviour>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (var mb in all)
if (mb is INetCrafterConnection c) return c;
return null;
}
}
Wire onJoinSuccess in the Inspector to hide the lobby, load the game scene, or enable game HUD.
05 — Testing Locally#
For local testing without the real hosted rendezvous service, point Relay Settings at a local/test coturn instance (a turnserver with a static long-term credential is enough) and either hardcode a join code for the test or run a minimal stand-in rendezvous endpoint. Run two Unity Editor instances on the same machine (File → Open Project in New Window) and enable Run In Background in both (Edit → Project Settings → Player → Resolution and Presentation) to confirm a Reliable message round-trips host→peer and an Unreliable sync rule replicates transform state before wiring in the production rendezvous service.
NetCrafter Relay 0.1.0 — Pivec Labs