feat: systeme de lobby avec liste de rooms

Backend:
- GET /rooms via matchMaker.query() pour lister les salles actives
- ArenaRoom: setMetadata avec nom de salle (Salle #<id6>)

NetworkManager:
- FetchRooms() / OnRoomsRefreshed event (UnityWebRequest GET /rooms)
- JoinByRoomId(), CreateRoom() en plus de JoinArena()
- Refactoring: PrepareJoin/FinishJoin/HandleJoinError pour eviter duplication

LobbyUI:
- Redesign: panel 620x520 avec setup perso (gauche) + liste rooms (droite)
- Bouton Rejoindre par salle, Creer une salle, Rejoindre n importe
- Pseudo pre-rempli depuis PlayerPrefs
- Refresh automatique toutes les 4s

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 22:14:27 +02:00
parent 44b758360c
commit 391c000a73
4 changed files with 409 additions and 277 deletions

View File

@@ -1,38 +1,27 @@
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
/// <summary> /// <summary>
/// Lobby UI displayed at scene start. Player enters a name, picks a color, /// Lobby UI: character setup + room list side by side.
/// and clicks "Rejoindre" to connect to the arena. /// - T to open/close chat, Tab for keybinds (handled elsewhere)
/// Manages the full pre-game → in-game transition: /// - Lists available rooms, lets the player create or join one
/// - Hides the Player hierarchy until connected
/// - Activates a spectator camera while in lobby
/// - Teleports the player ball to the server spawn position on join
/// Uses Dear ImGuistyle skin via ImGuiSkin.
/// </summary> /// </summary>
public class LobbyUI : MonoBehaviour public class LobbyUI : MonoBehaviour
{ {
[Header("Scene References")] [Header("Scene References")]
[Tooltip("The root 'Player' GameObject (contains PlayerSphere + cameras). Will be deactivated until connected.")]
public GameObject playerRoot; public GameObject playerRoot;
[Tooltip("The spectator camera GameObject (SpectatorCamera component).")]
public SpectatorCamera spectatorCamera; public SpectatorCamera spectatorCamera;
// Preset colors for selection private static readonly Color[] PresetColors =
private static readonly Color[] PresetColors = new Color[]
{ {
new Color(1f, 0.35f, 0.2f), // Orange-red new Color(1f, 0.35f, 0.2f),
new Color(0.2f, 0.6f, 1f), // Blue new Color(0.2f, 0.6f, 1f),
new Color(0.3f, 1f, 0.4f), // Green new Color(0.3f, 1f, 0.4f),
new Color(1f, 0.85f, 0.1f), // Yellow new Color(1f, 0.85f, 0.1f),
new Color(0.8f, 0.3f, 1f), // Purple new Color(0.8f, 0.3f, 1f),
new Color(1f, 0.5f, 0.7f), // Pink new Color(1f, 0.5f, 0.7f),
};
private static readonly string[] ColorNames = new string[]
{
"Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose"
}; };
private static readonly string[] ColorNames = { "Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose" };
// UI state // UI state
private bool _lobbyActive = true; private bool _lobbyActive = true;
@@ -42,66 +31,96 @@ public class LobbyUI : MonoBehaviour
private bool _isConnecting = false; private bool _isConnecting = false;
private bool _isReady = false; private bool _isReady = false;
// Cached color preview texture (avoid per-frame leak) // Room list
private NetworkManager.RoomInfo[] _rooms = new NetworkManager.RoomInfo[0];
private bool _roomsFetching = false;
private float _refreshTimer = 0f;
private const float REFRESH_INTERVAL = 4f;
private Vector2 _roomsScroll;
// Color preview texture
private Texture2D _colorPreviewTex; private Texture2D _colorPreviewTex;
private int _lastPreviewColorIndex = -1; private int _lastPreviewColorIndex = -1;
void Start() void Start()
{ {
// Generate a default name _playerName = PlayerPrefs.GetString("rolld_player_name", "Joueur" + Random.Range(100, 999));
_playerName = "Joueur" + Random.Range(100, 999);
// --- Hide the player hierarchy until connected ---
if (playerRoot != null) if (playerRoot != null)
playerRoot.SetActive(false); playerRoot.SetActive(false);
// --- Activate spectator camera ---
if (spectatorCamera != null) if (spectatorCamera != null)
{ {
// Wire the gameplay camera reference so spectator knows what to re-enable
var gameplayCam = playerRoot?.GetComponentInChildren<Camera>(true); var gameplayCam = playerRoot?.GetComponentInChildren<Camera>(true);
if (gameplayCam != null) if (gameplayCam != null)
spectatorCamera.gameplayCamera = gameplayCam; spectatorCamera.gameplayCamera = gameplayCam;
spectatorCamera.Activate(); spectatorCamera.Activate();
} }
// Subscribe to network events var nm = NetworkManager.Instance;
if (NetworkManager.Instance != null) if (nm != null)
{ {
NetworkManager.Instance.OnConnected += OnConnected; nm.OnConnected += OnConnected;
NetworkManager.Instance.OnDisconnected += OnDisconnected; nm.OnDisconnected += OnDisconnected;
nm.OnRoomsRefreshed += OnRoomsRefreshed;
} }
RefreshRooms();
} }
void OnDestroy() void OnDestroy()
{ {
if (NetworkManager.Instance != null) var nm = NetworkManager.Instance;
if (nm != null)
{ {
NetworkManager.Instance.OnConnected -= OnConnected; nm.OnConnected -= OnConnected;
NetworkManager.Instance.OnDisconnected -= OnDisconnected; nm.OnDisconnected -= OnDisconnected;
nm.OnRoomsRefreshed -= OnRoomsRefreshed;
} }
} }
void Update()
{
if (!_lobbyActive || _isConnecting) return;
_refreshTimer += Time.deltaTime;
if (_refreshTimer >= REFRESH_INTERVAL)
{
_refreshTimer = 0f;
RefreshRooms();
}
}
private void RefreshRooms()
{
if (_roomsFetching) return;
_roomsFetching = true;
NetworkManager.Instance?.FetchRooms();
}
private void OnRoomsRefreshed(NetworkManager.RoomInfo[] rooms)
{
_rooms = rooms;
_roomsFetching = false;
}
// ─── Network callbacks ────────────────────────────────────────────────
private void OnConnected() private void OnConnected()
{ {
_lobbyActive = false; _lobbyActive = false;
_isConnecting = false; _isConnecting = false;
_statusMessage = ""; _statusMessage = "";
CancelInvoke(nameof(CheckConnectionTimeout)); CancelInvoke(nameof(ConnectionTimeout));
// --- Activate the player hierarchy ---
if (playerRoot != null) if (playerRoot != null)
playerRoot.SetActive(true); playerRoot.SetActive(true);
// Teleport player ball to the server-assigned spawn position
var nm = NetworkManager.Instance; var nm = NetworkManager.Instance;
if (nm != null && playerRoot != null) if (nm != null && playerRoot != null)
{ {
var pc = playerRoot.GetComponentInChildren<PlayerController>(true); var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
if (pc != null) if (pc != null)
{ {
// Get spawn pos from the local player's state in the room
var localState = nm.GetLocalPlayerState(); var localState = nm.GetLocalPlayerState();
if (localState != null) if (localState != null)
{ {
@@ -115,20 +134,15 @@ public class LobbyUI : MonoBehaviour
} }
pc.transform.position = spawnPos; pc.transform.position = spawnPos;
pc.SetSpawnPosition(spawnPos); pc.SetSpawnPosition(spawnPos);
Debug.Log($"[Lobby] Player teleported to spawn: {spawnPos}");
} }
pc.enabled = true; pc.enabled = true;
// Setup local player visuals: 50% color tint + floating name label
pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor); pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor);
} }
} }
// --- Switch from spectator to gameplay camera ---
if (spectatorCamera != null) if (spectatorCamera != null)
spectatorCamera.Deactivate(); spectatorCamera.Deactivate();
// Unlock cursor for gameplay
Cursor.lockState = CursorLockMode.Locked; Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false; Cursor.visible = false;
} }
@@ -139,12 +153,11 @@ public class LobbyUI : MonoBehaviour
_isConnecting = false; _isConnecting = false;
_isReady = false; _isReady = false;
_statusMessage = "Déconnecté du serveur"; _statusMessage = "Déconnecté du serveur";
_refreshTimer = REFRESH_INTERVAL; // force immediate refresh
// Show cursor for lobby
Cursor.lockState = CursorLockMode.None; Cursor.lockState = CursorLockMode.None;
Cursor.visible = true; Cursor.visible = true;
// --- Deactivate the player hierarchy ---
if (playerRoot != null) if (playerRoot != null)
{ {
var pc = playerRoot.GetComponentInChildren<PlayerController>(true); var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
@@ -152,44 +165,55 @@ public class LobbyUI : MonoBehaviour
playerRoot.SetActive(false); playerRoot.SetActive(false);
} }
// --- Re-enable spectator camera ---
if (spectatorCamera != null) if (spectatorCamera != null)
spectatorCamera.Activate(); spectatorCamera.Activate();
} }
// ─── OnGUI ────────────────────────────────────────────────────────────
void OnGUI() void OnGUI()
{ {
if (!_lobbyActive) return; if (!_lobbyActive) return;
ImGuiSkin.EnsureReady(); ImGuiSkin.EnsureReady();
if (Cursor.lockState != CursorLockMode.None) if (Cursor.lockState != CursorLockMode.None)
{ {
Cursor.lockState = CursorLockMode.None; Cursor.lockState = CursorLockMode.None;
Cursor.visible = true; Cursor.visible = true;
} }
ImGuiSkin.DrawOverlay(); ImGuiSkin.DrawOverlay();
bool isConnected = NetworkManager.Instance != null && NetworkManager.Instance.IsConnected; bool isConnected = NetworkManager.Instance != null && NetworkManager.Instance.IsConnected;
if (!isConnected) if (!isConnected)
DrawSetupAndRoomList();
else
DrawWaitingRoom();
}
// ─── Setup + room list ────────────────────────────────────────────────
private void DrawSetupAndRoomList()
{ {
// ── Pre-connect panel ──────────────────────────────────────── const float W = 620f, H = 520f;
float panelWidth = 420; float x = (Screen.width - W) * 0.5f;
float panelHeight = 440; float y = (Screen.height - H) * 0.5f;
ImGuiSkin.BeginWindow(panelWidth, panelHeight, "ROLL'D");
GUILayout.Label("Rejoindre l'arène multijoueur", ImGuiSkin.WindowSubtitle); ImGuiSkin.BeginWindowAt(x, y, W, H, "ROLL'D");
GUILayout.Space(16); GUILayout.Label("Choisir une salle et configurer son personnage", ImGuiSkin.WindowSubtitle);
GUILayout.Space(10);
ImGuiSkin.DrawSectionHeader("PSEUDO"); GUILayout.BeginHorizontal();
// ── Left column : character setup ─────────────────────────────
GUILayout.BeginVertical(GUILayout.Width(240));
ImGuiSkin.DrawSectionHeader("PERSONNAGE");
GUILayout.Space(4); GUILayout.Space(4);
_playerName = GUILayout.TextField(_playerName, 16, ImGuiSkin.TextField, GUILayout.Height(30)); _playerName = GUILayout.TextField(_playerName, 16, ImGuiSkin.TextField, GUILayout.Height(30));
GUILayout.Space(12); GUILayout.Space(10);
ImGuiSkin.DrawSectionHeader("COULEUR"); ImGuiSkin.DrawSectionHeader("COULEUR");
GUILayout.Space(6); GUILayout.Space(4);
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
for (int i = 0; i < PresetColors.Length; i++) for (int i = 0; i < PresetColors.Length; i++)
@@ -197,20 +221,19 @@ public class LobbyUI : MonoBehaviour
Color c = PresetColors[i]; Color c = PresetColors[i];
bool selected = _selectedColorIndex == i; bool selected = _selectedColorIndex == i;
Color prevBg = GUI.backgroundColor; Color prevBg = GUI.backgroundColor;
GUI.backgroundColor = selected ? c : c * 0.7f; GUI.backgroundColor = selected ? c : c * 0.6f;
GUIStyle btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall) var btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall)
{ { fontStyle = selected ? FontStyle.Bold : FontStyle.Normal };
fontStyle = selected ? FontStyle.Bold : FontStyle.Normal,
};
if (selected) btnStyle.normal.textColor = Color.white; if (selected) btnStyle.normal.textColor = Color.white;
string label = selected ? $"▸ {ColorNames[i]}" : ColorNames[i]; if (GUILayout.Button(selected ? $"▸{ColorNames[i][0]}" : $"{ColorNames[i][0]}",
if (GUILayout.Button(label, btnStyle, GUILayout.Height(32), GUILayout.Width(60))) btnStyle, GUILayout.Height(30), GUILayout.Width(34)))
_selectedColorIndex = i; _selectedColorIndex = i;
GUI.backgroundColor = prevBg; GUI.backgroundColor = prevBg;
} }
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(4);
GUILayout.Space(4);
// Color swatch
if (_colorPreviewTex == null || _lastPreviewColorIndex != _selectedColorIndex) if (_colorPreviewTex == null || _lastPreviewColorIndex != _selectedColorIndex)
{ {
if (_colorPreviewTex == null) if (_colorPreviewTex == null)
@@ -222,55 +245,122 @@ public class LobbyUI : MonoBehaviour
_colorPreviewTex.Apply(); _colorPreviewTex.Apply();
_lastPreviewColorIndex = _selectedColorIndex; _lastPreviewColorIndex = _selectedColorIndex;
} }
GUILayout.BeginHorizontal(); var swatchStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleLeft, fontSize = 11 };
GUILayout.FlexibleSpace(); GUILayout.Label($"▌ {ColorNames[_selectedColorIndex]}", swatchStyle);
GUILayout.Box(_colorPreviewTex, GUIStyle.none, GUILayout.Width(80), GUILayout.Height(16));
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.Space(16);
GUILayout.FlexibleSpace();
// Create room button
GUI.enabled = !_isConnecting && !string.IsNullOrWhiteSpace(_playerName); GUI.enabled = !_isConnecting && !string.IsNullOrWhiteSpace(_playerName);
string buttonText = _isConnecting ? "Connexion..." : "▶ Rejoindre l'arène"; if (GUILayout.Button("+ Créer une salle", ImGuiSkin.Button, GUILayout.Height(36)))
if (GUILayout.Button(buttonText, ImGuiSkin.ButtonAccent, GUILayout.Height(44))) DoCreate();
JoinArena();
GUI.enabled = true;
GUILayout.Space(8);
GUILayout.Space(4);
// Join any (join or create fallback)
if (GUILayout.Button("▶ Rejoindre n'importe", ImGuiSkin.ButtonAccent, GUILayout.Height(36)))
DoJoinAny();
GUI.enabled = true;
GUILayout.EndVertical();
GUILayout.Space(12);
// ── Right column : room list ───────────────────────────────────
GUILayout.BeginVertical();
GUILayout.BeginHorizontal();
ImGuiSkin.DrawSectionHeader("SALLES DISPONIBLES");
GUILayout.FlexibleSpace();
GUI.enabled = !_roomsFetching;
if (GUILayout.Button(_roomsFetching ? "…" : "↻", ImGuiSkin.ButtonSmall,
GUILayout.Width(28), GUILayout.Height(22)))
{
_refreshTimer = 0f;
RefreshRooms();
}
GUI.enabled = true;
GUILayout.EndHorizontal();
GUILayout.Space(4);
float listH = H - 160f;
_roomsScroll = GUILayout.BeginScrollView(_roomsScroll, ImGuiSkin.ScrollView,
GUILayout.Height(listH));
if (_rooms.Length == 0)
{
var emptyStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleCenter };
GUILayout.FlexibleSpace();
GUILayout.Label(_roomsFetching ? "Chargement…" : "Aucune salle ouverte.", emptyStyle);
GUILayout.FlexibleSpace();
}
else
{
foreach (var room in _rooms)
{
string roomName = room.metadata?.name ?? ("Salle #" + room.roomId.Substring(0, 6));
int clients = room.clients;
int maxCli = room.maxClients;
GUILayout.BeginHorizontal();
var nameStyle = new GUIStyle(ImGuiSkin.LabelBold) { fontSize = 12 };
GUILayout.Label(roomName, nameStyle, GUILayout.Width(140));
var countStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
GUILayout.Label($"{clients} / {maxCli}", countStyle, GUILayout.Width(48));
GUILayout.FlexibleSpace();
bool full = clients >= maxCli;
GUI.enabled = !_isConnecting && !full && !string.IsNullOrWhiteSpace(_playerName);
if (GUILayout.Button(full ? "Pleine" : "▶ Rejoindre",
ImGuiSkin.ButtonSmall, GUILayout.Width(90), GUILayout.Height(26)))
DoJoinRoom(room.roomId);
GUI.enabled = true;
GUILayout.EndHorizontal();
ImGuiSkin.Separator();
GUILayout.Space(2);
}
}
GUILayout.EndScrollView();
GUILayout.EndVertical();
GUILayout.EndHorizontal(); // end columns
// ── Status bar ────────────────────────────────────────────────
GUILayout.Space(4);
if (!string.IsNullOrEmpty(_statusMessage)) if (!string.IsNullOrEmpty(_statusMessage))
{ {
bool isError = _statusMessage.Contains("Erreur") || _statusMessage.Contains("Déconnecté"); bool isError = _statusMessage.Contains("Erreur") || _statusMessage.Contains("Déconnecté");
GUIStyle statusStyle = isError ? ImGuiSkin.StatusRed : new GUIStyle(ImGuiSkin.Hint); GUILayout.Label(_statusMessage, isError ? ImGuiSkin.StatusRed : ImGuiSkin.Hint);
if (!isError) statusStyle.normal.textColor = ImGuiSkin.ColYellow;
GUILayout.Label(_statusMessage, statusStyle);
} }
ImGuiSkin.EndWindow(); ImGuiSkin.EndWindow();
} }
else
{
// ── Waiting room panel (connected, waiting for game to start) ──
float panelWidth = 380;
float panelHeight = 320;
ImGuiSkin.BeginWindow(panelWidth, panelHeight, "SALLE D'ATTENTE");
GUILayout.Label("En attente des joueurs...", ImGuiSkin.WindowSubtitle); // ─── Waiting room ─────────────────────────────────────────────────────
private void DrawWaitingRoom()
{
ImGuiSkin.BeginWindow(400f, 300f, "SALLE D'ATTENTE");
var nm = NetworkManager.Instance;
string roomDisplay = nm != null ? ("Salle #" + nm.RoomId.Substring(0, Mathf.Min(6, nm.RoomId.Length))) : "—";
GUILayout.Label(roomDisplay, ImGuiSkin.WindowSubtitle);
GUILayout.Space(12); GUILayout.Space(12);
// Player list
ImGuiSkin.DrawSectionHeader("JOUEURS CONNECTÉS"); ImGuiSkin.DrawSectionHeader("JOUEURS CONNECTÉS");
GUILayout.Space(4); GUILayout.Space(4);
var nm = NetworkManager.Instance; if (nm != null)
if (nm != null && nm.IsConnected)
{ {
// We can't directly iterate NetworkState.players from here easily, var s = new GUIStyle(GUI.skin.label) { fontSize = 13 };
// so show basic count s.normal.textColor = new Color(0.75f, 0.75f, 0.85f);
var style = new GUIStyle(GUI.skin.label) { fontSize = 13 }; GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", s);
style.normal.textColor = new Color(0.75f, 0.75f, 0.85f);
GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", style);
} }
GUILayout.Space(16); GUILayout.Space(16);
// Ready button
if (!_isReady) if (!_isReady)
{ {
if (GUILayout.Button("✔ Je suis prêt !", ImGuiSkin.ButtonAccent, GUILayout.Height(44))) if (GUILayout.Button("✔ Je suis prêt !", ImGuiSkin.ButtonAccent, GUILayout.Height(44)))
@@ -281,59 +371,61 @@ public class LobbyUI : MonoBehaviour
} }
else else
{ {
var readyStyle = new GUIStyle(GUI.skin.label) var rs = new GUIStyle(GUI.skin.label)
{ { alignment = TextAnchor.MiddleCenter, fontSize = 16, fontStyle = FontStyle.Bold };
alignment = TextAnchor.MiddleCenter, rs.normal.textColor = new Color(0.3f, 1f, 0.5f);
fontSize = 16, GUILayout.Label("✔ Prêt ! En attente des autres…", rs, GUILayout.Height(44));
fontStyle = FontStyle.Bold,
};
readyStyle.normal.textColor = new Color(0.3f, 1f, 0.5f);
GUILayout.Label("✔ Prêt ! En attente des autres...", readyStyle, GUILayout.Height(44));
} }
GUILayout.Space(8); GUILayout.Space(8);
var hintStyle = new GUIStyle(ImGuiSkin.Hint); GUILayout.Label("La partie démarre quand tout le monde est prêt\nou automatiquement après 30 secondes.", ImGuiSkin.Hint);
hintStyle.normal.textColor = new Color(0.5f, 0.5f, 0.6f);
GUILayout.Label("La partie démarre quand tout le monde est prêt\nou automatiquement après 30 secondes.", hintStyle);
ImGuiSkin.EndWindow(); ImGuiSkin.EndWindow();
} }
// ─── Actions ──────────────────────────────────────────────────────────
private string ValidateName()
{
string n = _playerName.Trim();
if (string.IsNullOrEmpty(n)) { _statusMessage = "Entre un pseudo d'abord."; return null; }
return n;
} }
private void JoinArena() private void DoJoinRoom(string roomId)
{ {
if (NetworkManager.Instance == null) string n = ValidateName(); if (n == null) return;
{
_statusMessage = "Erreur : NetworkManager introuvable";
return;
}
if (string.IsNullOrWhiteSpace(_playerName))
{
_statusMessage = "Entrez un pseudo";
return;
}
_isConnecting = true; _isConnecting = true;
_statusMessage = "Connexion au serveur..."; _statusMessage = "Connexion à la salle…";
NetworkManager.Instance?.JoinByRoomId(roomId, n, PresetColors[_selectedColorIndex]);
Color selectedColor = PresetColors[_selectedColorIndex]; Invoke(nameof(ConnectionTimeout), 10f);
NetworkManager.Instance.JoinArena(_playerName.Trim(), selectedColor);
// Monitor for errors after a delay
Invoke(nameof(CheckConnectionTimeout), 10f);
} }
private void CheckConnectionTimeout() private void DoCreate()
{ {
if (_isConnecting && !NetworkManager.Instance.IsConnected) string n = ValidateName(); if (n == null) return;
_isConnecting = true;
_statusMessage = "Création d'une salle…";
NetworkManager.Instance?.CreateRoom(n, PresetColors[_selectedColorIndex]);
Invoke(nameof(ConnectionTimeout), 10f);
}
private void DoJoinAny()
{ {
string n = ValidateName(); if (n == null) return;
_isConnecting = true;
_statusMessage = "Connexion…";
NetworkManager.Instance?.JoinArena(n, PresetColors[_selectedColorIndex]);
Invoke(nameof(ConnectionTimeout), 10f);
}
private void ConnectionTimeout()
{
if (!_isConnecting) return;
_isConnecting = false; _isConnecting = false;
_statusMessage = "Erreur : Impossible de joindre rolld.io. Réessayez dans quelques instants."; var nm = NetworkManager.Instance;
if (!string.IsNullOrEmpty(NetworkManager.Instance.LastError)) _statusMessage = "Impossible de se connecter. Réessaie.";
{ if (nm != null && !string.IsNullOrEmpty(nm.LastError))
_statusMessage += $"\n{NetworkManager.Instance.LastError}"; _statusMessage += $"\n{nm.LastError}";
}
}
} }
} }

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.Networking;
using Colyseus; using Colyseus;
using Colyseus.Schema; using Colyseus.Schema;
@@ -33,6 +35,24 @@ public class NetworkManager : MonoBehaviour
public string LocalPlayerName { get; private set; } = ""; public string LocalPlayerName { get; private set; } = "";
public Color LocalPlayerColor { get; private set; } = Color.white; 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<RoomInfo> items; }
public event Action<RoomInfo[]> 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<RoomInfo>()); yield break; }
var wrapper = JsonUtility.FromJson<RoomListWrapper>($"{{\"items\":{req.downloadHandler.text}}}");
OnRoomsRefreshed?.Invoke(wrapper?.items?.ToArray() ?? Array.Empty<RoomInfo>());
}
// --- Events --- // --- Events ---
public event Action OnConnected; public event Action OnConnected;
public event Action OnDisconnected; public event Action OnDisconnected;
@@ -100,97 +120,101 @@ public class NetworkManager : MonoBehaviour
// ─── Join / Leave ──────────────────────────────────────────────────── // ─── Join / Leave ────────────────────────────────────────────────────
public async void JoinArena(string playerName, Color color) // ─── Join helpers ─────────────────────────────────────────────────────
{
if (_isJoining || IsConnected)
{
Debug.LogWarning("[Network] Already connecting or connected.");
return;
}
private Dictionary<string, object> 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; _isJoining = true;
ConnectionStatus = "Connexion en cours..."; ConnectionStatus = "Connexion en cours...";
LastError = ""; LastError = "";
LocalPlayerName = playerName; LocalPlayerName = playerName;
LocalPlayerColor = color; LocalPlayerColor = color;
PlayerPrefs.SetString("rolld_player_name", playerName); PlayerPrefs.SetString("rolld_player_name", playerName);
try
{
Debug.Log($"[Network] Connecting to {serverURL}...");
_client = new Client(serverURL); _client = new Client(serverURL);
}
var options = new Dictionary<string, object> private void FinishJoin()
{ {
{ "name", playerName },
{ "colorR", color.r },
{ "colorG", color.g },
{ "colorB", color.b }
};
_room = await _client.JoinOrCreate<NetworkState>("arena", options);
LocalSessionId = _room.SessionId; LocalSessionId = _room.SessionId;
RoomId = _room.RoomId; RoomId = _room.RoomId;
IsConnected = true; IsConnected = true;
ConnectionStatus = "Connecté"; ConnectionStatus = "Connecté";
Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}"); Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}");
_callbacks = Callbacks.Get(_room); _callbacks = Callbacks.Get(_room);
// Players
_callbacks.OnAdd(state => state.players, (key, player) => OnPlayerAdd(key, player)); _callbacks.OnAdd(state => state.players, (key, player) => OnPlayerAdd(key, player));
_callbacks.OnRemove(state => state.players, (key, player) => OnPlayerRemove(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));
// Game state changes _room.OnMessage<EliminatedMsg>("eliminated", msg => { OnEliminated?.Invoke(msg.sessionId, msg.reason); });
_callbacks.Listen(state => state.phase, (newValue, prevValue) => _OnPhaseChanged(newValue)); _room.OnMessage<QualifiedMsg> ("qualified", msg => { OnQualified?.Invoke(msg.sessionId); });
_callbacks.Listen(state => state.countdown, (newValue, prevValue) => OnCountdownChanged?.Invoke(newValue)); _room.OnMessage<RoundStartMsg>("roundStart", msg => { OnRoundStart?.Invoke(msg.round, msg.mode, msg.totalRounds); });
_room.OnMessage<RoundEndMsg> ("roundEnd", msg => { OnRoundEnd?.Invoke(msg.round); });
// Server messages _room.OnMessage<GameEndMsg> ("gameEnd", msg => { OnGameEnd?.Invoke(msg.winner); });
_room.OnMessage<EliminatedMsg>("eliminated", msg => _room.OnMessage<ChatUI.ChatMessage>("chat", msg => { ChatUI.Instance?.ReceiveChatMessage(msg); });
{
Debug.Log($"[Network] Eliminated: {msg.sessionId} ({msg.reason})");
OnEliminated?.Invoke(msg.sessionId, msg.reason);
});
_room.OnMessage<QualifiedMsg>("qualified", msg =>
{
Debug.Log($"[Network] Qualified: {msg.sessionId}");
OnQualified?.Invoke(msg.sessionId);
});
_room.OnMessage<RoundStartMsg>("roundStart", msg =>
{
Debug.Log($"[Network] Round {msg.round} started ({msg.mode})");
OnRoundStart?.Invoke(msg.round, msg.mode, msg.totalRounds);
});
_room.OnMessage<RoundEndMsg>("roundEnd", msg =>
{
Debug.Log($"[Network] Round {msg.round} ended");
OnRoundEnd?.Invoke(msg.round);
});
_room.OnMessage<GameEndMsg>("gameEnd", msg =>
{
Debug.Log($"[Network] Game over — Winner: {msg.winner}");
OnGameEnd?.Invoke(msg.winner);
});
_room.OnMessage<ChatUI.ChatMessage>("chat", msg =>
{
ChatUI.Instance?.ReceiveChatMessage(msg);
});
_room.OnLeave += OnRoomLeave; _room.OnLeave += OnRoomLeave;
OnConnected?.Invoke(); OnConnected?.Invoke();
} }
catch (Exception e)
private void HandleJoinError(Exception e)
{ {
Debug.LogError($"[Network] Failed to join: {e.Message}"); Debug.LogError($"[Network] Failed to join: {e.Message}");
ConnectionStatus = "Erreur de connexion"; ConnectionStatus = "Erreur de connexion";
LastError = e.Message; LastError = e.Message;
IsConnected = false; IsConnected = false;
} }
finally
// ─── Public join methods ──────────────────────────────────────────────
public async void JoinArena(string playerName, Color color)
{ {
_isJoining = false; if (_isJoining || IsConnected) return;
PrepareJoin(playerName, color);
try
{
_room = await _client.JoinOrCreate<NetworkState>("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<NetworkState>(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<NetworkState>("arena", opts);
FinishJoin();
}
catch (Exception e) { HandleJoinError(e); }
finally { _isJoining = false; }
} }
public async void LeaveRoom() public async void LeaveRoom()

View File

@@ -1,5 +1,5 @@
const cors = require('cors'); const cors = require('cors');
const { Server } = require('@colyseus/core'); const { Server, matchMaker } = require('@colyseus/core');
const { WebSocketTransport } = require('@colyseus/ws-transport'); const { WebSocketTransport } = require('@colyseus/ws-transport');
const { ArenaRoom } = require('./rooms/ArenaRoom'); const { ArenaRoom } = require('./rooms/ArenaRoom');
const Stats = require('./stats/StatsManager'); const Stats = require('./stats/StatsManager');
@@ -61,6 +61,21 @@ const gameServer = new Server({
res.json({ ok }); res.json({ ok });
}); });
// ── Rooms ────────────────────────────────────────────────────────────
app.get('/rooms', async (_req, res) => {
try {
const rooms = await matchMaker.query({ name: 'arena' });
res.json(rooms.map(r => ({
roomId: r.roomId,
clients: r.clients,
maxClients: r.maxClients,
metadata: r.metadata || {},
})));
} catch (_) {
res.json([]);
}
});
// ── Chat ───────────────────────────────────────────────────────────── // ── Chat ─────────────────────────────────────────────────────────────
app.get('/chat/history', (req, res) => { app.get('/chat/history', (req, res) => {
res.json(Chat.getHistory(req.query.since)); res.json(Chat.getHistory(req.query.since));

View File

@@ -14,6 +14,7 @@ class ArenaRoom extends Room {
onCreate(options) { onCreate(options) {
this.setState(new GameState()); this.setState(new GameState());
this.setPatchRate(16); // ~62.5 Hz this.setPatchRate(16); // ~62.5 Hz
this.setMetadata({ name: options?.roomName || ('Salle #' + this.roomId.substring(0, 6)) });
this._phaseTimer = null; this._phaseTimer = null;
this._lobbyTimer = null; this._lobbyTimer = null;