using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; using Colyseus; using Colyseus.Schema; /// /// Singleton managing the Colyseus connection, room lifecycle, remote player spawning, /// and game-phase events (eliminated, qualified, roundStart, roundEnd, gameEnd). /// public class NetworkManager : MonoBehaviour { public static NetworkManager Instance { get; private set; } private const string serverURL = "wss://game.rolld.kerboul.me"; [Header("Prefab")] [Tooltip("Prefab for remote players (must have RemotePlayerController)")] public GameObject remotePlayerPrefab; // --- Public state for UI --- public bool IsConnected { get; private set; } public string RoomId { get; private set; } = ""; public string LocalSessionId { get; private set; } = ""; public int PlayerCount { get; private set; } public string ConnectionStatus { get; private set; } = "Déconnecté"; public string LastError { get; private set; } = ""; // Expose remote players for debug UI public Dictionary RemotePlayers => _remotePlayers; // Local player info (set during join) public string LocalPlayerName { get; private set; } = ""; public Color LocalPlayerColor { get; private set; } = Color.white; // --- Room listing --- [System.Serializable] public class RoomMeta { public string name; } [System.Serializable] public class RoomInfo { public string roomId; public int clients; public int maxClients; public RoomMeta metadata; } [System.Serializable] private class RoomListWrapper { public List items; } public event Action OnRoomsRefreshed; public void FetchRooms() => StartCoroutine(DoFetchRooms()); private IEnumerator DoFetchRooms() { using var req = UnityWebRequest.Get($"{serverURL.Replace("wss://", "https://").Replace("ws://", "http://")}/rooms"); yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) { OnRoomsRefreshed?.Invoke(Array.Empty()); yield break; } var wrapper = JsonUtility.FromJson($"{{\"items\":{req.downloadHandler.text}}}"); OnRoomsRefreshed?.Invoke(wrapper?.items?.ToArray() ?? Array.Empty()); } // --- Events --- public event Action OnConnected; public event Action OnDisconnected; public event Action OnPlayerJoined; public event Action OnPlayerLeft; // Game flow events public event Action OnPhaseChanged; // phase name public event Action OnCountdownChanged; // seconds remaining public event Action OnEliminated; // sessionId, reason public event Action OnQualified; // sessionId public event Action OnRoundStart; // roundNumber, mode, totalRounds public event Action OnRoundEnd; // roundNumber public event Action OnGameEnd; // winnerName // --- Internals --- private Client _client; private Room _room; private StateCallbackStrategy _callbacks; private readonly Dictionary _remotePlayers = new(); private float _broadcastTimer; private const float BROADCAST_INTERVAL = 0.01667f; // ~60/sec private bool _isJoining; private Transform _localPlayer; private Rigidbody _localPlayerRb; private Vector3 _lastSentPos; private Vector3 _lastSentVel; private Vector3 _lastSentAngVel; private const float POS_THRESHOLD = 0.005f; private const float VEL_THRESHOLD = 0.05f; private string _lastPhase = ""; void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } void Update() { if (!IsConnected || _room == null) return; _broadcastTimer += Time.deltaTime; if (_broadcastTimer >= BROADCAST_INTERVAL) { _broadcastTimer = 0f; BroadcastPosition(); } } public NetworkPlayer GetLocalPlayerState() { if (_room == null || _room.State.players == null || string.IsNullOrEmpty(LocalSessionId)) return null; _room.State.players.TryGetValue(LocalSessionId, out var player); return player; } // ─── Join / Leave ──────────────────────────────────────────────────── // ─── Join helpers ───────────────────────────────────────────────────── private Dictionary BuildJoinOptions(string playerName, Color color) => new() { { "name", playerName }, { "colorR", color.r }, { "colorG", color.g }, { "colorB", color.b }, }; private void PrepareJoin(string playerName, Color color) { _isJoining = true; ConnectionStatus = "Connexion en cours..."; LastError = ""; LocalPlayerName = playerName; LocalPlayerColor = color; PlayerPrefs.SetString("rolld_player_name", playerName); _client = new Client(serverURL); } private void FinishJoin() { LocalSessionId = _room.SessionId; RoomId = _room.RoomId; IsConnected = true; ConnectionStatus = "Connecté"; Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}"); _callbacks = Callbacks.Get(_room); _callbacks.OnAdd(state => state.players, (key, player) => OnPlayerAdd(key, player)); _callbacks.OnRemove(state => state.players, (key, player) => OnPlayerRemove(key, player)); _callbacks.Listen(state => state.phase, (v, _) => _OnPhaseChanged(v)); _callbacks.Listen(state => state.countdown, (v, _) => OnCountdownChanged?.Invoke(v)); _room.OnMessage("eliminated", msg => { OnEliminated?.Invoke(msg.sessionId, msg.reason); }); _room.OnMessage ("qualified", msg => { OnQualified?.Invoke(msg.sessionId); }); _room.OnMessage("roundStart", msg => { OnRoundStart?.Invoke(msg.round, msg.mode, msg.totalRounds); }); _room.OnMessage ("roundEnd", msg => { OnRoundEnd?.Invoke(msg.round); }); _room.OnMessage ("gameEnd", msg => { OnGameEnd?.Invoke(msg.winner); }); _room.OnMessage("chat", msg => { ChatUI.Instance?.ReceiveChatMessage(msg); }); _room.OnLeave += OnRoomLeave; // Seed players already present in the room (state decoded before callbacks were registered) if (_room.State.players != null) { foreach (var kvp in _room.State.players) OnPlayerAdd(kvp.Key, kvp.Value); } OnConnected?.Invoke(); } private void HandleJoinError(Exception e) { Debug.LogError($"[Network] Failed to join: {e.Message}"); ConnectionStatus = "Erreur de connexion"; LastError = e.Message; IsConnected = false; } // ─── Public join methods ────────────────────────────────────────────── public async void JoinArena(string playerName, Color color) { if (_isJoining || IsConnected) return; PrepareJoin(playerName, color); try { _room = await _client.JoinOrCreate("arena", BuildJoinOptions(playerName, color)); FinishJoin(); } catch (Exception e) { HandleJoinError(e); } finally { _isJoining = false; } } public async void JoinByRoomId(string roomId, string playerName, Color color) { if (_isJoining || IsConnected) return; PrepareJoin(playerName, color); try { _room = await _client.JoinById(roomId, BuildJoinOptions(playerName, color)); FinishJoin(); } catch (Exception e) { HandleJoinError(e); } finally { _isJoining = false; } } public async void CreateRoom(string playerName, Color color, string roomName = null) { if (_isJoining || IsConnected) return; PrepareJoin(playerName, color); try { var opts = BuildJoinOptions(playerName, color); if (roomName != null) opts["roomName"] = roomName; _room = await _client.Create("arena", opts); FinishJoin(); } catch (Exception e) { HandleJoinError(e); } finally { _isJoining = false; } } public async void LeaveRoom() { if (_room != null) await _room.Leave(); Cleanup(); } public async void SendReady() { if (_room != null && IsConnected) await _room.Send("ready", null); } public async void SendCheckpoint(int index) { if (_room != null && IsConnected) await _room.Send("checkpointReached", new { index }); } public async void SendChatMessage(string text) { if (_room != null && IsConnected) await _room.Send("chat", new { text }); } // ─── State Callbacks ───────────────────────────────────────────────── private void _OnPhaseChanged(string phase) { if (phase == _lastPhase) return; _lastPhase = phase; Debug.Log($"[Network] Phase → {phase}"); OnPhaseChanged?.Invoke(phase); } private void OnPlayerAdd(string sessionId, NetworkPlayer player) { Debug.Log($"[Network] Player joined: {sessionId} ({player.name})"); PlayerCount = _room.State.players?.Count ?? 0; if (sessionId == LocalSessionId) return; if (_remotePlayers.ContainsKey(sessionId)) return; // prevent duplicate spawn { Vector3 spawnPos = new Vector3(player.x, player.y, player.z); GameObject remoteBall = remotePlayerPrefab != null ? Instantiate(remotePlayerPrefab, spawnPos, Quaternion.identity) : GameObject.CreatePrimitive(PrimitiveType.Sphere); remoteBall.transform.position = spawnPos; remoteBall.name = $"RemotePlayer_{player.name}_{sessionId[..6]}"; var controller = remoteBall.GetComponent() ?? remoteBall.AddComponent(); controller.Initialize(sessionId, player.name, new Color(player.colorR, player.colorG, player.colorB)); _remotePlayers[sessionId] = controller; } _callbacks.OnChange(player, () => OnPlayerChange(sessionId, player)); OnPlayerJoined?.Invoke(sessionId); } private void OnPlayerRemove(string sessionId, NetworkPlayer player) { Debug.Log($"[Network] Player left: {sessionId}"); PlayerCount = _room.State.players?.Count ?? 0; if (_remotePlayers.TryGetValue(sessionId, out var controller)) { if (controller != null && controller.gameObject != null) Destroy(controller.gameObject); _remotePlayers.Remove(sessionId); } OnPlayerLeft?.Invoke(sessionId); } private void OnPlayerChange(string sessionId, NetworkPlayer player) { if (sessionId == LocalSessionId) return; if (_remotePlayers.TryGetValue(sessionId, out var controller)) { controller.SetTargetState( new Vector3(player.x, player.y, player.z), new Vector3(player.vx, player.vy, player.vz), new Quaternion(player.rx, player.ry, player.rz, player.rw), player.t, new Vector3(player.avx, player.avy, player.avz) ); controller.SetVisible(!player.isEliminated); } } // ─── Position Broadcasting ──────────────────────────────────────────── private void BroadcastPosition() { if (_room == null || !IsConnected) return; if (_localPlayer == null) { var pc = FindFirstObjectByType(); if (pc != null) { _localPlayer = pc.transform; _localPlayerRb = pc.GetComponent(); } else return; } Vector3 pos = _localPlayer.position; Vector3 vel = _localPlayerRb != null ? _localPlayerRb.linearVelocity : Vector3.zero; Vector3 angVel = _localPlayerRb != null ? _localPlayerRb.angularVelocity : Vector3.zero; if (Vector3.Distance(pos, _lastSentPos) < POS_THRESHOLD && Vector3.Distance(vel, _lastSentVel) < VEL_THRESHOLD && Vector3.Distance(angVel, _lastSentAngVel) < VEL_THRESHOLD) return; _lastSentPos = pos; _lastSentVel = vel; _lastSentAngVel = angVel; Quaternion rot = _localPlayer.rotation; var data = new Dictionary { { "x", pos.x }, { "y", pos.y }, { "z", pos.z }, { "vx", vel.x }, { "vy", vel.y }, { "vz", vel.z }, { "rx", rot.x }, { "ry", rot.y }, { "rz", rot.z }, { "rw", rot.w }, { "avx", angVel.x }, { "avy", angVel.y }, { "avz", angVel.z } }; _ = _room.Send("position", data); } // ─── Room Lifecycle ─────────────────────────────────────────────────── private void OnRoomLeave(int code) { Debug.Log($"[Network] Left room (code: {code})"); OnDisconnected?.Invoke(); // before Cleanup so listeners still have LocalPlayerName Cleanup(); } private void Cleanup() { IsConnected = false; ConnectionStatus = "Déconnecté"; RoomId = ""; PlayerCount = 0; LocalPlayerName = ""; LocalPlayerColor = Color.white; _lastPhase = ""; foreach (var kvp in _remotePlayers) { if (kvp.Value != null && kvp.Value.gameObject != null) Destroy(kvp.Value.gameObject); } _remotePlayers.Clear(); _room = null; _client = null; _callbacks = null; _localPlayer = null; _localPlayerRb = null; } void OnDestroy() { if (_room != null) _ = _room.Leave(false); } } // ─── Message DTOs ───────────────────────────────────────────────────────────── [Serializable] public class EliminatedMsg { public string sessionId; public string name; public string reason; } [Serializable] public class QualifiedMsg { public string sessionId; public string name; } [Serializable] public class RoundStartMsg { public int round; public string mode; public int totalRounds; } [Serializable] public class RoundEndMsg { public int round; } [Serializable] public class GameEndMsg { public string winner; }