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:
@@ -1,38 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Lobby UI displayed at scene start. Player enters a name, picks a color,
|
||||
/// and clicks "Rejoindre" to connect to the arena.
|
||||
/// Manages the full pre-game → in-game transition:
|
||||
/// - 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 ImGui–style skin via ImGuiSkin.
|
||||
/// Lobby UI: character setup + room list side by side.
|
||||
/// - T to open/close chat, Tab for keybinds (handled elsewhere)
|
||||
/// - Lists available rooms, lets the player create or join one
|
||||
/// </summary>
|
||||
public class LobbyUI : MonoBehaviour
|
||||
{
|
||||
[Header("Scene References")]
|
||||
[Tooltip("The root 'Player' GameObject (contains PlayerSphere + cameras). Will be deactivated until connected.")]
|
||||
public GameObject playerRoot;
|
||||
|
||||
[Tooltip("The spectator camera GameObject (SpectatorCamera component).")]
|
||||
public SpectatorCamera spectatorCamera;
|
||||
|
||||
// Preset colors for selection
|
||||
private static readonly Color[] PresetColors = new Color[]
|
||||
private static readonly Color[] PresetColors =
|
||||
{
|
||||
new Color(1f, 0.35f, 0.2f), // Orange-red
|
||||
new Color(0.2f, 0.6f, 1f), // Blue
|
||||
new Color(0.3f, 1f, 0.4f), // Green
|
||||
new Color(1f, 0.85f, 0.1f), // Yellow
|
||||
new Color(0.8f, 0.3f, 1f), // Purple
|
||||
new Color(1f, 0.5f, 0.7f), // Pink
|
||||
};
|
||||
|
||||
private static readonly string[] ColorNames = new string[]
|
||||
{
|
||||
"Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose"
|
||||
new Color(1f, 0.35f, 0.2f),
|
||||
new Color(0.2f, 0.6f, 1f),
|
||||
new Color(0.3f, 1f, 0.4f),
|
||||
new Color(1f, 0.85f, 0.1f),
|
||||
new Color(0.8f, 0.3f, 1f),
|
||||
new Color(1f, 0.5f, 0.7f),
|
||||
};
|
||||
private static readonly string[] ColorNames = { "Rouge", "Bleu", "Vert", "Jaune", "Violet", "Rose" };
|
||||
|
||||
// UI state
|
||||
private bool _lobbyActive = true;
|
||||
@@ -42,66 +31,96 @@ public class LobbyUI : MonoBehaviour
|
||||
private bool _isConnecting = 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 int _lastPreviewColorIndex = -1;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Generate a default name
|
||||
_playerName = "Joueur" + Random.Range(100, 999);
|
||||
_playerName = PlayerPrefs.GetString("rolld_player_name", "Joueur" + Random.Range(100, 999));
|
||||
|
||||
// --- Hide the player hierarchy until connected ---
|
||||
if (playerRoot != null)
|
||||
playerRoot.SetActive(false);
|
||||
|
||||
// --- Activate spectator camera ---
|
||||
if (spectatorCamera != null)
|
||||
{
|
||||
// Wire the gameplay camera reference so spectator knows what to re-enable
|
||||
var gameplayCam = playerRoot?.GetComponentInChildren<Camera>(true);
|
||||
if (gameplayCam != null)
|
||||
spectatorCamera.gameplayCamera = gameplayCam;
|
||||
|
||||
spectatorCamera.Activate();
|
||||
}
|
||||
|
||||
// Subscribe to network events
|
||||
if (NetworkManager.Instance != null)
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null)
|
||||
{
|
||||
NetworkManager.Instance.OnConnected += OnConnected;
|
||||
NetworkManager.Instance.OnDisconnected += OnDisconnected;
|
||||
nm.OnConnected += OnConnected;
|
||||
nm.OnDisconnected += OnDisconnected;
|
||||
nm.OnRoomsRefreshed += OnRoomsRefreshed;
|
||||
}
|
||||
|
||||
RefreshRooms();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (NetworkManager.Instance != null)
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null)
|
||||
{
|
||||
NetworkManager.Instance.OnConnected -= OnConnected;
|
||||
NetworkManager.Instance.OnDisconnected -= OnDisconnected;
|
||||
nm.OnConnected -= OnConnected;
|
||||
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()
|
||||
{
|
||||
_lobbyActive = false;
|
||||
_isConnecting = false;
|
||||
_statusMessage = "";
|
||||
CancelInvoke(nameof(CheckConnectionTimeout));
|
||||
CancelInvoke(nameof(ConnectionTimeout));
|
||||
|
||||
// --- Activate the player hierarchy ---
|
||||
if (playerRoot != null)
|
||||
playerRoot.SetActive(true);
|
||||
|
||||
// Teleport player ball to the server-assigned spawn position
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null && playerRoot != null)
|
||||
{
|
||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||
if (pc != null)
|
||||
{
|
||||
// Get spawn pos from the local player's state in the room
|
||||
var localState = nm.GetLocalPlayerState();
|
||||
if (localState != null)
|
||||
{
|
||||
@@ -115,20 +134,15 @@ public class LobbyUI : MonoBehaviour
|
||||
}
|
||||
pc.transform.position = spawnPos;
|
||||
pc.SetSpawnPosition(spawnPos);
|
||||
Debug.Log($"[Lobby] Player teleported to spawn: {spawnPos}");
|
||||
}
|
||||
pc.enabled = true;
|
||||
|
||||
// Setup local player visuals: 50% color tint + floating name label
|
||||
pc.SetupLocalPlayer(nm.LocalPlayerName, nm.LocalPlayerColor);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Switch from spectator to gameplay camera ---
|
||||
if (spectatorCamera != null)
|
||||
spectatorCamera.Deactivate();
|
||||
|
||||
// Unlock cursor for gameplay
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
@@ -139,12 +153,11 @@ public class LobbyUI : MonoBehaviour
|
||||
_isConnecting = false;
|
||||
_isReady = false;
|
||||
_statusMessage = "Déconnecté du serveur";
|
||||
_refreshTimer = REFRESH_INTERVAL; // force immediate refresh
|
||||
|
||||
// Show cursor for lobby
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
|
||||
// --- Deactivate the player hierarchy ---
|
||||
if (playerRoot != null)
|
||||
{
|
||||
var pc = playerRoot.GetComponentInChildren<PlayerController>(true);
|
||||
@@ -152,44 +165,55 @@ public class LobbyUI : MonoBehaviour
|
||||
playerRoot.SetActive(false);
|
||||
}
|
||||
|
||||
// --- Re-enable spectator camera ---
|
||||
if (spectatorCamera != null)
|
||||
spectatorCamera.Activate();
|
||||
}
|
||||
|
||||
// ─── OnGUI ────────────────────────────────────────────────────────────
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (!_lobbyActive) return;
|
||||
|
||||
ImGuiSkin.EnsureReady();
|
||||
|
||||
if (Cursor.lockState != CursorLockMode.None)
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
}
|
||||
|
||||
ImGuiSkin.DrawOverlay();
|
||||
|
||||
bool isConnected = NetworkManager.Instance != null && NetworkManager.Instance.IsConnected;
|
||||
|
||||
if (!isConnected)
|
||||
DrawSetupAndRoomList();
|
||||
else
|
||||
DrawWaitingRoom();
|
||||
}
|
||||
|
||||
// ─── Setup + room list ────────────────────────────────────────────────
|
||||
|
||||
private void DrawSetupAndRoomList()
|
||||
{
|
||||
// ── Pre-connect panel ────────────────────────────────────────
|
||||
float panelWidth = 420;
|
||||
float panelHeight = 440;
|
||||
ImGuiSkin.BeginWindow(panelWidth, panelHeight, "ROLL'D");
|
||||
const float W = 620f, H = 520f;
|
||||
float x = (Screen.width - W) * 0.5f;
|
||||
float y = (Screen.height - H) * 0.5f;
|
||||
|
||||
GUILayout.Label("Rejoindre l'arène multijoueur", ImGuiSkin.WindowSubtitle);
|
||||
GUILayout.Space(16);
|
||||
ImGuiSkin.BeginWindowAt(x, y, W, H, "ROLL'D");
|
||||
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);
|
||||
_playerName = GUILayout.TextField(_playerName, 16, ImGuiSkin.TextField, GUILayout.Height(30));
|
||||
GUILayout.Space(12);
|
||||
GUILayout.Space(10);
|
||||
|
||||
ImGuiSkin.DrawSectionHeader("COULEUR");
|
||||
GUILayout.Space(6);
|
||||
GUILayout.Space(4);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
for (int i = 0; i < PresetColors.Length; i++)
|
||||
@@ -197,20 +221,19 @@ public class LobbyUI : MonoBehaviour
|
||||
Color c = PresetColors[i];
|
||||
bool selected = _selectedColorIndex == i;
|
||||
Color prevBg = GUI.backgroundColor;
|
||||
GUI.backgroundColor = selected ? c : c * 0.7f;
|
||||
GUIStyle btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall)
|
||||
{
|
||||
fontStyle = selected ? FontStyle.Bold : FontStyle.Normal,
|
||||
};
|
||||
GUI.backgroundColor = selected ? c : c * 0.6f;
|
||||
var btnStyle = new GUIStyle(ImGuiSkin.ButtonSmall)
|
||||
{ fontStyle = selected ? FontStyle.Bold : FontStyle.Normal };
|
||||
if (selected) btnStyle.normal.textColor = Color.white;
|
||||
string label = selected ? $"▸ {ColorNames[i]}" : ColorNames[i];
|
||||
if (GUILayout.Button(label, btnStyle, GUILayout.Height(32), GUILayout.Width(60)))
|
||||
if (GUILayout.Button(selected ? $"▸{ColorNames[i][0]}" : $"{ColorNames[i][0]}",
|
||||
btnStyle, GUILayout.Height(30), GUILayout.Width(34)))
|
||||
_selectedColorIndex = i;
|
||||
GUI.backgroundColor = prevBg;
|
||||
}
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.Space(4);
|
||||
|
||||
GUILayout.Space(4);
|
||||
// Color swatch
|
||||
if (_colorPreviewTex == null || _lastPreviewColorIndex != _selectedColorIndex)
|
||||
{
|
||||
if (_colorPreviewTex == null)
|
||||
@@ -222,55 +245,122 @@ public class LobbyUI : MonoBehaviour
|
||||
_colorPreviewTex.Apply();
|
||||
_lastPreviewColorIndex = _selectedColorIndex;
|
||||
}
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.Box(_colorPreviewTex, GUIStyle.none, GUILayout.Width(80), GUILayout.Height(16));
|
||||
GUILayout.FlexibleSpace();
|
||||
GUILayout.EndHorizontal();
|
||||
GUILayout.Space(16);
|
||||
var swatchStyle = new GUIStyle(ImGuiSkin.LabelDim) { alignment = TextAnchor.MiddleLeft, fontSize = 11 };
|
||||
GUILayout.Label($"▌ {ColorNames[_selectedColorIndex]}", swatchStyle);
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// Create room button
|
||||
GUI.enabled = !_isConnecting && !string.IsNullOrWhiteSpace(_playerName);
|
||||
string buttonText = _isConnecting ? "Connexion..." : "▶ Rejoindre l'arène";
|
||||
if (GUILayout.Button(buttonText, ImGuiSkin.ButtonAccent, GUILayout.Height(44)))
|
||||
JoinArena();
|
||||
GUI.enabled = true;
|
||||
GUILayout.Space(8);
|
||||
if (GUILayout.Button("+ Créer une salle", ImGuiSkin.Button, GUILayout.Height(36)))
|
||||
DoCreate();
|
||||
|
||||
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))
|
||||
{
|
||||
bool isError = _statusMessage.Contains("Erreur") || _statusMessage.Contains("Déconnecté");
|
||||
GUIStyle statusStyle = isError ? ImGuiSkin.StatusRed : new GUIStyle(ImGuiSkin.Hint);
|
||||
if (!isError) statusStyle.normal.textColor = ImGuiSkin.ColYellow;
|
||||
GUILayout.Label(_statusMessage, statusStyle);
|
||||
GUILayout.Label(_statusMessage, isError ? ImGuiSkin.StatusRed : ImGuiSkin.Hint);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Player list
|
||||
ImGuiSkin.DrawSectionHeader("JOUEURS CONNECTÉS");
|
||||
GUILayout.Space(4);
|
||||
var nm = NetworkManager.Instance;
|
||||
if (nm != null && nm.IsConnected)
|
||||
if (nm != null)
|
||||
{
|
||||
// We can't directly iterate NetworkState.players from here easily,
|
||||
// so show basic count
|
||||
var style = new GUIStyle(GUI.skin.label) { fontSize = 13 };
|
||||
style.normal.textColor = new Color(0.75f, 0.75f, 0.85f);
|
||||
GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", style);
|
||||
var s = new GUIStyle(GUI.skin.label) { fontSize = 13 };
|
||||
s.normal.textColor = new Color(0.75f, 0.75f, 0.85f);
|
||||
GUILayout.Label($" {nm.PlayerCount} joueur(s) dans la salle", s);
|
||||
}
|
||||
GUILayout.Space(16);
|
||||
|
||||
// Ready button
|
||||
if (!_isReady)
|
||||
{
|
||||
if (GUILayout.Button("✔ Je suis prêt !", ImGuiSkin.ButtonAccent, GUILayout.Height(44)))
|
||||
@@ -281,59 +371,61 @@ public class LobbyUI : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
var readyStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 16,
|
||||
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));
|
||||
var rs = new GUIStyle(GUI.skin.label)
|
||||
{ alignment = TextAnchor.MiddleCenter, fontSize = 16, fontStyle = FontStyle.Bold };
|
||||
rs.normal.textColor = new Color(0.3f, 1f, 0.5f);
|
||||
GUILayout.Label("✔ Prêt ! En attente des autres…", rs, GUILayout.Height(44));
|
||||
}
|
||||
|
||||
GUILayout.Space(8);
|
||||
var hintStyle = new GUIStyle(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);
|
||||
GUILayout.Label("La partie démarre quand tout le monde est prêt\nou automatiquement après 30 secondes.", ImGuiSkin.Hint);
|
||||
|
||||
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)
|
||||
{
|
||||
_statusMessage = "Erreur : NetworkManager introuvable";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_playerName))
|
||||
{
|
||||
_statusMessage = "Entrez un pseudo";
|
||||
return;
|
||||
}
|
||||
|
||||
string n = ValidateName(); if (n == null) return;
|
||||
_isConnecting = true;
|
||||
_statusMessage = "Connexion au serveur...";
|
||||
|
||||
Color selectedColor = PresetColors[_selectedColorIndex];
|
||||
NetworkManager.Instance.JoinArena(_playerName.Trim(), selectedColor);
|
||||
|
||||
// Monitor for errors after a delay
|
||||
Invoke(nameof(CheckConnectionTimeout), 10f);
|
||||
_statusMessage = "Connexion à la salle…";
|
||||
NetworkManager.Instance?.JoinByRoomId(roomId, n, PresetColors[_selectedColorIndex]);
|
||||
Invoke(nameof(ConnectionTimeout), 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;
|
||||
_statusMessage = "Erreur : Impossible de joindre rolld.io. Réessayez dans quelques instants.";
|
||||
if (!string.IsNullOrEmpty(NetworkManager.Instance.LastError))
|
||||
{
|
||||
_statusMessage += $"\n{NetworkManager.Instance.LastError}";
|
||||
}
|
||||
}
|
||||
var nm = NetworkManager.Instance;
|
||||
_statusMessage = "Impossible de se connecter. Réessaie.";
|
||||
if (nm != null && !string.IsNullOrEmpty(nm.LastError))
|
||||
_statusMessage += $"\n{nm.LastError}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using Colyseus;
|
||||
using Colyseus.Schema;
|
||||
|
||||
@@ -33,6 +35,24 @@ public class NetworkManager : MonoBehaviour
|
||||
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<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 ---
|
||||
public event Action OnConnected;
|
||||
public event Action OnDisconnected;
|
||||
@@ -100,97 +120,101 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
// ─── Join / Leave ────────────────────────────────────────────────────
|
||||
|
||||
public async void JoinArena(string playerName, Color color)
|
||||
{
|
||||
if (_isJoining || IsConnected)
|
||||
{
|
||||
Debug.LogWarning("[Network] Already connecting or connected.");
|
||||
return;
|
||||
}
|
||||
// ─── Join helpers ─────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
ConnectionStatus = "Connexion en cours...";
|
||||
LastError = "";
|
||||
LocalPlayerName = playerName;
|
||||
LocalPlayerColor = color;
|
||||
PlayerPrefs.SetString("rolld_player_name", playerName);
|
||||
|
||||
try
|
||||
{
|
||||
Debug.Log($"[Network] Connecting to {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;
|
||||
RoomId = _room.RoomId;
|
||||
IsConnected = true;
|
||||
ConnectionStatus = "Connecté";
|
||||
|
||||
Debug.Log($"[Network] Joined room {RoomId} as {LocalSessionId}");
|
||||
|
||||
_callbacks = Callbacks.Get(_room);
|
||||
|
||||
// Players
|
||||
_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));
|
||||
|
||||
// Game state changes
|
||||
_callbacks.Listen(state => state.phase, (newValue, prevValue) => _OnPhaseChanged(newValue));
|
||||
_callbacks.Listen(state => state.countdown, (newValue, prevValue) => OnCountdownChanged?.Invoke(newValue));
|
||||
|
||||
// Server messages
|
||||
_room.OnMessage<EliminatedMsg>("eliminated", 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.OnMessage<EliminatedMsg>("eliminated", msg => { OnEliminated?.Invoke(msg.sessionId, msg.reason); });
|
||||
_room.OnMessage<QualifiedMsg> ("qualified", msg => { OnQualified?.Invoke(msg.sessionId); });
|
||||
_room.OnMessage<RoundStartMsg>("roundStart", msg => { OnRoundStart?.Invoke(msg.round, msg.mode, msg.totalRounds); });
|
||||
_room.OnMessage<RoundEndMsg> ("roundEnd", msg => { OnRoundEnd?.Invoke(msg.round); });
|
||||
_room.OnMessage<GameEndMsg> ("gameEnd", msg => { OnGameEnd?.Invoke(msg.winner); });
|
||||
_room.OnMessage<ChatUI.ChatMessage>("chat", msg => { ChatUI.Instance?.ReceiveChatMessage(msg); });
|
||||
_room.OnLeave += OnRoomLeave;
|
||||
|
||||
OnConnected?.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
private void HandleJoinError(Exception e)
|
||||
{
|
||||
Debug.LogError($"[Network] Failed to join: {e.Message}");
|
||||
ConnectionStatus = "Erreur de connexion";
|
||||
LastError = e.Message;
|
||||
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()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const cors = require('cors');
|
||||
const { Server } = require('@colyseus/core');
|
||||
const { Server, matchMaker } = require('@colyseus/core');
|
||||
const { WebSocketTransport } = require('@colyseus/ws-transport');
|
||||
const { ArenaRoom } = require('./rooms/ArenaRoom');
|
||||
const Stats = require('./stats/StatsManager');
|
||||
@@ -61,6 +61,21 @@ const gameServer = new Server({
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
app.get('/chat/history', (req, res) => {
|
||||
res.json(Chat.getHistory(req.query.since));
|
||||
|
||||
@@ -14,6 +14,7 @@ class ArenaRoom extends Room {
|
||||
onCreate(options) {
|
||||
this.setState(new GameState());
|
||||
this.setPatchRate(16); // ~62.5 Hz
|
||||
this.setMetadata({ name: options?.roomName || ('Salle #' + this.roomId.substring(0, 6)) });
|
||||
|
||||
this._phaseTimer = null;
|
||||
this._lobbyTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user