Feat - Add Loading menju
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using DG.Tweening;
|
||||
using Ashwild.Player;
|
||||
|
||||
namespace Ashwild.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Full-screen loading overlay shown while a session starts and the game scene loads over the
|
||||
/// network. It is deliberately NOT a UIPanel: panels live on a per-scene UIManager and are
|
||||
/// destroyed when their scene unloads, but FishNet loads the game scene with ReplaceOption.All —
|
||||
/// so the menu (and anything on it) is torn down mid-transition. This controller therefore marks
|
||||
/// itself DontDestroyOnLoad and survives the swap, covering the screen continuously from the save
|
||||
/// click until the local player has spawned. It hides the rotating menu camera and the scene
|
||||
/// switch behind a quote-cycling curtain.
|
||||
///
|
||||
/// Place it on its own high-sorting-order Canvas in the menu scene (mirror SplashScreenController).
|
||||
/// It is driven entirely by the PlayerEvents bus: SessionStarting/SessionJoining raise it,
|
||||
/// LocalPlayerSpawned lowers it, and SessionError/SessionStopped lower it as a safety net so a
|
||||
/// failed host never leaves the player stuck behind the curtain.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class LoadingScreen : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[Tooltip("Label that displays the current quote (and its author on a second line).")]
|
||||
[SerializeField] private TextMeshProUGUI quoteLabel;
|
||||
|
||||
[Tooltip("Pool of quotes to cycle through. Assign a Loading Quotes asset.")]
|
||||
[SerializeField] private LoadingQuotes quotes;
|
||||
|
||||
[Header("Curtain Fade")]
|
||||
[SerializeField] private float fadeInDuration = 0.35f;
|
||||
[SerializeField] private float fadeOutDuration = 0.5f;
|
||||
[SerializeField] private Ease fadeInEase = Ease.OutQuad;
|
||||
[SerializeField] private Ease fadeOutEase = Ease.InQuad;
|
||||
|
||||
[Tooltip("Minimum time the curtain stays up once raised, so an instant load doesn't flash.")]
|
||||
[SerializeField] private float minimumDisplayTime = 1.5f;
|
||||
|
||||
[Header("Quote Cycling")]
|
||||
[Tooltip("How long each quote stays on screen before swapping to the next.")]
|
||||
[SerializeField] private float quoteDuration = 4f;
|
||||
[Tooltip("Cross-fade duration when swapping one quote for the next.")]
|
||||
[SerializeField] private float quoteFadeDuration = 0.4f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
/// <summary>
|
||||
/// Single persistent instance — a second copy arriving with a reloaded menu scene self-destructs.
|
||||
/// </summary>
|
||||
private static LoadingScreen instance;
|
||||
|
||||
private CanvasGroup canvasGroup;
|
||||
|
||||
/// <summary>
|
||||
/// True while the curtain is up (or being raised). Guards against double-show / double-hide.
|
||||
/// </summary>
|
||||
private bool isShown;
|
||||
|
||||
/// <summary>
|
||||
/// Unscaled timestamp at which the curtain was raised, used to enforce the minimum display time.
|
||||
/// </summary>
|
||||
private float shownAt;
|
||||
|
||||
/// <summary>
|
||||
/// Index of the quote currently displayed, so the next pick can avoid repeating it.
|
||||
/// </summary>
|
||||
private int currentQuoteIndex = -1;
|
||||
|
||||
private Tween curtainTween;
|
||||
private Sequence quoteSequence;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the singleton, persists across the menu→game scene swap, and starts hidden.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
if (instance != null && instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
canvasGroup.alpha = 0f;
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.interactable = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the session lifecycle events that raise and lower the curtain.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerEvents.SessionStarting += HandleSessionBeginning;
|
||||
PlayerEvents.SessionJoining += HandleSessionJoining;
|
||||
PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned;
|
||||
PlayerEvents.SessionError += HandleSessionError;
|
||||
PlayerEvents.SessionStopped += HandleSessionStopped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes — mirrors OnEnable exactly.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
PlayerEvents.SessionStarting -= HandleSessionBeginning;
|
||||
PlayerEvents.SessionJoining -= HandleSessionJoining;
|
||||
PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned;
|
||||
PlayerEvents.SessionError -= HandleSessionError;
|
||||
PlayerEvents.SessionStopped -= HandleSessionStopped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills any running tweens so nothing animates a destroyed target on teardown.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
curtainTween?.Kill();
|
||||
quoteSequence?.Kill();
|
||||
if (instance == this) instance = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Raises the curtain when the local player starts hosting a session.
|
||||
/// </summary>
|
||||
private void HandleSessionBeginning() => Show();
|
||||
|
||||
/// <summary>
|
||||
/// Raises the curtain when the local player begins joining a remote session.
|
||||
/// </summary>
|
||||
private void HandleSessionJoining(string code) => Show();
|
||||
|
||||
/// <summary>
|
||||
/// Lowers the curtain once the local player has spawned into the loaded game scene.
|
||||
/// </summary>
|
||||
private void HandleLocalPlayerSpawned() => Hide();
|
||||
|
||||
/// <summary>
|
||||
/// Safety net: a connection failure must drop the curtain so the menu becomes usable again.
|
||||
/// </summary>
|
||||
private void HandleSessionError(string reason) => Hide();
|
||||
|
||||
/// <summary>
|
||||
/// Safety net: if the session tears down before a player spawns, never leave the curtain up.
|
||||
/// </summary>
|
||||
private void HandleSessionStopped() => Hide();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Curtain
|
||||
|
||||
/// <summary>
|
||||
/// Fades the curtain in (blocking input underneath) and starts cycling quotes. Ignored when
|
||||
/// already shown so re-entrant session events don't restart the animation.
|
||||
/// </summary>
|
||||
private void Show()
|
||||
{
|
||||
if (isShown) return;
|
||||
isShown = true;
|
||||
shownAt = Time.unscaledTime;
|
||||
|
||||
curtainTween?.Kill();
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
canvasGroup.interactable = true;
|
||||
|
||||
curtainTween = canvasGroup.DOFade(1f, fadeInDuration)
|
||||
.SetEase(fadeInEase)
|
||||
.SetUpdate(true);
|
||||
|
||||
ShowNextQuote(instant: true);
|
||||
StartQuoteCycle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fades the curtain out and stops the quote cycle. Honours the minimum display time so an
|
||||
/// instant load still shows the loading screen for a readable beat instead of flashing.
|
||||
/// </summary>
|
||||
private void Hide()
|
||||
{
|
||||
if (!isShown) return;
|
||||
isShown = false;
|
||||
|
||||
float elapsed = Time.unscaledTime - shownAt;
|
||||
float delay = Mathf.Max(0f, minimumDisplayTime - elapsed);
|
||||
|
||||
curtainTween?.Kill();
|
||||
curtainTween = canvasGroup.DOFade(0f, fadeOutDuration)
|
||||
.SetDelay(delay)
|
||||
.SetEase(fadeOutEase)
|
||||
.SetUpdate(true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.interactable = false;
|
||||
StopQuoteCycle();
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Quotes
|
||||
|
||||
/// <summary>
|
||||
/// Builds the looping cross-fade that swaps the quote every <see cref="quoteDuration"/> seconds.
|
||||
/// Runs on unscaled time so it keeps animating even while the load freezes the timescale.
|
||||
///
|
||||
/// Bails out (leaving the first quote shown statically) when there is nothing to cycle or when
|
||||
/// <see cref="quoteDuration"/> is not positive: a looping DOTween sequence with a zero total
|
||||
/// duration replays infinitely within a single frame and hard-freezes the editor, so the
|
||||
/// positive-duration guard here is mandatory, not cosmetic.
|
||||
/// </summary>
|
||||
private void StartQuoteCycle()
|
||||
{
|
||||
StopQuoteCycle();
|
||||
if (quotes == null || quotes.Count <= 1 || quoteLabel == null) return;
|
||||
if (quoteDuration <= 0f) return;
|
||||
|
||||
float fade = Mathf.Max(0f, quoteFadeDuration);
|
||||
|
||||
quoteSequence = DOTween.Sequence().SetUpdate(true);
|
||||
quoteSequence.AppendInterval(quoteDuration);
|
||||
quoteSequence.Append(quoteLabel.DOFade(0f, fade));
|
||||
quoteSequence.AppendCallback(() => ShowNextQuote(instant: false));
|
||||
quoteSequence.Append(quoteLabel.DOFade(1f, fade));
|
||||
quoteSequence.SetLoops(-1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the running quote cycle, if any.
|
||||
/// </summary>
|
||||
private void StopQuoteCycle()
|
||||
{
|
||||
quoteSequence?.Kill();
|
||||
quoteSequence = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks the next random quote and writes it to the label. When <paramref name="instant"/> is
|
||||
/// true the label is forced fully opaque (used on the very first quote, before the cross-fade
|
||||
/// loop takes over); otherwise the surrounding sequence owns the alpha.
|
||||
/// </summary>
|
||||
private void ShowNextQuote(bool instant)
|
||||
{
|
||||
if (quotes == null || !quotes.HasQuotes || quoteLabel == null) return;
|
||||
|
||||
LoadingQuotes.Quote quote = quotes.GetRandom(currentQuoteIndex, out currentQuoteIndex);
|
||||
quoteLabel.text = Format(quote);
|
||||
|
||||
if (instant)
|
||||
{
|
||||
Color c = quoteLabel.color;
|
||||
c.a = 1f;
|
||||
quoteLabel.color = c;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composes the displayed string: the quote, plus an em-dashed author line when one is set.
|
||||
/// </summary>
|
||||
private static string Format(LoadingQuotes.Quote quote)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(quote.author)
|
||||
? quote.text
|
||||
: $"{quote.text}\n<size=80%>— {quote.author}</size>";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user