Reorganize scripts into subfolders by responsibility
Move scripts under Assets/Scripts/ into Components, GameMode, Helper, Network, Enum, Struct and ScriptableObject folders. Pure file moves (plus folder .meta files); no code changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
382bfb36df
commit
e23fae0c50
126 files changed
+112
No files matched your search
@@ -0,0 +1,93 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder on-screen match HUD: the current sub-phase + countdown (top centre) and a hunter crosshair
|
||||
/// during the seek phase. Drawn with IMGUI (OnGUI) on purpose - it needs no Canvas / font asset authoring,
|
||||
/// so the match-state logic can be exercised immediately; swap it for a proper uGUI canvas once the look is
|
||||
/// finalised. Added at runtime by <see cref="MatchManager"/> (AddComponent), so it rides the persistent
|
||||
/// manager object and reads <see cref="MatchManager.Instance"/> on every machine.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class MatchHud : MonoBehaviour
|
||||
{
|
||||
private GUIStyle _phaseStyle;
|
||||
private GUIStyle _timeStyle;
|
||||
private int _lastSecond = -1; // countdown text cache: OnGUI runs 2+ times per frame,
|
||||
private string _timeText = ""; // so the string is only rebuilt when the displayed second changes
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
var match = MatchManager.Instance;
|
||||
if (!match || !match.MatchActive) return;
|
||||
|
||||
EnsureStyles();
|
||||
|
||||
int s = Mathf.CeilToInt(Mathf.Max(0f, match.LocalPhaseRemaining));
|
||||
if (s != _lastSecond)
|
||||
{
|
||||
_lastSecond = s;
|
||||
_timeText = $"{s / 60:0}:{s % 60:00}";
|
||||
}
|
||||
|
||||
const float w = 360f;
|
||||
var phaseRect = new Rect((Screen.width - w) * 0.5f, 12f, w, 30f);
|
||||
var timeRect = new Rect(phaseRect.x, phaseRect.y + 28f, w, 36f);
|
||||
DrawShadowed(phaseRect, PhaseLabel(match.LocalSubPhase), _phaseStyle);
|
||||
DrawShadowed(timeRect, _timeText, _timeStyle);
|
||||
|
||||
if (match.LocalSubPhase == MatchManager.MatchSubPhase.Seek && NetworkPlayerSetup.LocalViewerIsHunter())
|
||||
DrawCrosshair();
|
||||
}
|
||||
|
||||
private static string PhaseLabel(MatchManager.MatchSubPhase phase)
|
||||
{
|
||||
switch (phase)
|
||||
{
|
||||
case MatchManager.MatchSubPhase.Hide: return "Verstecken";
|
||||
case MatchManager.MatchSubPhase.Seek: return "Suchen";
|
||||
case MatchManager.MatchSubPhase.Resolution: return "Auflösung";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawCrosshair()
|
||||
{
|
||||
float cx = Screen.width * 0.5f;
|
||||
float cy = Screen.height * 0.5f;
|
||||
const float r = 8f, t = 2f;
|
||||
Color prev = GUI.color;
|
||||
GUI.color = new Color(1f, 1f, 1f, 0.85f);
|
||||
GUI.DrawTexture(new Rect(cx - r, cy - t * 0.5f, r * 2f, t), Texture2D.whiteTexture);
|
||||
GUI.DrawTexture(new Rect(cx - t * 0.5f, cy - r, t, r * 2f), Texture2D.whiteTexture);
|
||||
GUI.color = prev;
|
||||
}
|
||||
|
||||
// One label drawn twice (dark offset behind, then the real colour) so it stays readable on any backdrop.
|
||||
private static void DrawShadowed(Rect r, string text, GUIStyle style)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
Color real = style.normal.textColor;
|
||||
style.normal.textColor = new Color(0f, 0f, 0f, 0.7f);
|
||||
GUI.Label(new Rect(r.x + 1f, r.y + 1f, r.width, r.height), text, style);
|
||||
style.normal.textColor = real;
|
||||
GUI.Label(r, text, style);
|
||||
}
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (_phaseStyle == null) _phaseStyle = MakeCenteredBoldLabel(22);
|
||||
if (_timeStyle == null) _timeStyle = MakeCenteredBoldLabel(28);
|
||||
}
|
||||
|
||||
private static GUIStyle MakeCenteredBoldLabel(int fontSize)
|
||||
{
|
||||
var style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = fontSize,
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
style.normal.textColor = Color.white;
|
||||
return style;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 130fbae3929f3274a87fa23acc5b45f5
|
||||
@@ -0,0 +1,314 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
using UnityEngine.InputSystem;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// uGUI binding for <see cref="OnlineSessionManager"/>. Drives the server-browser canvas - Host / Refresh /
|
||||
/// pick a row / Join / Disconnect plus a status line - and mirrors the manager's state through its
|
||||
/// <see cref="OnlineSessionManager.Changed"/> / <see cref="OnlineSessionManager.SessionsChanged"/> events.
|
||||
/// Lives on the browser canvas; the session logic stays on the NetworkManager's
|
||||
/// <see cref="OnlineSessionManager"/>.
|
||||
///
|
||||
/// Selection model: Refresh fills the list with clones of <see cref="entryTemplate"/> (each a clickable row
|
||||
/// showing name + player count). Clicking a row selects it; Join connects to the selected session. While
|
||||
/// connected, only Disconnect is interactable.
|
||||
///
|
||||
/// Overlay behaviour: the panel is always shown in Main (not connected). Once a connection is up it hides;
|
||||
/// in the lobby/map Esc toggles it back as a modal pause overlay (cursor freed, local gameplay suspended via
|
||||
/// <see cref="GameModeManager.SetPaused"/>). The Disconnect button is context-sensitive: in Main (not
|
||||
/// connected) it is an application "Exit" (quits the build, or stops play mode in the editor) so the player
|
||||
/// can leave without Alt+F4; on the map it becomes "Back to Lobby" for the match authority (ends the match);
|
||||
/// everyone else (in the lobby, or a non-authority client on the map) gets a plain disconnect. Only the
|
||||
/// visual <see cref="panelRoot"/> is toggled - this binder stays alive on its (always-active) canvas so it
|
||||
/// keeps reading Esc while the panel is hidden.
|
||||
/// </summary>
|
||||
public class OnlineSessionBrowserUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private OnlineSessionManager manager;
|
||||
|
||||
[Header("Buttons")]
|
||||
[SerializeField] private Button hostButton;
|
||||
[SerializeField] private Button refreshButton;
|
||||
[SerializeField] private Button joinButton;
|
||||
[SerializeField] private Button disconnectButton;
|
||||
|
||||
[Header("List")]
|
||||
[Tooltip("Content RectTransform of the session ScrollRect (rows are added here).")]
|
||||
[SerializeField] private RectTransform listContent;
|
||||
[Tooltip("A row prototype kept disabled; cloned once per session. Children named 'Name' and 'Players' carry the text.")]
|
||||
[SerializeField] private GameObject entryTemplate;
|
||||
|
||||
[Header("Status")]
|
||||
[SerializeField] private TMP_Text statusLabel;
|
||||
|
||||
[Header("Overlay")]
|
||||
[Tooltip("Visual container toggled by Esc while connected (the panel root). Must be a CHILD of this " +
|
||||
"binder's object so the binder keeps running - and reading Esc - while the panel is hidden.")]
|
||||
[SerializeField] private GameObject panelRoot;
|
||||
|
||||
[Tooltip("Background tint of the selected row.")]
|
||||
[SerializeField] private Color selectedTint = new Color(0.26f, 0.52f, 0.96f, 1f);
|
||||
[Tooltip("Background tint of an unselected row.")]
|
||||
[SerializeField] private Color normalTint = new Color(1f, 1f, 1f, 0.05f);
|
||||
|
||||
private struct Row { public string id; public Image image; public GameObject go; }
|
||||
private readonly List<Row> _rows = new List<Row>();
|
||||
private string _selectedId;
|
||||
|
||||
private MatchManager _match; // local match state (scene kind + authority); found at runtime
|
||||
private GameModeManager _gameMode; // pause hook for the modal overlay; found at runtime
|
||||
private TMP_Text _disconnectLabel; // disconnect button caption (relabelled to "Back to Lobby")
|
||||
|
||||
private bool _open; // Esc-toggled visibility while connected
|
||||
private bool _connected; // last-seen connection state (to detect transitions)
|
||||
private Scene _lastScene; // last-seen active scene (to auto-close on a scene change)
|
||||
private string _disconnectText; // last applied caption (avoids per-frame TMP churn)
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// The template is the prototype, never shown itself - layout ignores it while disabled.
|
||||
if (entryTemplate) entryTemplate.SetActive(false);
|
||||
|
||||
_match = FindFirstObjectByType<MatchManager>();
|
||||
_gameMode = FindFirstObjectByType<GameModeManager>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (hostButton) hostButton.onClick.AddListener(OnHost);
|
||||
if (refreshButton) refreshButton.onClick.AddListener(OnRefresh);
|
||||
if (joinButton) joinButton.onClick.AddListener(OnJoin);
|
||||
if (disconnectButton)
|
||||
{
|
||||
disconnectButton.onClick.AddListener(OnDisconnect);
|
||||
_disconnectLabel = disconnectButton.GetComponentInChildren<TMP_Text>(true);
|
||||
}
|
||||
|
||||
if (manager)
|
||||
{
|
||||
manager.Changed += OnManagerChanged;
|
||||
manager.SessionsChanged += RebuildList;
|
||||
}
|
||||
|
||||
_connected = manager && manager.IsConnected;
|
||||
RebuildList();
|
||||
OnManagerChanged();
|
||||
ApplyVisibility();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (hostButton) hostButton.onClick.RemoveListener(OnHost);
|
||||
if (refreshButton) refreshButton.onClick.RemoveListener(OnRefresh);
|
||||
if (joinButton) joinButton.onClick.RemoveListener(OnJoin);
|
||||
if (disconnectButton) disconnectButton.onClick.RemoveListener(OnDisconnect);
|
||||
|
||||
if (manager)
|
||||
{
|
||||
manager.Changed -= OnManagerChanged;
|
||||
manager.SessionsChanged -= RebuildList;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!manager) return;
|
||||
bool connected = manager.IsConnected;
|
||||
|
||||
// Auto-close the overlay if the scene changed underneath it (a remote ended/started the match, or
|
||||
// we walked into Start Match) so it never lingers as a stale pause across a transition.
|
||||
// Compared as Scene structs (handles), not names, so nothing is allocated per frame.
|
||||
Scene scene = SceneManager.GetActiveScene();
|
||||
if (connected && _open && _lastScene.IsValid() && scene != _lastScene)
|
||||
SetOpen(false);
|
||||
_lastScene = scene;
|
||||
|
||||
if (!connected) return; // Main: panel always shown, Esc ignored
|
||||
|
||||
bool inLobbyOrMap = _match && (_match.IsLocalSceneLobby || _match.IsLocalSceneMap);
|
||||
if (inLobbyOrMap && EscapePressed())
|
||||
SetOpen(!_open);
|
||||
|
||||
RefreshDisconnectLabel();
|
||||
}
|
||||
|
||||
private void OnHost() { if (manager) manager.Host(); }
|
||||
private void OnRefresh() { if (manager) manager.Refresh(); }
|
||||
private void OnJoin() { if (manager && !string.IsNullOrEmpty(_selectedId)) manager.Join(_selectedId); }
|
||||
|
||||
private void OnDisconnect()
|
||||
{
|
||||
if (!manager) return;
|
||||
|
||||
// In Main (no session) the button is an application Exit, so the player can quit the build
|
||||
// without Alt+F4.
|
||||
if (!manager.IsConnected)
|
||||
{
|
||||
QuitApplication();
|
||||
return;
|
||||
}
|
||||
|
||||
// On the map, the match AUTHORITY's button means "Back to Lobby" (end the match for everyone)
|
||||
// rather than leaving the session. Everyone else - in the lobby, or a non-authority client on the
|
||||
// map - does a plain disconnect.
|
||||
if (_match && _match.IsLocalSceneMap && _match.IsLocalMatchAuthority)
|
||||
{
|
||||
_match.RequestEndMatch();
|
||||
SetOpen(false); // heading back to the lobby; close the overlay + resume input
|
||||
return;
|
||||
}
|
||||
|
||||
manager.Disconnect();
|
||||
}
|
||||
|
||||
private static void QuitApplication()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.isPlaying = false;
|
||||
#else
|
||||
Application.Quit();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnManagerChanged()
|
||||
{
|
||||
if (!manager) return;
|
||||
|
||||
bool connected = manager.IsConnected;
|
||||
if (connected != _connected)
|
||||
{
|
||||
_connected = connected;
|
||||
// Connection up (hosted/joined) -> hide the panel; Esc reopens it in the lobby/map.
|
||||
// Back to Main (disconnect/shutdown) -> show it again and drop any pause.
|
||||
_open = false;
|
||||
if (!connected && _gameMode) _gameMode.SetPaused(false);
|
||||
ApplyVisibility();
|
||||
}
|
||||
|
||||
if (statusLabel) statusLabel.text = manager.Status;
|
||||
UpdateButtons();
|
||||
RefreshDisconnectLabel();
|
||||
}
|
||||
|
||||
private void SetOpen(bool open)
|
||||
{
|
||||
_open = open;
|
||||
ApplyVisibility();
|
||||
if (_gameMode) _gameMode.SetPaused(open);
|
||||
}
|
||||
|
||||
private void ApplyVisibility()
|
||||
{
|
||||
bool show = !(manager && manager.IsConnected) || _open;
|
||||
if (panelRoot && panelRoot.activeSelf != show)
|
||||
panelRoot.SetActive(show);
|
||||
}
|
||||
|
||||
private void RefreshDisconnectLabel()
|
||||
{
|
||||
if (!_disconnectLabel) return;
|
||||
string text;
|
||||
if (!(manager && manager.IsConnected))
|
||||
text = "Exit"; // Main: quit the application
|
||||
else if (_match && _match.IsLocalSceneMap && _match.IsLocalMatchAuthority)
|
||||
text = "Back to Lobby";
|
||||
else
|
||||
text = "Disconnect";
|
||||
if (text != _disconnectText)
|
||||
{
|
||||
_disconnectText = text;
|
||||
_disconnectLabel.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateButtons()
|
||||
{
|
||||
if (!manager) return;
|
||||
bool busy = manager.IsBusy;
|
||||
bool connected = manager.IsConnected;
|
||||
bool hasSelection = !string.IsNullOrEmpty(_selectedId);
|
||||
|
||||
if (hostButton) hostButton.interactable = !busy && !connected;
|
||||
if (refreshButton) refreshButton.interactable = !busy && !connected;
|
||||
if (joinButton) joinButton.interactable = !busy && !connected && hasSelection;
|
||||
// Usable whenever idle: connected -> Disconnect / Back to Lobby; in Main -> Exit.
|
||||
if (disconnectButton) disconnectButton.interactable = !busy;
|
||||
}
|
||||
|
||||
private void RebuildList()
|
||||
{
|
||||
for (int i = 0; i < _rows.Count; i++)
|
||||
if (_rows[i].go) Destroy(_rows[i].go);
|
||||
_rows.Clear();
|
||||
|
||||
if (manager && entryTemplate && listContent)
|
||||
{
|
||||
var sessions = manager.Sessions;
|
||||
bool selectionStillPresent = false;
|
||||
|
||||
for (int i = 0; i < sessions.Count; i++)
|
||||
{
|
||||
var s = sessions[i];
|
||||
var go = Instantiate(entryTemplate, listContent);
|
||||
go.SetActive(true);
|
||||
|
||||
var nameText = FindText(go.transform, "Name");
|
||||
if (nameText) nameText.text = s.Name;
|
||||
|
||||
var playersText = FindText(go.transform, "Players");
|
||||
if (playersText)
|
||||
{
|
||||
int taken = Mathf.Max(0, s.MaxPlayers - s.AvailableSlots);
|
||||
playersText.text = $"{taken}/{s.MaxPlayers}";
|
||||
}
|
||||
|
||||
string id = s.Id; // capture per-row for the click closure
|
||||
var button = go.GetComponent<Button>();
|
||||
if (button) button.onClick.AddListener(() => Select(id));
|
||||
|
||||
_rows.Add(new Row { id = id, image = go.GetComponent<Image>(), go = go });
|
||||
if (id == _selectedId) selectionStillPresent = true;
|
||||
}
|
||||
|
||||
if (!selectionStillPresent) _selectedId = null;
|
||||
}
|
||||
|
||||
ApplyTints();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
private void Select(string id)
|
||||
{
|
||||
_selectedId = id;
|
||||
ApplyTints();
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
private void ApplyTints()
|
||||
{
|
||||
for (int i = 0; i < _rows.Count; i++)
|
||||
if (_rows[i].image)
|
||||
_rows[i].image.color = _rows[i].id == _selectedId ? selectedTint : normalTint;
|
||||
}
|
||||
|
||||
private static TMP_Text FindText(Transform root, string childName)
|
||||
{
|
||||
var t = root.Find(childName);
|
||||
return t ? t.GetComponent<TMP_Text>() : null;
|
||||
}
|
||||
|
||||
// NOT TypingGuard-gated (GameplayKeyPressed): Esc must close the panel even while a text field has focus.
|
||||
private static bool EscapePressed()
|
||||
{
|
||||
#if ENABLE_INPUT_SYSTEM
|
||||
return InputUtil.KeyPressedThisFrame(Key.Escape);
|
||||
#else
|
||||
return InputUtil.KeyPressedThisFrame(KeyCode.Escape);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c001d18ade3f180488179878fbaa3eef
|
||||
Reference in new issue
Block a user