263 lines
8.9 KiB
C#
263 lines
8.9 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.Video;
|
|
using UnityEngine.SceneManagement;
|
|
using DG.Tweening;
|
|
|
|
namespace Ashwild.UI
|
|
{
|
|
/// <summary>
|
|
/// 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>();
|
|
|
|
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("[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plays the clip once it is prepared.
|
|
/// </summary>
|
|
private void OnPrepared(VideoPlayer vp)
|
|
{
|
|
vp.prepareCompleted -= OnPrepared;
|
|
if (renderTexture != null)
|
|
renderTexture.DiscardContents();
|
|
vp.Play();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Marks the video done and attempts the handoff.
|
|
/// </summary>
|
|
private void OnVideoFinished(VideoPlayer vp)
|
|
{
|
|
videoFinished = true;
|
|
TryHandoff();
|
|
}
|
|
|
|
#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;
|
|
|
|
fadeTween = canvasGroup.DOFade(0f, fadeOutDuration)
|
|
.SetEase(fadeOutEase)
|
|
.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
|
|
}
|
|
}
|