(Fix) Loading Screen
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using FishNet;
|
||||
using FishNet.Connection;
|
||||
@@ -29,6 +30,10 @@ namespace Ashwild.Network
|
||||
[Tooltip("The single source of truth for the gameplay scene loaded over the network.")]
|
||||
[SerializeField] private string gameSceneName = "TestScene";
|
||||
|
||||
[Tooltip("Max seconds to wait for the loading curtain to fully fade in before loading the game " +
|
||||
"scene anyway. Safety net so a missing/disabled LoadingScreen can never soft-lock the host.")]
|
||||
[SerializeField] private float curtainWaitTimeout = 3f;
|
||||
|
||||
[Header("Player")]
|
||||
[Tooltip("The networked player prefab (NetworkObject + PlayerNetworkController).")]
|
||||
[SerializeField] private NetworkObject playerPrefab;
|
||||
@@ -61,6 +66,12 @@ namespace Ashwild.Network
|
||||
/// </summary>
|
||||
private bool pendingSceneLoad;
|
||||
|
||||
/// <summary>
|
||||
/// Set once the loading curtain has fully faded in; gates the heavy game scene load so the
|
||||
/// activation freeze happens behind an opaque curtain. Reset at the start of every host launch.
|
||||
/// </summary>
|
||||
private bool curtainShown;
|
||||
|
||||
/// <summary>
|
||||
/// Round-robin cursor over the available spawn points.
|
||||
/// </summary>
|
||||
@@ -107,6 +118,7 @@ namespace Ashwild.Network
|
||||
networkManager.ServerManager.OnRemoteConnectionState += HandleRemoteState;
|
||||
networkManager.ClientManager.OnClientConnectionState += HandleClientState;
|
||||
networkManager.SceneManager.OnClientPresenceChangeEnd += HandleClientPresenceEnd;
|
||||
PlayerEvents.LoadingCurtainShown += HandleLoadingCurtainShown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -120,6 +132,7 @@ namespace Ashwild.Network
|
||||
networkManager.ServerManager.OnRemoteConnectionState -= HandleRemoteState;
|
||||
networkManager.ClientManager.OnClientConnectionState -= HandleClientState;
|
||||
networkManager.SceneManager.OnClientPresenceChangeEnd -= HandleClientPresenceEnd;
|
||||
PlayerEvents.LoadingCurtainShown -= HandleLoadingCurtainShown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -151,6 +164,7 @@ namespace Ashwild.Network
|
||||
PlayerEvents.RaiseSessionStarting();
|
||||
Log("Starting host (server + client) over Steam…");
|
||||
|
||||
curtainShown = false;
|
||||
pendingSceneLoad = true;
|
||||
|
||||
bool serverOk = networkManager.ServerManager.StartConnection();
|
||||
@@ -215,6 +229,26 @@ namespace Ashwild.Network
|
||||
if (networkManager.ServerManager.Started) networkManager.ServerManager.StopConnection(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leaves the current session and returns to the menu scene behind the loading curtain. Mirrors
|
||||
/// the host launch: it raises the curtain, waits for it to fully fade in (so the menu-load freeze
|
||||
/// is hidden), tears the session down, loads the menu, then signals MenuReady to drop the curtain.
|
||||
/// Orchestrated here — not on the per-scene GameUIManager — because this object persists across
|
||||
/// the scene swap while the caller is destroyed by the load.
|
||||
/// </summary>
|
||||
public void ReturnToMenu(string menuSceneName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(menuSceneName))
|
||||
{
|
||||
Debug.LogError("[NetworkSession] ReturnToMenu called with no menu scene name.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerEvents.RaiseReturningToMenu();
|
||||
curtainShown = false;
|
||||
StartCoroutine(ReturnToMenuRoutine(menuSceneName));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Connection Handlers
|
||||
@@ -235,7 +269,7 @@ namespace Ashwild.Network
|
||||
if (pendingSceneLoad)
|
||||
{
|
||||
pendingSceneLoad = false;
|
||||
LoadGameSceneNetworked();
|
||||
StartCoroutine(LoadGameSceneWhenCurtainShown());
|
||||
}
|
||||
}
|
||||
else if (args.ConnectionState == LocalConnectionState.Stopped)
|
||||
@@ -268,6 +302,12 @@ namespace Ashwild.Network
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The loading curtain finished fading in — release the gate so the deferred game scene
|
||||
/// load can proceed (see <see cref="LoadGameSceneWhenCurtainShown"/>).
|
||||
/// </summary>
|
||||
private void HandleLoadingCurtainShown() => curtainShown = true;
|
||||
|
||||
/// <summary>
|
||||
/// A *remote* client connected to or disconnected from our server; updates the count.
|
||||
/// </summary>
|
||||
@@ -343,6 +383,53 @@ namespace Ashwild.Network
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Defers the heavy networked scene load until the loading curtain has fully faded in
|
||||
/// (PlayerEvents.LoadingCurtainShown), so the unavoidable activation freeze of a big scene
|
||||
/// happens behind an already-opaque curtain instead of stalling the frame the curtain was
|
||||
/// meant to appear on. Falls back to loading anyway after <see cref="curtainWaitTimeout"/> so
|
||||
/// a missing or disabled LoadingScreen can never soft-lock the host. Runs on unscaled time.
|
||||
/// </summary>
|
||||
private IEnumerator LoadGameSceneWhenCurtainShown()
|
||||
{
|
||||
float start = Time.unscaledTime;
|
||||
while (!curtainShown && Time.unscaledTime - start < curtainWaitTimeout)
|
||||
yield return null;
|
||||
|
||||
if (!curtainShown)
|
||||
Log($"Loading curtain not reported within {curtainWaitTimeout}s — loading scene anyway.");
|
||||
|
||||
LoadGameSceneNetworked();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the curtain to fully fade in (or the timeout), tears the session down, loads the
|
||||
/// menu scene locally (Single — the persistent NetworkManager survives), then raises MenuReady
|
||||
/// so the curtain drops once the menu is up. Runs on the persistent NetworkManager object so it
|
||||
/// outlives the game scene being unloaded. Uses unscaled time.
|
||||
/// </summary>
|
||||
private IEnumerator ReturnToMenuRoutine(string menuSceneName)
|
||||
{
|
||||
float start = Time.unscaledTime;
|
||||
while (!curtainShown && Time.unscaledTime - start < curtainWaitTimeout)
|
||||
yield return null;
|
||||
|
||||
StopSession();
|
||||
|
||||
AsyncOperation op = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync(menuSceneName, LoadSceneMode.Single);
|
||||
if (op != null)
|
||||
{
|
||||
while (!op.isDone)
|
||||
yield return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[NetworkSession] Could not load menu scene '{menuSceneName}' — is it in Build Settings?", this);
|
||||
}
|
||||
|
||||
PlayerEvents.RaiseMenuReady();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the gameplay scene as a global networked scene, replacing the menu.
|
||||
/// </summary>
|
||||
|
||||
@@ -150,6 +150,9 @@ namespace Ashwild.Player
|
||||
public static event Action<string> SessionError; // human-readable failure reason
|
||||
public static event Action<int> RemotePlayerCountChanged; // number of *other* connected clients
|
||||
public static event Action LocalPlayerSpawned; // the local owned player NetworkObject is ready
|
||||
public static event Action LoadingCurtainShown; // loading curtain finished fading in — safe to start the heavy scene load behind it
|
||||
public static event Action ReturningToMenu; // leaving the session back to the menu — raise the curtain over the swap
|
||||
public static event Action MenuReady; // the menu scene has finished loading — lower the curtain
|
||||
public static event Action<ulong, string, string> InviteReceived; // inviter SteamID64, inviter name, connect code
|
||||
|
||||
// ============================================================
|
||||
@@ -330,6 +333,9 @@ namespace Ashwild.Player
|
||||
public static void RaiseSessionError(string reason) { Log(nameof(SessionError)); SessionError?.Invoke(reason); }
|
||||
public static void RaiseRemotePlayerCountChanged(int count) { Log(nameof(RemotePlayerCountChanged)); RemotePlayerCountChanged?.Invoke(count); }
|
||||
public static void RaiseLocalPlayerSpawned() { Log(nameof(LocalPlayerSpawned)); LocalPlayerSpawned?.Invoke(); }
|
||||
public static void RaiseLoadingCurtainShown() { Log(nameof(LoadingCurtainShown)); LoadingCurtainShown?.Invoke(); }
|
||||
public static void RaiseReturningToMenu() { Log(nameof(ReturningToMenu)); ReturningToMenu?.Invoke(); }
|
||||
public static void RaiseMenuReady() { Log(nameof(MenuReady)); MenuReady?.Invoke(); }
|
||||
|
||||
/// <summary>
|
||||
/// A friend invited the local player to their session (delivered by Steam). Carries the
|
||||
|
||||
@@ -147,13 +147,19 @@ namespace Ashwild.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leaves the session and returns to the main menu scene.
|
||||
/// Leaves the session and returns to the main menu scene behind the loading curtain. The
|
||||
/// transition is owned by the persistent NetworkSessionManager (it survives the scene swap,
|
||||
/// this manager does not); falls back to a direct, uncovered load only if it is missing.
|
||||
/// </summary>
|
||||
public void QuitToMenu()
|
||||
{
|
||||
if (NetworkSessionManager.Instance != null)
|
||||
NetworkSessionManager.Instance.StopSession();
|
||||
{
|
||||
NetworkSessionManager.Instance.ReturnToMenu(menuSceneName);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning("[GameUIManager] NetworkSessionManager missing — loading menu directly without the loading curtain.", this);
|
||||
SceneManager.LoadScene(menuSceneName);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,11 @@ namespace Ashwild.UI
|
||||
/// 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.
|
||||
/// It is driven entirely by the PlayerEvents bus: SessionStarting/SessionJoining raise it for the
|
||||
/// menu→game transition (lowered by LocalPlayerSpawned), and ReturningToMenu raises it for the
|
||||
/// game→menu transition (lowered by MenuReady once the menu has loaded). SessionError lowers it as
|
||||
/// a safety net so a failed host never leaves the player stuck behind the curtain; SessionStopped
|
||||
/// does the same except while a return-to-menu is in flight, where that teardown event is expected.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
@@ -41,6 +43,12 @@ namespace Ashwild.UI
|
||||
[Tooltip("Minimum time the curtain stays up once raised, so an instant load doesn't flash.")]
|
||||
[SerializeField] private float minimumDisplayTime = 1.5f;
|
||||
|
||||
[Tooltip("Extra time the curtain holds after the ready signal (player spawned / menu loaded) " +
|
||||
"before fading out, so the scene's first heavy frames (object pop-in, shader warmup) " +
|
||||
"settle behind it instead of stuttering on screen. Stacks as the max with the " +
|
||||
"remaining minimum display time.")]
|
||||
[SerializeField] private float settleDelay = 0.75f;
|
||||
|
||||
[Header("Quote Cycling")]
|
||||
[Tooltip("How long each quote stays on screen before swapping to the next.")]
|
||||
[SerializeField] private float quoteDuration = 4f;
|
||||
@@ -68,6 +76,13 @@ namespace Ashwild.UI
|
||||
/// </summary>
|
||||
private float shownAt;
|
||||
|
||||
/// <summary>
|
||||
/// True while the curtain is covering a return-to-menu transition. SessionStopped fires during
|
||||
/// that teardown, so this flag tells the curtain to ignore it and wait for MenuReady instead —
|
||||
/// otherwise the curtain would drop the instant the session stopped, before the menu has loaded.
|
||||
/// </summary>
|
||||
private bool awaitingMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Index of the quote currently displayed, so the next pick can avoid repeating it.
|
||||
/// </summary>
|
||||
@@ -82,6 +97,12 @@ namespace Ashwild.UI
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the singleton, persists across the menu→game scene swap, and starts hidden.
|
||||
///
|
||||
/// Detaches to a root object before <see cref="Object.DontDestroyOnLoad"/>: that call is a
|
||||
/// no-op on a child, so a nested curtain would be destroyed the moment FishNet unloads the menu
|
||||
/// (ReplaceOption.All) — leaving the load uncovered and, later, no curtain at all to raise on
|
||||
/// the return to menu. Promoting it here keeps the scene authoring free to nest it under a
|
||||
/// layout object while still guaranteeing it survives every scene swap.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
@@ -91,6 +112,7 @@ namespace Ashwild.UI
|
||||
return;
|
||||
}
|
||||
instance = this;
|
||||
transform.SetParent(null, false);
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
@@ -107,6 +129,8 @@ namespace Ashwild.UI
|
||||
PlayerEvents.SessionStarting += HandleSessionBeginning;
|
||||
PlayerEvents.SessionJoining += HandleSessionJoining;
|
||||
PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned;
|
||||
PlayerEvents.ReturningToMenu += HandleReturningToMenu;
|
||||
PlayerEvents.MenuReady += HandleMenuReady;
|
||||
PlayerEvents.SessionError += HandleSessionError;
|
||||
PlayerEvents.SessionStopped += HandleSessionStopped;
|
||||
}
|
||||
@@ -119,6 +143,8 @@ namespace Ashwild.UI
|
||||
PlayerEvents.SessionStarting -= HandleSessionBeginning;
|
||||
PlayerEvents.SessionJoining -= HandleSessionJoining;
|
||||
PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned;
|
||||
PlayerEvents.ReturningToMenu -= HandleReturningToMenu;
|
||||
PlayerEvents.MenuReady -= HandleMenuReady;
|
||||
PlayerEvents.SessionError -= HandleSessionError;
|
||||
PlayerEvents.SessionStopped -= HandleSessionStopped;
|
||||
}
|
||||
@@ -140,12 +166,30 @@ namespace Ashwild.UI
|
||||
/// <summary>
|
||||
/// Raises the curtain when the local player starts hosting a session.
|
||||
/// </summary>
|
||||
private void HandleSessionBeginning() => Show();
|
||||
private void HandleSessionBeginning()
|
||||
{
|
||||
awaitingMenu = false;
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the curtain when the local player begins joining a remote session.
|
||||
/// </summary>
|
||||
private void HandleSessionJoining(string code) => Show();
|
||||
private void HandleSessionJoining(string code)
|
||||
{
|
||||
awaitingMenu = false;
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the curtain over a return-to-menu transition and arms the wait for MenuReady, so the
|
||||
/// upcoming SessionStopped (part of the teardown) does not drop it prematurely.
|
||||
/// </summary>
|
||||
private void HandleReturningToMenu()
|
||||
{
|
||||
awaitingMenu = true;
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lowers the curtain once the local player has spawned into the loaded game scene.
|
||||
@@ -153,14 +197,32 @@ namespace Ashwild.UI
|
||||
private void HandleLocalPlayerSpawned() => Hide();
|
||||
|
||||
/// <summary>
|
||||
/// Safety net: a connection failure must drop the curtain so the menu becomes usable again.
|
||||
/// Lowers the curtain once the menu scene has finished loading on a return-to-menu transition.
|
||||
/// </summary>
|
||||
private void HandleSessionError(string reason) => Hide();
|
||||
private void HandleMenuReady()
|
||||
{
|
||||
awaitingMenu = false;
|
||||
Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safety net: if the session tears down before a player spawns, never leave the curtain up.
|
||||
/// Safety net: a connection failure must drop the curtain so the menu becomes usable again.
|
||||
/// </summary>
|
||||
private void HandleSessionStopped() => Hide();
|
||||
private void HandleSessionError(string reason)
|
||||
{
|
||||
awaitingMenu = false;
|
||||
Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safety net for a session that tears down before a player spawns. Skipped while returning to
|
||||
/// the menu, where SessionStopped is expected and the curtain must stay up until MenuReady.
|
||||
/// </summary>
|
||||
private void HandleSessionStopped()
|
||||
{
|
||||
if (awaitingMenu) return;
|
||||
Hide();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -169,6 +231,12 @@ namespace Ashwild.UI
|
||||
/// <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.
|
||||
///
|
||||
/// Raises <see cref="PlayerEvents.LoadingCurtainShown"/> only once the fade-in has fully
|
||||
/// completed: NetworkSessionManager waits for that signal before kicking off the heavy game
|
||||
/// scene load, so the unavoidable activation freeze always happens behind an already-opaque
|
||||
/// curtain instead of stalling the very frame the curtain was meant to appear on. Killing the
|
||||
/// tween early (an instant Hide) suppresses the signal, which is the intended behaviour.
|
||||
/// </summary>
|
||||
private void Show()
|
||||
{
|
||||
@@ -182,15 +250,18 @@ namespace Ashwild.UI
|
||||
|
||||
curtainTween = canvasGroup.DOFade(1f, fadeInDuration)
|
||||
.SetEase(fadeInEase)
|
||||
.SetUpdate(true);
|
||||
.SetUpdate(true)
|
||||
.OnComplete(PlayerEvents.RaiseLoadingCurtainShown);
|
||||
|
||||
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.
|
||||
/// Fades the curtain out and stops the quote cycle. The fade is delayed by the larger of the
|
||||
/// remaining minimum display time (so an instant load still shows the curtain for a readable
|
||||
/// beat instead of flashing) and the settle delay (so the scene's first heavy frames after the
|
||||
/// ready signal happen behind the curtain rather than stuttering on screen).
|
||||
/// </summary>
|
||||
private void Hide()
|
||||
{
|
||||
@@ -198,7 +269,7 @@ namespace Ashwild.UI
|
||||
isShown = false;
|
||||
|
||||
float elapsed = Time.unscaledTime - shownAt;
|
||||
float delay = Mathf.Max(0f, minimumDisplayTime - elapsed);
|
||||
float delay = Mathf.Max(settleDelay, minimumDisplayTime - elapsed);
|
||||
|
||||
curtainTween?.Kill();
|
||||
curtainTween = canvasGroup.DOFade(0f, fadeOutDuration)
|
||||
|
||||
@@ -1,58 +1,171 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.Video;
|
||||
using UnityEngine.SceneManagement;
|
||||
using DG.Tweening;
|
||||
|
||||
namespace Ashwild.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Plays a full-screen splash video at launch, then fades out to reveal the menu underneath.
|
||||
/// Decoupled from UIManager: it lives on its own high-sorting-order Canvas and simply covers
|
||||
/// the menu until the clip ends.
|
||||
/// Boot splash: plays a full-screen video at launch while loading the menu scene additively in
|
||||
/// the background, then hands off to it. It lives on its own high-sorting-order Canvas in a tiny
|
||||
/// bootstrap scene (0_Init) so the menu — and the persistent objects it carries (NetworkManager,
|
||||
/// NetworkSessionManager, LoadingScreen) — initialize hidden beneath the splash. Once BOTH the
|
||||
/// video has ended AND the menu scene has finished loading, it fades out, makes the menu the
|
||||
/// active scene, and unloads its own bootstrap scene.
|
||||
///
|
||||
/// Loading the menu in parallel (not after) means the splash playback time doubles as the menu's
|
||||
/// loading screen, so the swap is seamless with no black flash. When <see cref="menuSceneName"/>
|
||||
/// is left empty the controller falls back to legacy overlay mode: it simply fades out to reveal
|
||||
/// a menu already present in the same scene, and never unloads anything.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class SplashScreenController : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private VideoPlayer videoPlayer;
|
||||
[SerializeField] private RawImage videoImage;
|
||||
[SerializeField] private RenderTexture renderTexture;
|
||||
|
||||
[Header("Boot")]
|
||||
[Tooltip("Menu scene to load additively while the splash plays, then hand off to. Leave empty " +
|
||||
"for legacy overlay mode (fade out to reveal a menu already in this scene).")]
|
||||
[SerializeField] private string menuSceneName = "MenuScene";
|
||||
|
||||
[Header("Fade Out")]
|
||||
[SerializeField] private float fadeOutDuration = 0.5f;
|
||||
[SerializeField] private Ease fadeOutEase = Ease.InQuad;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private CanvasGroup canvasGroup;
|
||||
|
||||
/// <summary>
|
||||
/// True once the handoff (fade out + unload) has begun, so it can never run twice.
|
||||
/// </summary>
|
||||
private bool finished;
|
||||
|
||||
/// <summary>
|
||||
/// True once the splash video has ended (or was skipped because none is assigned).
|
||||
/// </summary>
|
||||
private bool videoFinished;
|
||||
|
||||
/// <summary>
|
||||
/// True once the menu scene has finished loading (or immediately in legacy overlay mode).
|
||||
/// </summary>
|
||||
private bool menuReady;
|
||||
|
||||
private Tween fadeTween;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Caches the CanvasGroup and covers the screen immediately so whatever loads underneath
|
||||
/// (the menu) initializes hidden.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
|
||||
// Cover everything immediately so the menu initializes hidden underneath.
|
||||
canvasGroup.alpha = 1f;
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
canvasGroup.interactable = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the video end event.
|
||||
/// </summary>
|
||||
private void OnEnable()
|
||||
{
|
||||
if (videoPlayer != null)
|
||||
videoPlayer.loopPointReached += OnVideoFinished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes — mirrors OnEnable exactly.
|
||||
/// </summary>
|
||||
private void OnDisable()
|
||||
{
|
||||
if (videoPlayer != null)
|
||||
videoPlayer.loopPointReached -= OnVideoFinished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kicks off the background menu load and starts the splash video in parallel.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
BeginMenuLoad();
|
||||
BeginVideo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the fade tween so it never animates a destroyed CanvasGroup on teardown.
|
||||
/// </summary>
|
||||
private void OnDestroy() => fadeTween?.Kill();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Menu Load
|
||||
|
||||
/// <summary>
|
||||
/// Starts loading the menu scene additively. Overlay mode (no scene name, or the splash already
|
||||
/// lives in the menu scene) marks the menu ready at once and never loads/unloads anything — this
|
||||
/// also guards against a misconfiguration where the splash sits in the menu scene yet names it,
|
||||
/// which would otherwise make the handoff unload the menu out from under itself. An already-loaded
|
||||
/// target is likewise treated as ready, guarding against a double-load.
|
||||
/// </summary>
|
||||
private void BeginMenuLoad()
|
||||
{
|
||||
if (IsOverlayMode())
|
||||
{
|
||||
menuReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (SceneManager.GetSceneByName(menuSceneName).isLoaded)
|
||||
{
|
||||
menuReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncOperation op = SceneManager.LoadSceneAsync(menuSceneName, LoadSceneMode.Additive);
|
||||
if (op == null)
|
||||
{
|
||||
Debug.LogError($"[Splash] Could not start loading menu scene '{menuSceneName}' — is it in Build Settings?", this);
|
||||
menuReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
op.completed += _ =>
|
||||
{
|
||||
menuReady = true;
|
||||
TryHandoff();
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Video
|
||||
|
||||
/// <summary>
|
||||
/// Prepares and plays the splash video, preparing first to avoid a black flash on the first
|
||||
/// frame. When no player/clip is assigned the video step is skipped so the handoff still runs.
|
||||
/// </summary>
|
||||
private void BeginVideo()
|
||||
{
|
||||
if (videoPlayer == null || videoPlayer.clip == null)
|
||||
{
|
||||
Debug.LogWarning("SplashScreenController: no VideoPlayer/clip assigned, skipping splash.");
|
||||
FadeOut();
|
||||
Debug.LogWarning("[Splash] No VideoPlayer/clip assigned — skipping splash video.", this);
|
||||
videoFinished = true;
|
||||
TryHandoff();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,11 +173,13 @@ namespace Ashwild.UI
|
||||
videoPlayer.renderMode = VideoRenderMode.RenderTexture;
|
||||
videoPlayer.targetTexture = renderTexture;
|
||||
|
||||
// Prepare first to avoid a black flash / stutter on the first frame.
|
||||
videoPlayer.prepareCompleted += OnPrepared;
|
||||
videoPlayer.Prepare();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays the clip once it is prepared.
|
||||
/// </summary>
|
||||
private void OnPrepared(VideoPlayer vp)
|
||||
{
|
||||
vp.prepareCompleted -= OnPrepared;
|
||||
@@ -73,23 +188,75 @@ namespace Ashwild.UI
|
||||
vp.Play();
|
||||
}
|
||||
|
||||
private void OnVideoFinished(VideoPlayer vp) => FadeOut();
|
||||
/// <summary>
|
||||
/// Marks the video done and attempts the handoff.
|
||||
/// </summary>
|
||||
private void OnVideoFinished(VideoPlayer vp)
|
||||
{
|
||||
videoFinished = true;
|
||||
TryHandoff();
|
||||
}
|
||||
|
||||
private void FadeOut()
|
||||
#endregion
|
||||
|
||||
#region Handoff
|
||||
|
||||
/// <summary>
|
||||
/// Fades out and hands off only once both the video has ended and the menu is loaded, so the
|
||||
/// splash never cuts the menu's load short nor lingers after the clip.
|
||||
/// </summary>
|
||||
private void TryHandoff()
|
||||
{
|
||||
if (finished) return;
|
||||
if (!videoFinished || !menuReady) return;
|
||||
FadeOutAndHandoff();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fades the curtain out, then makes the menu the active scene and unloads this bootstrap
|
||||
/// scene (in overlay mode it just deactivates itself, leaving the surrounding scene intact).
|
||||
/// </summary>
|
||||
private void FadeOutAndHandoff()
|
||||
{
|
||||
finished = true;
|
||||
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
canvasGroup.interactable = false;
|
||||
|
||||
canvasGroup.DOFade(0f, fadeOutDuration)
|
||||
fadeTween = canvasGroup.DOFade(0f, fadeOutDuration)
|
||||
.SetEase(fadeOutEase)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
if (videoPlayer != null) videoPlayer.Stop();
|
||||
gameObject.SetActive(false);
|
||||
});
|
||||
.OnComplete(CompleteHandoff);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the video and either unloads this bootstrap scene (after promoting the menu to the
|
||||
/// active scene) or simply deactivates the overlay when running in legacy overlay mode.
|
||||
/// </summary>
|
||||
private void CompleteHandoff()
|
||||
{
|
||||
if (videoPlayer != null) videoPlayer.Stop();
|
||||
|
||||
if (IsOverlayMode() || !SceneManager.GetSceneByName(menuSceneName).isLoaded)
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
SceneManager.SetActiveScene(SceneManager.GetSceneByName(menuSceneName));
|
||||
SceneManager.UnloadSceneAsync(gameObject.scene);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when there is no separate menu scene to hand off to: either no name is set, or the
|
||||
/// splash itself lives in the named scene. In that case the splash is just an overlay above an
|
||||
/// already-present menu and must never unload its own scene.
|
||||
/// </summary>
|
||||
private bool IsOverlayMode()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(menuSceneName)
|
||||
|| gameObject.scene.name == menuSceneName;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user