Reorganize scripts into subfolders by responsibility
Move scripts under Assets/Scripts/ into Components, GameMode, Helper, Network, Enum, Struct and ScriptableObject folders. Pure file moves (plus folder .meta files); no code changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
382bfb36df
commit
e23fae0c50
126 files changed
+112
No files matched your search
@@ -0,0 +1,241 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using Unity.Services.Authentication;
|
||||
using Unity.Services.Core;
|
||||
using Unity.Services.Multiplayer;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Online hosting/joining over Unity Gaming Services so players can connect across the internet
|
||||
/// without port forwarding. Uses the unified Multiplayer Services "Sessions" API, which bundles
|
||||
/// Relay (NAT-free messaging via Unity's relay servers) + Lobby (the public server list) + anonymous
|
||||
/// Authentication behind single calls, and configures + starts the NGO transport itself - so we do
|
||||
/// NOT call <see cref="NetworkManager.StartHost"/>/<c>StartClient</c> here; CreateSessionAsync starts
|
||||
/// the host and JoinSession* starts the client.
|
||||
///
|
||||
/// Terminology: the UGS "session" is the entry in the public server list (network layer). It is a
|
||||
/// different thing from the in-game "Lobby" scene (the pre-match staging area that <see cref="MatchManager"/>
|
||||
/// loads after the connection is up). This component does not touch the match flow at all - the usual
|
||||
/// NetworkManager callbacks fire exactly as they do for loopback hosting, so MatchManager, spawning and
|
||||
/// authority are unaffected.
|
||||
///
|
||||
/// This is the logic layer only: state + async host/refresh/join/disconnect calls, surfaced through the
|
||||
/// <see cref="Changed"/>/<see cref="SessionsChanged"/> events. The uGUI server browser that drives it lives
|
||||
/// on its own canvas (<see cref="OnlineSessionBrowserUI"/>); this component carries no UI of its own.
|
||||
/// Requires the project to be linked to a UGS cloud project (Project Settings > Services) with Relay,
|
||||
/// Lobby and Authentication (Anonymous) enabled; otherwise sign-in fails with a clear status message.
|
||||
/// Attach to the same GameObject as the NetworkManager.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(NetworkManager))]
|
||||
[DisallowMultipleComponent]
|
||||
public class OnlineSessionManager : MonoBehaviour
|
||||
{
|
||||
[Tooltip("Maximum players per hosted session (host included).")]
|
||||
public int maxPlayers = 8;
|
||||
|
||||
private NetworkManager _net;
|
||||
private ISession _session; // active session (host or client); null when not in one
|
||||
private List<ISessionInfo> _sessions = new List<ISessionInfo>();
|
||||
private string _hostName = "";
|
||||
private string _status = "Idle";
|
||||
private bool _busy;
|
||||
private bool _leaving; // suppress the defensive leave while an explicit leave is in flight
|
||||
|
||||
/// <summary>Raised when status, busy state, or the connection state changes, so a bound UI can re-render.
|
||||
/// Async continuations resume on the main thread, so handlers run there.</summary>
|
||||
public event Action Changed;
|
||||
|
||||
/// <summary>Raised after the browsable session list is replaced (a refresh completed).</summary>
|
||||
public event Action SessionsChanged;
|
||||
|
||||
public string Status => _status;
|
||||
public bool IsBusy => _busy;
|
||||
public bool IsConnected => _net && _net.IsListening;
|
||||
public IReadOnlyList<ISessionInfo> Sessions => _sessions;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_net = GetComponent<NetworkManager>();
|
||||
_hostName = $"{SystemInfo.deviceName} game";
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (!_net) return;
|
||||
_net.OnServerStarted += OnNetworkStarted;
|
||||
_net.OnClientStarted += OnNetworkStarted;
|
||||
_net.OnServerStopped += OnNetworkStopped;
|
||||
_net.OnClientStopped += OnNetworkStopped;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (!_net) return;
|
||||
_net.OnServerStarted -= OnNetworkStarted;
|
||||
_net.OnClientStarted -= OnNetworkStarted;
|
||||
_net.OnServerStopped -= OnNetworkStopped;
|
||||
_net.OnClientStopped -= OnNetworkStopped;
|
||||
}
|
||||
|
||||
private void OnNetworkStarted() => Changed?.Invoke();
|
||||
|
||||
// The match-end / disconnect / Shutdown paths stop the NetworkManager without telling UGS. Leave the
|
||||
// session here too so the Relay allocation and the public lobby entry are released.
|
||||
private void OnNetworkStopped(bool _)
|
||||
{
|
||||
var stale = _session;
|
||||
_session = null;
|
||||
if (stale != null && !_leaving) LeaveSilently(stale);
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private async void LeaveSilently(ISession session)
|
||||
{
|
||||
try { await session.LeaveAsync(); }
|
||||
catch (Exception e) { Debug.LogWarning($"[OnlineSession] leave-on-stop failed: {e.Message}"); }
|
||||
}
|
||||
|
||||
// ----- Public API (driven by OnlineSessionBrowserUI) -------------------------------------------------
|
||||
|
||||
public void Host() => HostAsync();
|
||||
public void Refresh() => RefreshAsync();
|
||||
public void Join(string sessionId) => JoinAsync(sessionId);
|
||||
public void Disconnect() => DisconnectAsync();
|
||||
|
||||
// ----- Implementation --------------------------------------------------------------------------------
|
||||
|
||||
private void SetStatus(string status)
|
||||
{
|
||||
_status = status;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private void SetBusy(bool busy)
|
||||
{
|
||||
_busy = busy;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task EnsureSignedInAsync()
|
||||
{
|
||||
if (UnityServices.State != ServicesInitializationState.Initialized)
|
||||
{
|
||||
// Anonymous tokens are cached per machine under company/product, NOT per project path - so
|
||||
// junction-mirror clones (shared ProjectSettings) would otherwise resolve to one UGS player id
|
||||
// and collide on join (409 LobbyConflict). Namespacing the auth cache by a hash of this install's
|
||||
// path gives each clone a stable, distinct identity. Harmless in production - separate machines
|
||||
// already have separate caches; this only matters for same-machine clone testing.
|
||||
var options = new InitializationOptions();
|
||||
options.SetProfile(ProfileForThisInstall());
|
||||
await UnityServices.InitializeAsync(options);
|
||||
}
|
||||
if (!AuthenticationService.Instance.IsSignedIn)
|
||||
await AuthenticationService.Instance.SignInAnonymouslyAsync();
|
||||
}
|
||||
|
||||
// Deterministic, regex-safe (^[a-zA-Z0-9_-]{1,30}$) profile derived from the install path, so the same
|
||||
// clone is always the same UGS player across restarts - no random per-launch identity, no orphaned ghost
|
||||
// players/lobbies piling up. SHA1 over dataPath -> "mirror_" + 12 hex chars = 19 chars (fits the limit).
|
||||
private static string ProfileForThisInstall()
|
||||
{
|
||||
using (var sha1 = System.Security.Cryptography.SHA1.Create())
|
||||
{
|
||||
var bytes = sha1.ComputeHash(System.Text.Encoding.UTF8.GetBytes(Application.dataPath));
|
||||
var name = new System.Text.StringBuilder("mirror_", 19);
|
||||
for (int i = 0; i < 6; i++) name.Append(bytes[i].ToString("x2"));
|
||||
return name.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// Shared busy/sign-in/error skeleton around one session operation; the body carries the
|
||||
// op-specific statuses and state.
|
||||
private async System.Threading.Tasks.Task RunSessionOpAsync(string failLabel, Func<System.Threading.Tasks.Task> body)
|
||||
{
|
||||
SetBusy(true);
|
||||
try
|
||||
{
|
||||
SetStatus("Signing in…");
|
||||
await EnsureSignedInAsync();
|
||||
await body();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
SetStatus($"{failLabel} failed: {e.Message}");
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally { SetBusy(false); }
|
||||
}
|
||||
|
||||
private async void HostAsync()
|
||||
{
|
||||
if (_busy || IsConnected) return;
|
||||
await RunSessionOpAsync("Host", async () =>
|
||||
{
|
||||
SetStatus("Creating session…");
|
||||
var options = new SessionOptions
|
||||
{
|
||||
Name = string.IsNullOrWhiteSpace(_hostName) ? "Chameleon game" : _hostName.Trim(),
|
||||
MaxPlayers = Mathf.Max(2, maxPlayers)
|
||||
}.WithRelayNetwork();
|
||||
_session = await MultiplayerService.Instance.CreateSessionAsync(options);
|
||||
SetStatus($"Hosting \"{_session.Name}\" (code {_session.Code})");
|
||||
});
|
||||
}
|
||||
|
||||
private async void RefreshAsync()
|
||||
{
|
||||
if (_busy) return;
|
||||
await RunSessionOpAsync("Refresh", async () =>
|
||||
{
|
||||
SetStatus("Querying sessions…");
|
||||
var results = await MultiplayerService.Instance.QuerySessionsAsync(new QuerySessionsOptions());
|
||||
_sessions = new List<ISessionInfo>(results.Sessions);
|
||||
SessionsChanged?.Invoke();
|
||||
SetStatus($"{_sessions.Count} session(s) found");
|
||||
});
|
||||
}
|
||||
|
||||
private async void JoinAsync(string sessionId)
|
||||
{
|
||||
if (_busy || IsConnected || string.IsNullOrEmpty(sessionId)) return;
|
||||
await RunSessionOpAsync("Join", async () =>
|
||||
{
|
||||
SetStatus("Joining session…");
|
||||
_session = await MultiplayerService.Instance.JoinSessionByIdAsync(sessionId);
|
||||
SetStatus($"Joined \"{_session.Name}\"");
|
||||
});
|
||||
}
|
||||
|
||||
private async void DisconnectAsync()
|
||||
{
|
||||
if (_busy) return;
|
||||
|
||||
// A UGS session leaves cleanly (LeaveAsync also tears down the NGO connection). A plain loopback
|
||||
// host/client - if one is ever active without a session - just shuts the transport down.
|
||||
if (_session != null)
|
||||
{
|
||||
SetBusy(true);
|
||||
_leaving = true;
|
||||
var session = _session;
|
||||
_session = null;
|
||||
try
|
||||
{
|
||||
SetStatus("Leaving…");
|
||||
await session.LeaveAsync();
|
||||
SetStatus("Left session");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
SetStatus($"Leave failed: {e.Message}");
|
||||
Debug.LogException(e);
|
||||
}
|
||||
finally { _leaving = false; SetBusy(false); }
|
||||
}
|
||||
else if (IsConnected)
|
||||
{
|
||||
_net.Shutdown();
|
||||
SetStatus("Disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user