feat: free-roam mode + fix multiplayer sync + remote player polish
Backend (ArenaRoom.js):
- Strip race state machine (lobby/countdown/playing/round/qualify). Persistent
"playing" phase, no rounds, no checkpoints. Free-roam multi.
- Spawn lowered to y=1.5 (was 5) + MIN_DIST raised to 5 (was 3) to avoid
ejecting overlapping players at connect.
- Schema kept intact (handshake-safe); deprecated fields default-valued.
- npm run schema:gen wired (anti-drift codegen).
Unity client:
- C# schema generated by schema-codegen into RolldSchema namespace
(Generated/GameState.cs, Generated/Player.cs). NetworkSchema.cs removed —
handshake no longer scans global namespace.
- NetworkManager: typed Room<GameState>, callbacks rebound, seeds players
already in room on join.
- RemotePlayerController:
* Post-spawn 1.5s grace window (BumpReady) — local PlayerController.HandleBump
ignores remotes during grace.
* Solid SphereCollider disabled during grace, re-enabled afterwards — fixes
the kinematic-vs-dynamic eject when a new client spawns inside someone.
* NPCBall prefab material switched from invisible-in-URP Default-Material to
BallShader.shadergraph.
* TrailRenderer added, tinted with player's chosen color.
* Name label distance-scales (1x-8x) so pseudos remain readable far away.
- GameHUD: OnGUI emptied — race UI (rounds, mode, timer, playersAlive) gone.
- GameCanvas.jsx: BUILD_PREFIX/VERSION bumped for cache-bust.
Frontend WebGL build (pretty_build): final build with all the above.
This commit is contained in:
43
game/Assets/Scripts/Network/Generated/GameState.cs
Normal file
43
game/Assets/Scripts/Network/Generated/GameState.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// THIS FILE HAS BEEN GENERATED AUTOMATICALLY
|
||||
// DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING
|
||||
//
|
||||
// GENERATED USING @colyseus/schema 4.0.15
|
||||
//
|
||||
|
||||
using Colyseus.Schema;
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
using UnityEngine.Scripting;
|
||||
#endif
|
||||
|
||||
namespace RolldSchema {
|
||||
public partial class GameState : Schema {
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
[Preserve]
|
||||
#endif
|
||||
public GameState() { }
|
||||
[Type(0, "map", typeof(MapSchema<Player>))]
|
||||
public MapSchema<Player> players = null;
|
||||
|
||||
[Type(1, "string")]
|
||||
public string phase = default(string);
|
||||
|
||||
[Type(2, "float32")]
|
||||
public float countdown = default(float);
|
||||
|
||||
[Type(3, "int8")]
|
||||
public sbyte roundNumber = default(sbyte);
|
||||
|
||||
[Type(4, "int8")]
|
||||
public sbyte totalRounds = default(sbyte);
|
||||
|
||||
[Type(5, "int8")]
|
||||
public sbyte playersAlive = default(sbyte);
|
||||
|
||||
[Type(6, "string")]
|
||||
public string gameMode = default(string);
|
||||
|
||||
[Type(7, "string")]
|
||||
public string winnerName = default(string);
|
||||
}
|
||||
}
|
||||
2
game/Assets/Scripts/Network/Generated/GameState.cs.meta
Normal file
2
game/Assets/Scripts/Network/Generated/GameState.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7c8bd319747bfa4a82569b7dc0458be
|
||||
85
game/Assets/Scripts/Network/Generated/Player.cs
Normal file
85
game/Assets/Scripts/Network/Generated/Player.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// THIS FILE HAS BEEN GENERATED AUTOMATICALLY
|
||||
// DO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING
|
||||
//
|
||||
// GENERATED USING @colyseus/schema 4.0.15
|
||||
//
|
||||
|
||||
using Colyseus.Schema;
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
using UnityEngine.Scripting;
|
||||
#endif
|
||||
|
||||
namespace RolldSchema {
|
||||
public partial class Player : Schema {
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
[Preserve]
|
||||
#endif
|
||||
public Player() { }
|
||||
[Type(0, "float32")]
|
||||
public float x = default(float);
|
||||
|
||||
[Type(1, "float32")]
|
||||
public float y = default(float);
|
||||
|
||||
[Type(2, "float32")]
|
||||
public float z = default(float);
|
||||
|
||||
[Type(3, "float32")]
|
||||
public float vx = default(float);
|
||||
|
||||
[Type(4, "float32")]
|
||||
public float vy = default(float);
|
||||
|
||||
[Type(5, "float32")]
|
||||
public float vz = default(float);
|
||||
|
||||
[Type(6, "float32")]
|
||||
public float rx = default(float);
|
||||
|
||||
[Type(7, "float32")]
|
||||
public float ry = default(float);
|
||||
|
||||
[Type(8, "float32")]
|
||||
public float rz = default(float);
|
||||
|
||||
[Type(9, "float32")]
|
||||
public float rw = default(float);
|
||||
|
||||
[Type(10, "float64")]
|
||||
public double t = default(double);
|
||||
|
||||
[Type(11, "string")]
|
||||
public string name = default(string);
|
||||
|
||||
[Type(12, "float32")]
|
||||
public float colorR = default(float);
|
||||
|
||||
[Type(13, "float32")]
|
||||
public float colorG = default(float);
|
||||
|
||||
[Type(14, "float32")]
|
||||
public float colorB = default(float);
|
||||
|
||||
[Type(15, "float32")]
|
||||
public float avx = default(float);
|
||||
|
||||
[Type(16, "float32")]
|
||||
public float avy = default(float);
|
||||
|
||||
[Type(17, "float32")]
|
||||
public float avz = default(float);
|
||||
|
||||
[Type(18, "boolean")]
|
||||
public bool isEliminated = default(bool);
|
||||
|
||||
[Type(19, "boolean")]
|
||||
public bool isQualified = default(bool);
|
||||
|
||||
[Type(20, "boolean")]
|
||||
public bool isReady = default(bool);
|
||||
|
||||
[Type(21, "int8")]
|
||||
public sbyte checkpointIndex = default(sbyte);
|
||||
}
|
||||
}
|
||||
2
game/Assets/Scripts/Network/Generated/Player.cs.meta
Normal file
2
game/Assets/Scripts/Network/Generated/Player.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d8173f164ec47946a28c13d9638d8cf
|
||||
@@ -5,6 +5,7 @@ using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
using Colyseus;
|
||||
using Colyseus.Schema;
|
||||
using RolldSchema;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton managing the Colyseus connection, room lifecycle, remote player spawning,
|
||||
@@ -70,8 +71,8 @@ public class NetworkManager : MonoBehaviour
|
||||
|
||||
// --- Internals ---
|
||||
private Client _client;
|
||||
private Room<NetworkState> _room;
|
||||
private StateCallbackStrategy<NetworkState> _callbacks;
|
||||
private Room<GameState> _room;
|
||||
private StateCallbackStrategy<GameState> _callbacks;
|
||||
private readonly Dictionary<string, RemotePlayerController> _remotePlayers = new();
|
||||
private float _broadcastTimer;
|
||||
private const float BROADCAST_INTERVAL = 0.01667f; // ~60/sec
|
||||
@@ -111,7 +112,7 @@ public class NetworkManager : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkPlayer GetLocalPlayerState()
|
||||
public Player GetLocalPlayerState()
|
||||
{
|
||||
if (_room == null || _room.State.players == null || string.IsNullOrEmpty(LocalSessionId)) return null;
|
||||
_room.State.players.TryGetValue(LocalSessionId, out var player);
|
||||
@@ -168,7 +169,7 @@ public class NetworkManager : MonoBehaviour
|
||||
if (_room.State.players != null)
|
||||
{
|
||||
foreach (var key in _room.State.players.Keys)
|
||||
OnPlayerAdd((string)key, (NetworkPlayer)_room.State.players[key]);
|
||||
OnPlayerAdd((string)key, (Player)_room.State.players[key]);
|
||||
}
|
||||
|
||||
OnConnected?.Invoke();
|
||||
@@ -190,7 +191,7 @@ public class NetworkManager : MonoBehaviour
|
||||
PrepareJoin(playerName, color);
|
||||
try
|
||||
{
|
||||
_room = await _client.JoinOrCreate<NetworkState>("arena", BuildJoinOptions(playerName, color));
|
||||
_room = await _client.JoinOrCreate<GameState>("arena", BuildJoinOptions(playerName, color));
|
||||
FinishJoin();
|
||||
}
|
||||
catch (Exception e) { HandleJoinError(e); }
|
||||
@@ -203,7 +204,7 @@ public class NetworkManager : MonoBehaviour
|
||||
PrepareJoin(playerName, color);
|
||||
try
|
||||
{
|
||||
_room = await _client.JoinById<NetworkState>(roomId, BuildJoinOptions(playerName, color));
|
||||
_room = await _client.JoinById<GameState>(roomId, BuildJoinOptions(playerName, color));
|
||||
FinishJoin();
|
||||
}
|
||||
catch (Exception e) { HandleJoinError(e); }
|
||||
@@ -218,7 +219,7 @@ public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
var opts = BuildJoinOptions(playerName, color);
|
||||
if (roomName != null) opts["roomName"] = roomName;
|
||||
_room = await _client.Create<NetworkState>("arena", opts);
|
||||
_room = await _client.Create<GameState>("arena", opts);
|
||||
FinishJoin();
|
||||
}
|
||||
catch (Exception e) { HandleJoinError(e); }
|
||||
@@ -259,7 +260,7 @@ public class NetworkManager : MonoBehaviour
|
||||
OnPhaseChanged?.Invoke(phase);
|
||||
}
|
||||
|
||||
private void OnPlayerAdd(string sessionId, NetworkPlayer player)
|
||||
private void OnPlayerAdd(string sessionId, Player player)
|
||||
{
|
||||
Debug.Log($"[Network] Player joined: {sessionId} ({player.name})");
|
||||
PlayerCount = _room.State.players?.Count ?? 0;
|
||||
@@ -288,7 +289,7 @@ public class NetworkManager : MonoBehaviour
|
||||
OnPlayerJoined?.Invoke(sessionId);
|
||||
}
|
||||
|
||||
private void OnPlayerRemove(string sessionId, NetworkPlayer player)
|
||||
private void OnPlayerRemove(string sessionId, Player player)
|
||||
{
|
||||
Debug.Log($"[Network] Player left: {sessionId}");
|
||||
PlayerCount = _room.State.players?.Count ?? 0;
|
||||
@@ -303,7 +304,7 @@ public class NetworkManager : MonoBehaviour
|
||||
OnPlayerLeft?.Invoke(sessionId);
|
||||
}
|
||||
|
||||
private void OnPlayerChange(string sessionId, NetworkPlayer player)
|
||||
private void OnPlayerChange(string sessionId, Player player)
|
||||
{
|
||||
if (sessionId == LocalSessionId) return;
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Generated from @colyseus/schema 4.0.15 — DO NOT EDIT MANUALLY
|
||||
// Class names kept as NetworkPlayer/NetworkState to match existing codebase references.
|
||||
|
||||
using Colyseus.Schema;
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
using UnityEngine.Scripting;
|
||||
#endif
|
||||
|
||||
public partial class NetworkPlayer : Schema
|
||||
{
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
[Preserve]
|
||||
#endif
|
||||
public NetworkPlayer() { }
|
||||
|
||||
[Type(0, "float32")] public float x = 0;
|
||||
[Type(1, "float32")] public float y = 5;
|
||||
[Type(2, "float32")] public float z = 0;
|
||||
[Type(3, "float32")] public float vx = 0;
|
||||
[Type(4, "float32")] public float vy = 0;
|
||||
[Type(5, "float32")] public float vz = 0;
|
||||
[Type(6, "float32")] public float rx = 0;
|
||||
[Type(7, "float32")] public float ry = 0;
|
||||
[Type(8, "float32")] public float rz = 0;
|
||||
[Type(9, "float32")] public float rw = 1;
|
||||
[Type(10, "float64")] public double t = 0;
|
||||
[Type(11, "string")] public string name = "";
|
||||
[Type(12, "float32")] public float colorR = 1;
|
||||
[Type(13, "float32")] public float colorG = 1;
|
||||
[Type(14, "float32")] public float colorB = 1;
|
||||
[Type(15, "float32")] public float avx = 0;
|
||||
[Type(16, "float32")] public float avy = 0;
|
||||
[Type(17, "float32")] public float avz = 0;
|
||||
[Type(18, "boolean")] public bool isEliminated = false;
|
||||
[Type(19, "boolean")] public bool isQualified = false;
|
||||
[Type(20, "boolean")] public bool isReady = false;
|
||||
[Type(21, "int8")] public sbyte checkpointIndex = 0;
|
||||
}
|
||||
|
||||
public partial class NetworkState : Schema
|
||||
{
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
[Preserve]
|
||||
#endif
|
||||
public NetworkState() { }
|
||||
|
||||
[Type(0, "map", typeof(MapSchema<NetworkPlayer>))]
|
||||
public MapSchema<NetworkPlayer> players = null;
|
||||
|
||||
[Type(1, "string")] public string phase = "lobby";
|
||||
[Type(2, "float32")] public float countdown = 0;
|
||||
[Type(3, "int8")] public sbyte roundNumber = 1;
|
||||
[Type(4, "int8")] public sbyte totalRounds = 3;
|
||||
[Type(5, "int8")] public sbyte playersAlive = 0;
|
||||
[Type(6, "string")] public string gameMode = "race";
|
||||
[Type(7, "string")] public string winnerName = "";
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ce16348bc0580b49860d9bd80e7bec0
|
||||
@@ -24,10 +24,16 @@ public class RemotePlayerController : MonoBehaviour
|
||||
[Tooltip("Rotation slerp speed")]
|
||||
public float rotationSpeed = 24f;
|
||||
|
||||
[Header("Spawn")]
|
||||
[Tooltip("Seconds after spawn during which this remote ignores local bump interactions (avoids upward eject if balls overlap at spawn).")]
|
||||
public float spawnBumpGrace = 1.5f;
|
||||
|
||||
// Public info
|
||||
public string SessionId { get; private set; }
|
||||
public string PlayerName { get; private set; }
|
||||
public Color PlayerColor { get; private set; }
|
||||
public float SpawnTime { get; private set; }
|
||||
public bool BumpReady => Time.time - SpawnTime > spawnBumpGrace;
|
||||
|
||||
// --- Snapshot buffer ---
|
||||
private struct Snapshot
|
||||
@@ -61,6 +67,7 @@ public class RemotePlayerController : MonoBehaviour
|
||||
SessionId = sessionId;
|
||||
PlayerName = playerName;
|
||||
PlayerColor = color;
|
||||
SpawnTime = Time.time;
|
||||
_currentRotation = transform.rotation;
|
||||
_bufferCount = 0;
|
||||
_initialized = true;
|
||||
@@ -99,12 +106,17 @@ public class RemotePlayerController : MonoBehaviour
|
||||
|
||||
// Add a trigger collider slightly larger than the physics collider
|
||||
// so the local player can detect bumps
|
||||
var existingCollider = GetComponent<SphereCollider>();
|
||||
float baseRadius = existingCollider != null ? existingCollider.radius : 0.5f;
|
||||
_solidCollider = GetComponent<SphereCollider>();
|
||||
float baseRadius = _solidCollider != null ? _solidCollider.radius : 0.5f;
|
||||
var trigger = gameObject.AddComponent<SphereCollider>();
|
||||
trigger.isTrigger = true;
|
||||
trigger.radius = baseRadius * 1.15f; // 15% larger
|
||||
|
||||
// During the spawn grace window, disable the SOLID collider so the kinematic
|
||||
// remote can't physically eject an overlapping local player at spawn.
|
||||
// (The trigger stays — bump detection is gated by BumpReady in PlayerController.)
|
||||
if (_solidCollider != null) _solidCollider.enabled = false;
|
||||
|
||||
// Disable any player input on remote balls
|
||||
var playerInput = GetComponent<UnityEngine.InputSystem.PlayerInput>();
|
||||
if (playerInput != null)
|
||||
@@ -114,12 +126,16 @@ public class RemotePlayerController : MonoBehaviour
|
||||
if (playerController != null)
|
||||
playerController.enabled = false;
|
||||
|
||||
// Create floating name label
|
||||
// Create floating name label + speed trail
|
||||
CreateNameLabel();
|
||||
CreateTrail(color);
|
||||
|
||||
Debug.Log($"[RemotePlayer] Initialized: {playerName} ({sessionId[..6]}) color={color}");
|
||||
}
|
||||
|
||||
private SphereCollider _solidCollider;
|
||||
private bool _solidReenabled;
|
||||
|
||||
/// <summary>
|
||||
/// Called by NetworkManager when a state update arrives from the server.
|
||||
/// Pushes a new snapshot into the interpolation buffer.
|
||||
@@ -149,7 +165,16 @@ public class RemotePlayerController : MonoBehaviour
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!_initialized || _bufferCount == 0) return;
|
||||
if (!_initialized) return;
|
||||
|
||||
// Re-enable the solid collider once the grace window has elapsed.
|
||||
if (!_solidReenabled && BumpReady && _solidCollider != null)
|
||||
{
|
||||
_solidCollider.enabled = true;
|
||||
_solidReenabled = true;
|
||||
}
|
||||
|
||||
if (_bufferCount == 0) return;
|
||||
|
||||
// Render time = current time minus interpolation delay
|
||||
float renderTime = Time.time - interpolationDelay;
|
||||
@@ -248,15 +273,37 @@ public class RemotePlayerController : MonoBehaviour
|
||||
if (cam != null)
|
||||
{
|
||||
Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position;
|
||||
float camDist = lookDir.magnitude;
|
||||
lookDir.y = 0f;
|
||||
if (lookDir.sqrMagnitude > 0.001f)
|
||||
_nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir);
|
||||
|
||||
// Distance-based scale: keeps the pseudo readable when players are far apart.
|
||||
// Below 8 m → base size; above → grows linearly, capped at 8× to avoid screen takeover.
|
||||
float scaleFactor = Mathf.Clamp(camDist / 8f, 1f, 8f);
|
||||
_nameLabelObj.transform.localScale = Vector3.one * (0.1f * scaleFactor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject _nameLabelObj; // Keep reference for billboard update
|
||||
|
||||
private void CreateTrail(Color playerColor)
|
||||
{
|
||||
var trail = GetComponent<TrailRenderer>() ?? gameObject.AddComponent<TrailRenderer>();
|
||||
trail.time = 0.4f;
|
||||
trail.startWidth = 0.3f;
|
||||
trail.endWidth = 0.02f;
|
||||
trail.minVertexDistance = 0.1f;
|
||||
trail.autodestruct = false;
|
||||
trail.emitting = true;
|
||||
trail.material = new Material(Shader.Find("Sprites/Default"));
|
||||
// Use the player's chosen color so each remote has a visually distinct trail
|
||||
// (lobby presets avoid orange, so it never clashes with the local player's orange trail).
|
||||
trail.startColor = new Color(playerColor.r, playerColor.g, playerColor.b, 0.7f);
|
||||
trail.endColor = new Color(playerColor.r, playerColor.g, playerColor.b, 0f);
|
||||
}
|
||||
|
||||
private void CreateNameLabel()
|
||||
{
|
||||
GameObject labelObj = new GameObject("NameLabel");
|
||||
|
||||
@@ -101,108 +101,7 @@ public class GameHUD : MonoBehaviour
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (_phase == "lobby" && !_localRaceActive) return;
|
||||
|
||||
ImGuiSkin.EnsureReady();
|
||||
var nm = NetworkManager.Instance;
|
||||
|
||||
// ── Countdown (center, large) ─────────────────────────────────────
|
||||
if (_phase == "countdown" && _countdown > 0f)
|
||||
{
|
||||
float scale = 1f + _countdownPulse * 0.4f;
|
||||
float fontSize = 96f * scale;
|
||||
var countStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = Mathf.RoundToInt(fontSize),
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
countStyle.normal.textColor = new Color(1f, 0.85f, 0.1f, 1f);
|
||||
GUI.Label(new Rect(0, Screen.height * 0.3f, Screen.width, 120f),
|
||||
Mathf.CeilToInt(_countdown).ToString(), countStyle);
|
||||
|
||||
// "Préparez-vous !" label below
|
||||
var subStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 22,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
subStyle.normal.textColor = new Color(1f, 1f, 1f, 0.8f);
|
||||
string modeLabel = _gameMode switch {
|
||||
"race" => "COURSE",
|
||||
"survival" => "SURVIVAL",
|
||||
"teams" => "ÉQUIPES",
|
||||
_ => _gameMode.ToUpper()
|
||||
};
|
||||
GUI.Label(new Rect(0, Screen.height * 0.3f + 110f, Screen.width, 36f),
|
||||
$"— {modeLabel} —", subStyle);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Top-left: Round & Mode ─────────────────────────────────────────
|
||||
float panelX = 12f;
|
||||
float panelY = 12f;
|
||||
float panelW = 220f;
|
||||
float panelH = 70f;
|
||||
|
||||
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
||||
GUI.DrawTexture(new Rect(panelX, panelY, panelW, panelH), _bgTex);
|
||||
GUI.color = Color.white;
|
||||
|
||||
var roundStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleLeft,
|
||||
fontSize = 14,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
roundStyle.normal.textColor = new Color(1f, 0.85f, 0.1f);
|
||||
GUI.Label(new Rect(panelX + 8f, panelY + 4f, panelW - 16f, 28f),
|
||||
$"ROUND {_roundNumber} / {_totalRounds}", roundStyle);
|
||||
|
||||
var modeStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleLeft, fontSize = 12 };
|
||||
modeStyle.normal.textColor = new Color(0.7f, 0.7f, 0.85f);
|
||||
string modeFull = _gameMode switch {
|
||||
"race" => "COURSE", "survival" => "SURVIVAL", "teams" => "ÉQUIPES", _ => _gameMode.ToUpper()
|
||||
};
|
||||
GUI.Label(new Rect(panelX + 8f, panelY + 32f, panelW - 16f, 24f), modeFull, modeStyle);
|
||||
|
||||
// ── Top-right: Players alive ──────────────────────────────────────
|
||||
float prX = Screen.width - 180f;
|
||||
GUI.color = new Color(0.08f, 0.08f, 0.12f, 0.85f);
|
||||
GUI.DrawTexture(new Rect(prX, panelY, 168f, panelH), _bgTex);
|
||||
GUI.color = Color.white;
|
||||
|
||||
var aliveStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 28,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
aliveStyle.normal.textColor = new Color(0.3f, 1f, 0.5f);
|
||||
GUI.Label(new Rect(prX, panelY + 2f, 168f, 40f), $"{_cachedPlayersAlive}", aliveStyle);
|
||||
|
||||
var aliveLabel = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11 };
|
||||
aliveLabel.normal.textColor = new Color(0.6f, 0.6f, 0.7f);
|
||||
GUI.Label(new Rect(prX, panelY + 40f, 168f, 22f), "joueurs en jeu", aliveLabel);
|
||||
|
||||
// ── Round timer (top center) ──────────────────────────────────────
|
||||
float displayTimer = _timerRunning ? _roundTimer : (_localRaceActive ? _localRaceTimer : -1f);
|
||||
if (displayTimer >= 0f)
|
||||
{
|
||||
int mins = Mathf.FloorToInt(displayTimer / 60f);
|
||||
int secs = Mathf.FloorToInt(displayTimer % 60f);
|
||||
var timerStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = 18,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
timerStyle.normal.textColor = new Color(0.85f, 0.85f, 0.9f, 0.9f);
|
||||
GUI.Label(new Rect(Screen.width * 0.5f - 60f, panelY, 120f, 40f),
|
||||
$"{mins:00}:{secs:00}", timerStyle);
|
||||
}
|
||||
|
||||
// Race UI disabled — free-roam mode has no rounds/countdown/timer.
|
||||
}
|
||||
|
||||
// Static accessors for cross-script use
|
||||
|
||||
Reference in New Issue
Block a user