by Pivec Labs
Player Hosting Join codes, no port forwarding

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.

01

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.

02

01 — Hosting a session#

Hosting just means calling Connect() on the RelayConnectionManager with no join code set:

csharp
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).

csharp
private void OnEnable()
{
    NetCrafterEventDispatcher.Instance?.Subscribe("RelayHosting", OnHosting);
}

private void OnHosting(object joinCode)
{
    joinCodeText.text = "Join code: " + (string)joinCode;
}
03

02 — Joining a session#

Joiners set the join code before connecting:

csharp
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.

04

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:

csharp
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():

csharp
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:

csharp
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();
05

04 — Building a Join Flow#

A minimal join panel — join-code input, Connect button, status text, and a UnityEvent onJoinSuccess:

csharp
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.

06

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