using UnityEngine; using UnityEngine.UI; using UnityEngine.Video; using UnityEngine.SceneManagement; using DG.Tweening; namespace Ashwild.UI { /// /// 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 /// 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. /// [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; /// /// True once the handoff (fade out + unload) has begun, so it can never run twice. /// private bool finished; /// /// True once the splash video has ended (or was skipped because none is assigned). /// private bool videoFinished; /// /// True once the menu scene has finished loading (or immediately in legacy overlay mode). /// private bool menuReady; private Tween fadeTween; #endregion #region Unity Lifecycle /// /// Caches the CanvasGroup and covers the screen immediately so whatever loads underneath /// (the menu) initializes hidden. /// private void Awake() { canvasGroup = GetComponent(); canvasGroup.alpha = 1f; canvasGroup.blocksRaycasts = true; canvasGroup.interactable = true; } /// /// Subscribes to the video end event. /// private void OnEnable() { if (videoPlayer != null) videoPlayer.loopPointReached += OnVideoFinished; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { if (videoPlayer != null) videoPlayer.loopPointReached -= OnVideoFinished; } /// /// Kicks off the background menu load and starts the splash video in parallel. /// private void Start() { BeginMenuLoad(); BeginVideo(); } /// /// Kills the fade tween so it never animates a destroyed CanvasGroup on teardown. /// private void OnDestroy() => fadeTween?.Kill(); #endregion #region Menu Load /// /// 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. /// 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 /// /// 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. /// private void BeginVideo() { if (videoPlayer == null || videoPlayer.clip == null) { Debug.LogWarning("[Splash] No VideoPlayer/clip assigned — skipping splash video.", this); videoFinished = true; TryHandoff(); return; } videoPlayer.isLooping = false; videoPlayer.renderMode = VideoRenderMode.RenderTexture; videoPlayer.targetTexture = renderTexture; videoPlayer.prepareCompleted += OnPrepared; videoPlayer.Prepare(); } /// /// Plays the clip once it is prepared. /// private void OnPrepared(VideoPlayer vp) { vp.prepareCompleted -= OnPrepared; if (renderTexture != null) renderTexture.DiscardContents(); vp.Play(); } /// /// Marks the video done and attempts the handoff. /// private void OnVideoFinished(VideoPlayer vp) { videoFinished = true; TryHandoff(); } #endregion #region Handoff /// /// 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. /// private void TryHandoff() { if (finished) return; if (!videoFinished || !menuReady) return; FadeOutAndHandoff(); } /// /// 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). /// private void FadeOutAndHandoff() { finished = true; canvasGroup.blocksRaycasts = false; canvasGroup.interactable = false; fadeTween = canvasGroup.DOFade(0f, fadeOutDuration) .SetEase(fadeOutEase) .OnComplete(CompleteHandoff); } /// /// 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. /// 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); } /// /// 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. /// private bool IsOverlayMode() { return string.IsNullOrWhiteSpace(menuSceneName) || gameObject.scene.name == menuSceneName; } #endregion } }