// JANKENBOTS — M1 step-1 connectivity harness. // // This is THROWAWAY prototype code. Its only job is to get two friends into the // same networked session with zero port-forwarding: one clicks Host (which spins // up a Unity Relay allocation and prints a short join code), the other types that // code and clicks Join. Everything is host-authoritative from here on out; this // file just establishes the pipe. // // Attach this component to the SAME GameObject that carries NetworkManager + // UnityTransport (see the M1 scene-wiring notes). The IMGUI panel at the bottom is // deliberately ugly — it exists so we can smoke-test connectivity before any real // UI exists. using System; using System.Threading.Tasks; using UnityEngine; using Unity.Netcode; using Unity.Netcode.Transports.UTP; using Unity.Services.Core; using Unity.Services.Authentication; using Unity.Services.Relay; // RelayService, RelayServiceException using Unity.Services.Relay.Models; // Allocation, JoinAllocation, AllocationUtils <-- REQUIRED for AllocationUtils public class NetworkBootstrap : MonoBehaviour { [Header("Relay")] // maxConnections = number of CLIENTS excluding the host. Keep this small for M1 // smoke-testing; bump toward 24 for the ~25-player target later. [SerializeField] private int maxConnections = 4; // "dtls" = encrypted UDP, the right choice for desktop friends. ("udp" is // unencrypted; "wss" is WebGL-only and would also need utp.UseWebSockets = true.) [SerializeField] private string connectionType = "dtls"; // Cached transport pulled off the same GameObject at startup. private UnityTransport _utp; // The join code the host hands out. Empty until we've successfully hosted. private string _hostJoinCode = ""; // What the joining player types in. Bound to the OnGUI text field. private string _joinCodeInput = ""; // Simple UI/log line so the harness gives feedback without opening the console. private string _status = "Idle — Host or Join to begin."; // Guard so double-clicking Host/Join doesn't fire two overlapping async flows. private bool _busy; // ----------------------------------------------------------------------------- // Startup: bring Unity Services online and sign in anonymously ONCE. Relay and // Auth both refuse to work until this completes, so we do it eagerly on Start // rather than lazily inside Host/Join (which also re-guard, so it's safe either way). // ----------------------------------------------------------------------------- private async void Start() { _utp = NetworkManager.Singleton != null ? NetworkManager.Singleton.GetComponent() : GetComponent(); if (_utp == null) Debug.LogError("[NetworkBootstrap] No UnityTransport found on the NetworkManager GameObject."); await EnsureServicesReady(); } // Idempotent: safe to call before every Host/Join. Guarded by the Services state // machine and the sign-in flag so we never double-initialize or double-sign-in. private async Task EnsureServicesReady() { try { if (UnityServices.State != ServicesInitializationState.Initialized) await UnityServices.InitializeAsync(); if (!AuthenticationService.Instance.IsSignedIn) await AuthenticationService.Instance.SignInAnonymouslyAsync(); } catch (Exception e) { // A linked Unity Cloud project ID (Project Settings ▸ Services) is required // or InitializeAsync throws here. Debug.LogError($"[NetworkBootstrap] Services init/sign-in failed: {e}"); _status = "Services init failed — see console."; throw; // let the caller abort its Host/Join flow. } } // ----------------------------------------------------------------------------- // HOST: create a Relay allocation, feed it into UnityTransport, fetch the human- // friendly join code, then StartHost(). Returns the join code (or null on failure). // ----------------------------------------------------------------------------- public async Task HostGame() { if (_busy) return null; _busy = true; _status = "Hosting…"; try { await EnsureServicesReady(); // Reserve a slot on Relay. This is where the actual relay server is picked. Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxConnections); // Hand the allocation to the transport. NOTE: with the unified // com.unity.services.multiplayer SDK, AllocationUtils.ToRelayServerData is the // ONLY valid conversion — the old `new RelayServerData(allocation, "dtls")` // ctor does not exist here. _utp.SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, connectionType)); // The short code friends will type to join. _hostJoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); // SetRelayServerData must happen BEFORE StartHost. StartHost() returns bool. if (!NetworkManager.Singleton.StartHost()) { Debug.LogError("[NetworkBootstrap] StartHost() returned false."); _status = "StartHost failed — see console."; return null; } _status = $"Hosting. Join code: {_hostJoinCode}"; Debug.Log($"[NetworkBootstrap] Hosting with join code {_hostJoinCode}"); return _hostJoinCode; } catch (RelayServiceException e) { Debug.LogError($"[NetworkBootstrap] Relay error while hosting: {e}"); _status = "Relay error while hosting — see console."; return null; } catch (Exception e) { Debug.LogError($"[NetworkBootstrap] Unexpected error while hosting: {e}"); _status = "Host failed — see console."; return null; } finally { _busy = false; } } // ----------------------------------------------------------------------------- // JOIN: resolve the join code into a JoinAllocation, configure transport, StartClient(). // ----------------------------------------------------------------------------- public async Task JoinGame(string joinCode) { if (_busy) return false; if (string.IsNullOrWhiteSpace(joinCode)) { Debug.LogError("[NetworkBootstrap] JoinGame called with an empty code."); _status = "Enter a join code first."; return false; } _busy = true; _status = "Joining…"; try { await EnsureServicesReady(); // Relay codes are case-insensitive but we trim stray whitespace/newlines. JoinAllocation allocation = await RelayService.Instance.JoinAllocationAsync(joinCode.Trim()); _utp.SetRelayServerData(AllocationUtils.ToRelayServerData(allocation, connectionType)); if (!NetworkManager.Singleton.StartClient()) { Debug.LogError("[NetworkBootstrap] StartClient() returned false."); _status = "StartClient failed — see console."; return false; } _status = "Joined. Waiting for host…"; Debug.Log("[NetworkBootstrap] Client started, connecting to host via Relay."); return true; } catch (RelayServiceException e) { Debug.LogError($"[NetworkBootstrap] Relay error while joining: {e}"); _status = "Bad code / Relay error — see console."; return false; } catch (Exception e) { Debug.LogError($"[NetworkBootstrap] Unexpected error while joining: {e}"); _status = "Join failed — see console."; return false; } finally { _busy = false; } } // ----------------------------------------------------------------------------- // Dead-simple IMGUI harness. Once we're connected the buttons hide themselves and // we just show status + (for the host) a selectable join code you can copy out. // async void event handlers are fine for a throwaway prototype. // ----------------------------------------------------------------------------- private void OnGUI() { const float pad = 10f; GUILayout.BeginArea(new Rect(pad, pad, 320f, 260f), GUI.skin.box); GUILayout.Label("JANKENBOTS — M1 connectivity"); GUILayout.Label(_status); GUILayout.Space(6f); bool live = NetworkManager.Singleton != null && (NetworkManager.Singleton.IsHost || NetworkManager.Singleton.IsServer || NetworkManager.Singleton.IsClient); if (!live) { GUI.enabled = !_busy; // HOST ------------------------------------------------------------- if (GUILayout.Button("Host", GUILayout.Height(30f))) _ = HostGame(); // fire-and-forget; HostGame owns its own error logging. GUILayout.Space(8f); // JOIN ------------------------------------------------------------- GUILayout.Label("Join code:"); _joinCodeInput = GUILayout.TextField(_joinCodeInput, GUILayout.Height(24f)); if (GUILayout.Button("Join", GUILayout.Height(30f))) _ = JoinGame(_joinCodeInput); GUI.enabled = true; } else { // Connected. If we're the host, surface the code as a selectable text field // so the tester can highlight + copy it to paste into chat. if (NetworkManager.Singleton.IsHost && !string.IsNullOrEmpty(_hostJoinCode)) { GUILayout.Label("Share this join code:"); // A read-only-ish selectable field: users can select/copy, edits are ignored. GUILayout.TextField(_hostJoinCode, GUILayout.Height(28f)); } GUILayout.Space(8f); if (GUILayout.Button("Disconnect", GUILayout.Height(28f))) { NetworkManager.Singleton.Shutdown(); _hostJoinCode = ""; _status = "Disconnected."; } } GUILayout.EndArea(); } }