196 lines
6.0 KiB
C#
196 lines
6.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using Ashwild.Player;
|
|
|
|
namespace Ashwild.UI
|
|
{
|
|
/// <summary>
|
|
/// Generic panel-stack engine shared by every scene that drives a stack of <see cref="UIPanel"/>s
|
|
/// (the menu and the in-game UI). It owns the panel registry, the navigation history and the
|
|
/// show/hide flow, and dispatches the Cancel (Escape) input from the bus to a scene-specific
|
|
/// policy via <see cref="OnEscape"/>.
|
|
///
|
|
/// This is abstract: put the concrete <c>MenuUIManager</c> or <c>GameUIManager</c> on the object,
|
|
/// never this. Menu and game live in separate scenes, so only one manager is active at a time —
|
|
/// hence a single shared <see cref="Instance"/> of the base type, which lets panels reach "their"
|
|
/// manager through <c>UIManager.Instance</c> regardless of which concrete subclass is running.
|
|
/// </summary>
|
|
public abstract class UIManager : MonoBehaviour
|
|
{
|
|
#region State
|
|
|
|
/// <summary>
|
|
/// The UI manager for the currently loaded scene (menu or game).
|
|
/// </summary>
|
|
public static UIManager Instance { get; private set; }
|
|
|
|
[Header("Panels")]
|
|
[SerializeField] protected List<UIPanel> panels;
|
|
[Tooltip("Panel shown on start. Leave empty for scenes that begin with no panel (e.g. gameplay).")]
|
|
[SerializeField] protected UIPanel defaultPanel;
|
|
|
|
private Dictionary<Type, UIPanel> map;
|
|
private Stack<UIPanel> history;
|
|
private UIPanel current;
|
|
|
|
/// <summary>
|
|
/// The panel currently on top of the stack (null when nothing is open).
|
|
/// </summary>
|
|
public UIPanel Current => current;
|
|
|
|
/// <summary>
|
|
/// True while at least one panel is open.
|
|
/// </summary>
|
|
protected bool HasOpenPanel => current != null;
|
|
|
|
/// <summary>
|
|
/// True when there is a panel below the current one to go back to.
|
|
/// </summary>
|
|
protected bool CanGoBack => history != null && history.Count > 1;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Establishes the singleton and builds the type→panel lookup.
|
|
/// </summary>
|
|
protected virtual void Awake()
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
|
|
map = panels.Where(p => p != null).ToDictionary(p => p.GetType());
|
|
history = new Stack<UIPanel>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribes to the Cancel (Escape) input from the bus.
|
|
/// </summary>
|
|
protected virtual void OnEnable()
|
|
{
|
|
PlayerEvents.CancelPressed += OnEscape;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes — mirrors OnEnable.
|
|
/// </summary>
|
|
protected virtual void OnDisable()
|
|
{
|
|
PlayerEvents.CancelPressed -= OnEscape;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hides every panel, then opens the default one (if any).
|
|
/// </summary>
|
|
protected virtual void Start()
|
|
{
|
|
foreach (UIPanel p in panels)
|
|
if (p != null) p.HideInstant();
|
|
|
|
if (defaultPanel != null)
|
|
OpenPanel(defaultPanel.GetType());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the singleton reference.
|
|
/// </summary>
|
|
protected virtual void OnDestroy()
|
|
{
|
|
if (Instance == this) Instance = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// Opens the registered panel of type T.
|
|
/// </summary>
|
|
public void OpenPanel<T>() where T : UIPanel => OpenPanel(typeof(T));
|
|
|
|
/// <summary>
|
|
/// Opens the given panel instance (resolved by its concrete type).
|
|
/// </summary>
|
|
public void OpenPanel(UIPanel panel)
|
|
{
|
|
if (panel == null) return;
|
|
OpenPanel(panel.GetType());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Goes back one level in the navigation history. Never closes the last panel — use
|
|
/// <see cref="CloseAll"/> for that (the menu always keeps its default panel visible).
|
|
/// </summary>
|
|
public void Back()
|
|
{
|
|
if (history.Count <= 1) return;
|
|
|
|
UIPanel closing = history.Pop();
|
|
closing.Hide();
|
|
|
|
current = history.Peek();
|
|
current.Show();
|
|
OnCurrentChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes every open panel, returning to the "no panel" state (used by gameplay to leave pause).
|
|
/// </summary>
|
|
public void CloseAll()
|
|
{
|
|
if (history.Count == 0) return;
|
|
|
|
while (history.Count > 0)
|
|
history.Pop().Hide();
|
|
|
|
current = null;
|
|
OnCurrentChanged();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Internal
|
|
|
|
/// <summary>
|
|
/// Core open: hides the current panel, pushes and shows the requested one.
|
|
/// </summary>
|
|
protected void OpenPanel(Type t)
|
|
{
|
|
if (map == null) return;
|
|
if (!map.TryGetValue(t, out UIPanel next))
|
|
{
|
|
Debug.LogWarning($"[{GetType().Name}] no panel of type {t.Name} registered.", this);
|
|
return;
|
|
}
|
|
if (current == next) return;
|
|
|
|
if (current != null) current.Hide();
|
|
|
|
history.Push(next);
|
|
current = next;
|
|
next.Show();
|
|
OnCurrentChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scene-specific Escape behaviour (menu = navigate back, game = toggle pause).
|
|
/// </summary>
|
|
protected abstract void OnEscape();
|
|
|
|
/// <summary>
|
|
/// Hook fired whenever the top panel changes (opened / went back / closed all).
|
|
/// Subclasses use it to react to the stack becoming empty or non-empty.
|
|
/// </summary>
|
|
protected virtual void OnCurrentChanged() { }
|
|
|
|
#endregion
|
|
}
|
|
}
|