M1: add NGO + Multiplayer Services packages and prototype scripts (compiling)

Netcode for GameObjects 2.13.0 + Unity Multiplayer Services 2.2.4 (unified
Relay/Lobby SDK; supersedes deprecated standalone relay/lobby). Four M1
scripts, verified against live editor reflection and compiling clean:
- Net/NetworkBootstrap.cs  Relay host/join-by-code harness (IMGUI)
- Net/PlayerDriver.cs       replicated host-auth capsule (input->ServerRpc->host)
- Bot/TreadPart.cs          v0.1 tread feel: snap-to-cruise/lock-pivot/lean
- Bot/SeatManager.cs        left/right tread seat claim + leaver-safety

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-07-10 23:24:47 +01:00
parent f3ab75312c
commit 351a7f1f82
15 changed files with 1006 additions and 4 deletions

View file

@ -0,0 +1,257 @@
// 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<UnityTransport>()
: GetComponent<UnityTransport>();
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<string> 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<bool> 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();
}
}