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;
|
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 ImGui–style 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}";
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user