Revamp the prompt system, the bootstrapper system, the starting cinematic

This commit is contained in:
Michal Pikulski
2025-10-16 19:43:19 +02:00
parent df604fbc03
commit 50448c5bd3
89 changed files with 3964 additions and 677 deletions

View File

@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
namespace Bootstrap
{
/// <summary>
/// Service that provides notification and management of boot completion status.
/// Allows systems to subscribe to boot completion events, register initialization actions with priorities,
/// or await boot completion asynchronously.
/// </summary>
public static class BootCompletionService
{
/// <summary>
/// Indicates if the boot process has completed
/// </summary>
public static bool IsBootComplete { get; private set; } = false;
/// <summary>
/// Event triggered when boot completes
/// </summary>
public static event Action OnBootComplete;
/// <summary>
/// Represents an initialization action with priority
/// </summary>
private class InitializationAction
{
public Action Action { get; }
public int Priority { get; }
public string Name { get; }
public InitializationAction(Action action, int priority, string name)
{
Action = action;
Priority = priority;
Name = name;
}
}
// List of initialization actions to be executed once boot completes
private static List<InitializationAction> _initializationActions = new List<InitializationAction>();
// TaskCompletionSource for async await pattern
private static TaskCompletionSource<bool> _bootCompletionTask = new TaskCompletionSource<bool>();
/// <summary>
/// Called by CustomBoot when the boot process is complete
/// </summary>
internal static void HandleBootCompleted()
{
if (IsBootComplete)
return;
IsBootComplete = true;
Debug.Log("[BootCompletionService] Boot process completed, executing initialization actions");
// Execute initialization actions in priority order (lower number = higher priority)
ExecuteInitializationActions();
// Trigger the event
OnBootComplete?.Invoke();
// Complete the task for async waiters
_bootCompletionTask.TrySetResult(true);
Debug.Log("[BootCompletionService] All boot completion handlers executed");
}
/// <summary>
/// Register an action to be executed when boot completes.
/// Lower priority numbers run first.
/// </summary>
/// <param name="action">The action to execute</param>
/// <param name="priority">Priority (lower numbers run first)</param>
/// <param name="name">Name for debugging</param>
public static void RegisterInitAction(Action action, int priority = 100, string name = null)
{
if (action == null)
return;
if (string.IsNullOrEmpty(name))
name = $"Action_{_initializationActions.Count}";
var initAction = new InitializationAction(action, priority, name);
if (IsBootComplete)
{
// If boot is already complete, execute immediately
Debug.Log($"[BootCompletionService] Executing late registration: {name} (Priority: {priority})");
try
{
action();
}
catch (Exception ex)
{
Debug.LogError($"[BootCompletionService] Error executing init action '{name}': {ex}");
}
}
else
{
// Otherwise add to the queue
_initializationActions.Add(initAction);
Debug.Log($"[BootCompletionService] Registered init action: {name} (Priority: {priority})");
}
}
/// <summary>
/// Wait asynchronously for boot completion
/// </summary>
/// <returns>Task that completes when boot is complete</returns>
public static Task WaitForBootCompletionAsync()
{
if (IsBootComplete)
return Task.CompletedTask;
return _bootCompletionTask.Task;
}
/// <summary>
/// Execute all registered initialization actions in priority order
/// </summary>
private static void ExecuteInitializationActions()
{
// Sort by priority (lowest first)
var sortedActions = _initializationActions
.OrderBy(a => a.Priority)
.ToList();
foreach (var action in sortedActions)
{
try
{
Debug.Log($"[BootCompletionService] Executing: {action.Name} (Priority: {action.Priority})");
action.Action();
}
catch (Exception ex)
{
Debug.LogError($"[BootCompletionService] Error executing init action '{action.Name}': {ex}");
}
}
// Clear the list after execution
_initializationActions.Clear();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aa0228cf33a64515bc166b7a9bc8c0b9
timeCreated: 1760606319

View File

@@ -0,0 +1,269 @@
using System;
using UnityEngine;
using UI;
using Core;
using UnityEngine.SceneManagement;
using Cinematics;
namespace Bootstrap
{
/// <summary>
/// Controller for the boot scene that coordinates bootstrap initialization with loading screen
/// </summary>
public class BootSceneController : MonoBehaviour
{
[SerializeField] private string mainMenuSceneName = "MainMenu";
[SerializeField] private float minDelayAfterBoot = 0.5f; // Small delay after boot to ensure smooth transition
[SerializeField] private bool debugMode = false;
[SerializeField] private InitialLoadingScreen initialLoadingScreen; // Reference to our specialized loading screen
// Progress distribution between bootstrap and scene loading
[SerializeField, Range(0.1f, 0.9f)] private float bootProgressWeight = 0.5f; // Default 50/50 split
private enum LoadingPhase { Bootstrap, SceneLoading }
private LoadingPhase _currentPhase = LoadingPhase.Bootstrap;
private bool _bootComplete = false;
private bool _hasStartedLoading = false;
private float _sceneLoadingProgress = 0f;
private void Start()
{
Debug.Log("[BootSceneController] Boot scene started");
// Ensure the initial loading screen exists
if (initialLoadingScreen == null)
{
Debug.LogError("[BootSceneController] No InitialLoadingScreen assigned! Please assign it in the inspector.");
return;
}
// Subscribe to the loading screen completion event
initialLoadingScreen.OnLoadingScreenFullyHidden += OnInitialLoadingComplete;
// Show the loading screen immediately with our combined progress provider
initialLoadingScreen.ShowLoadingScreen(GetCombinedProgress);
// Subscribe to boot progress events
CustomBoot.OnBootProgressChanged += OnBootProgressChanged;
// Register our boot completion handler with the BootCompletionService
// This will execute either immediately if boot is already complete,
// or when the boot process completes
BootCompletionService.RegisterInitAction(
OnBootCompleted,
50, // Higher priority (lower number)
"BootSceneController.OnBootCompleted"
);
// In debug mode, log additional information
if (debugMode)
{
InvokeRepeating(nameof(LogDebugInfo), 0.1f, 0.5f);
}
}
/// <summary>
/// Called when the initial loading screen is fully hidden
/// </summary>
private void OnInitialLoadingComplete()
{
Debug.Log("[BootSceneController] Initial loading screen fully hidden, boot sequence completed");
// Play the intro cinematic if available
if (CinematicsManager.Instance != null)
{
Debug.Log("[BootSceneController] Attempting to play intro cinematic");
// Use LoadAndPlayCinematic to play the intro sequence
CinematicsManager.Instance.LoadAndPlayCinematic("IntroSequence");
// Immediately unload the StartingScene - no need to wait for cinematic to finish
// since CinematicsManager is bootstrapped and won't be unloaded
UnloadStartingScene();
}
else
{
// If no cinematics manager, unload the StartingScene directly
UnloadStartingScene();
}
}
private void OnDestroy()
{
// Clean up event subscriptions
CustomBoot.OnBootCompleted -= OnBootCompleted;
CustomBoot.OnBootProgressChanged -= OnBootProgressChanged;
if (initialLoadingScreen != null)
{
initialLoadingScreen.OnLoadingScreenFullyHidden -= OnInitialLoadingComplete;
}
if (debugMode)
{
CancelInvoke(nameof(LogDebugInfo));
}
}
/// <summary>
/// Progress provider that combines bootstrap and scene loading progress
/// </summary>
private float GetCombinedProgress()
{
switch (_currentPhase)
{
case LoadingPhase.Bootstrap:
// Scale bootstrap progress from 0 to bootProgressWeight
return CustomBoot.CurrentProgress * bootProgressWeight;
case LoadingPhase.SceneLoading:
// Scale scene loading progress from bootProgressWeight to 1.0
return bootProgressWeight + (_sceneLoadingProgress * (1f - bootProgressWeight));
default:
return 0f;
}
}
private void OnBootProgressChanged(float progress)
{
if (debugMode)
{
Debug.Log($"[BootSceneController] Bootstrap progress: {progress:P0}, Combined: {GetCombinedProgress():P0}");
}
}
private void LogDebugInfo()
{
Debug.Log($"[BootSceneController] Debug - Phase: {_currentPhase}, Bootstrap: {CustomBoot.CurrentProgress:P0}, " +
$"Scene: {_sceneLoadingProgress:P0}, Combined: {GetCombinedProgress():P0}, Boot Complete: {_bootComplete}");
}
private void OnBootCompleted()
{
// Unsubscribe to prevent duplicate calls
CustomBoot.OnBootCompleted -= OnBootCompleted;
Debug.Log("[BootSceneController] Boot process completed");
_bootComplete = true;
// After a small delay, start loading the main menu
// This prevents jerky transitions if boot happens very quickly
Invoke(nameof(StartLoadingMainMenu), minDelayAfterBoot);
}
private void StartLoadingMainMenu()
{
if (_hasStartedLoading)
return;
_hasStartedLoading = true;
_currentPhase = LoadingPhase.SceneLoading;
LoadMainMenu();
}
private async void LoadMainMenu()
{
Debug.Log($"[BootSceneController] Loading main menu scene: {mainMenuSceneName}");
try
{
// Initialize scene loading progress to 0 to ensure proper remapping
_sceneLoadingProgress = 0f;
// Create a custom progress reporter using a custom class
var progressHandler = new ProgressHandler(value => {
// Store the raw scene loading progress (0-1)
_sceneLoadingProgress = value;
if (debugMode)
{
Debug.Log($"[BootSceneController] Scene loading raw: {value:P0}, Combined: {GetCombinedProgress():P0}");
}
});
// Step 1: Additively load the main menu scene - don't unload StartingScene yet
var op = SceneManager.LoadSceneAsync(mainMenuSceneName, LoadSceneMode.Additive);
// Disable scene activation until we're ready to show it
op.allowSceneActivation = true;
// Track progress while loading
while (!op.isDone)
{
progressHandler.ReportProgress(op.progress);
await System.Threading.Tasks.Task.Yield();
}
// Update the current gameplay scene in SceneManagerService
SceneManagerService.Instance.CurrentGameplayScene = mainMenuSceneName;
// Ensure progress is complete
_sceneLoadingProgress = 1f;
// Step 2: Scene is fully loaded, now hide the loading screen
// This will trigger OnInitialLoadingComplete via the event when animation completes
initialLoadingScreen.HideLoadingScreen();
// Step 3: The OnInitialLoadingComplete method will handle playing the intro cinematic
// Step 4: StartingScene will be unloaded after the cinematic completes in OnIntroCinematicFinished
}
catch (Exception e)
{
Debug.LogError($"[BootSceneController] Error loading main menu: {e.Message}");
// Still try to hide the loading screen even if there was an error
initialLoadingScreen.HideLoadingScreen();
}
}
/// <summary>
/// Unloads the StartingScene, completing the transition to the main menu
/// </summary>
private async void UnloadStartingScene()
{
try
{
// Get the current scene (StartingScene)
Scene currentScene = SceneManager.GetActiveScene();
string startingSceneName = currentScene.name;
Debug.Log($"[BootSceneController] Unloading StartingScene: {startingSceneName}");
// Unload the StartingScene
await SceneManager.UnloadSceneAsync(startingSceneName);
// Set the main menu scene as the active scene
Scene mainMenuScene = SceneManager.GetSceneByName(mainMenuSceneName);
SceneManager.SetActiveScene(mainMenuScene);
Debug.Log($"[BootSceneController] Transition complete: {startingSceneName} unloaded, {mainMenuSceneName} is now active");
// Destroy the boot scene controller since its job is done
Destroy(gameObject);
}
catch (Exception e)
{
Logging.Warning($"[BootSceneController] Error unloading StartingScene: {e.Message}");
}
}
/// <summary>
/// Helper class to handle progress reporting without running into explicit interface implementation issues
/// </summary>
private class ProgressHandler
{
private Action<float> _progressAction;
public ProgressHandler(Action<float> progressAction)
{
_progressAction = progressAction;
}
public void ReportProgress(float value)
{
_progressAction?.Invoke(value);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fdb797d6fcdc469bb9bfb9ad3c5f51b5
timeCreated: 1760604860

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
@@ -14,9 +15,24 @@ namespace Bootstrap
/// Current initialisation status
/// </summary>
public static bool Initialised { get; private set; }
/// <summary>
/// Event triggered when boot progress changes
/// </summary>
public static event Action<float> OnBootProgressChanged;
/// <summary>
/// Event triggered when boot process completes
/// </summary>
public static event Action OnBootCompleted;
/// <summary>
/// Current progress of the boot process (0-1)
/// </summary>
public static float CurrentProgress { get; private set; }
/// <summary>
// Called as soon as the game begins
/// Called as soon as the game begins
/// </summary>
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
private static void Initialise()
@@ -32,6 +48,9 @@ namespace Bootstrap
/// </summary>
public static void PerformInitialisation()
{
//Reset progress
CurrentProgress = 0f;
//In editor, perform initialisation synchronously
if (Application.isEditor)
{
@@ -72,6 +91,17 @@ namespace Bootstrap
{
await LoadCustomBootSettings();
Initialised = true;
CurrentProgress = 1f;
OnBootProgressChanged?.Invoke(1f);
OnBootCompleted?.Invoke();
// Notify the BootCompletionService that boot is complete
if (Application.isPlaying)
{
// Direct call to boot completion service
Debug.Log("[CustomBoot] Calling BootCompletionService.HandleBootCompleted()");
BootCompletionService.HandleBootCompleted();
}
}
/// <summary>
@@ -81,6 +111,17 @@ namespace Bootstrap
{
LoadCustomBootSettingsSync();
Initialised = true;
CurrentProgress = 1f;
OnBootProgressChanged?.Invoke(1f);
OnBootCompleted?.Invoke();
// Notify the BootCompletionService that boot is complete
if (Application.isPlaying)
{
// Direct call to boot completion service
Debug.Log("[CustomBoot] Calling BootCompletionService.HandleBootCompleted()");
BootCompletionService.HandleBootCompleted();
}
}
@@ -177,5 +218,16 @@ namespace Bootstrap
result.InitialiseSync();
return handle;
}
/// <summary>
/// Updates the current progress value and triggers the progress event
/// </summary>
/// <param name="progress">Progress value between 0-1</param>
internal static void UpdateProgress(float progress)
{
CurrentProgress = Mathf.Clamp01(progress);
OnBootProgressChanged?.Invoke(CurrentProgress);
Debug.Log($"[CustomBoot] Progress: {CurrentProgress:P0}");
}
}
}

View File

@@ -31,15 +31,37 @@ namespace Bootstrap
RuntimeContainer = new GameObject($"{name}_Container");
DontDestroyOnLoad(RuntimeContainer);
Instances = new GameObject[BootPrefabs.Length];
// Calculate total prefabs for progress tracking
int totalPrefabs = BootPrefabs.Length;
float progressPerPrefab = totalPrefabs > 0 ? 1f / totalPrefabs : 1f;
float currentProgress = 0f;
for (var i = 0; i < BootPrefabs.Length; i++)
{
if (!BootPrefabs[i]) continue;
if (!BootPrefabs[i])
{
// Report incremental progress even for null prefabs
currentProgress = (i + 1) * progressPerPrefab;
CustomBoot.UpdateProgress(currentProgress);
continue;
}
var instance = GameObject.InstantiateAsync(BootPrefabs[i], RuntimeContainer.transform);
while (!instance.isDone)
{
// Report partial progress while waiting
float progressInStep = instance.progress * progressPerPrefab;
float overallProgress = i * progressPerPrefab + progressInStep;
CustomBoot.UpdateProgress(overallProgress);
await Task.Yield();
}
Instances[i] = instance.Result[0];
// Report completion of this step
currentProgress = (i + 1) * progressPerPrefab;
CustomBoot.UpdateProgress(currentProgress);
}
}
@@ -55,12 +77,28 @@ namespace Bootstrap
}
Instances = new GameObject[BootPrefabs.Length];
// Calculate total prefabs for progress tracking
int totalPrefabs = BootPrefabs.Length;
float progressPerPrefab = totalPrefabs > 0 ? 1f / totalPrefabs : 1f;
float currentProgress = 0f;
for (var i = 0; i < BootPrefabs.Length; i++)
{
if (!BootPrefabs[i]) continue;
if (!BootPrefabs[i])
{
// Report incremental progress even for null prefabs
currentProgress = (i + 1) * progressPerPrefab;
CustomBoot.UpdateProgress(currentProgress);
continue;
}
var instance = GameObject.Instantiate(BootPrefabs[i], RuntimeContainer.transform);
Instances[i] = instance;
// Report completion of this step
currentProgress = (i + 1) * progressPerPrefab;
CustomBoot.UpdateProgress(currentProgress);
}
}

View File

@@ -0,0 +1,240 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using Core;
namespace Bootstrap
{
/// <summary>
/// Specialized loading screen controller specifically for the initial boot sequence.
/// This handles the combined progress of bootstrap initialization and main menu loading.
/// </summary>
public class InitialLoadingScreen : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject loadingScreenContainer;
[SerializeField] private Image progressBarImage;
[Header("Settings")]
[SerializeField] private float minimumDisplayTime = 1.0f;
[SerializeField] private float progressUpdateInterval = 0.1f;
private float _displayStartTime;
private Coroutine _progressCoroutine;
private bool _loadingComplete = false;
private bool _animationComplete = false;
private Action _onLoadingScreenFullyHidden;
/// <summary>
/// Event that fires when the loading screen is fully hidden (both loading and animation completed)
/// </summary>
public event Action OnLoadingScreenFullyHidden;
/// <summary>
/// Delegate for providing progress values from different sources
/// </summary>
public delegate float ProgressProvider();
/// <summary>
/// Current progress provider being used for the loading screen
/// </summary>
private ProgressProvider _currentProgressProvider;
/// <summary>
/// Default progress provider that returns 0 (or 1 if loading is complete)
/// </summary>
private float DefaultProgressProvider() => _loadingComplete ? 1f : 0f;
/// <summary>
/// Check if the loading screen is currently active
/// </summary>
public bool IsActive => loadingScreenContainer != null && loadingScreenContainer.activeSelf;
private void Awake()
{
if (loadingScreenContainer == null)
loadingScreenContainer = gameObject;
// Ensure the loading screen is initially hidden
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(false);
}
}
/// <summary>
/// Shows the loading screen and resets the progress bar to zero
/// </summary>
/// <param name="progressProvider">Optional delegate to provide progress values (0-1). If null, uses default provider.</param>
/// <param name="onComplete">Optional callback when loading screen is fully hidden</param>
public void ShowLoadingScreen(ProgressProvider progressProvider = null, Action onComplete = null)
{
// Store the completion callback
_onLoadingScreenFullyHidden = onComplete;
// Set the progress provider, use default if none provided
_currentProgressProvider = progressProvider ?? DefaultProgressProvider;
// Stop any existing progress coroutine
if (_progressCoroutine != null)
{
StopCoroutine(_progressCoroutine);
_progressCoroutine = null;
}
_displayStartTime = Time.time;
_loadingComplete = false;
_animationComplete = false;
if (progressBarImage != null)
{
progressBarImage.fillAmount = 0f;
}
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(true);
}
// Start the progress filling coroutine
_progressCoroutine = StartCoroutine(AnimateProgressBar());
}
/// <summary>
/// Animates the progress bar at a steady pace over the minimum display time,
/// while also checking actual loading progress from the current progress provider
/// </summary>
private IEnumerator AnimateProgressBar()
{
float startTime = Time.time;
// Continue until both animation and loading are complete
while (!_animationComplete)
{
// Calculate the steady progress based on elapsed time
float elapsedTime = Time.time - startTime;
float steadyProgress = Mathf.Clamp01(elapsedTime / minimumDisplayTime);
// Get the actual loading progress from the current provider
float actualProgress = _currentProgressProvider();
// If loading is complete, actualProgress should be 1.0
if (_loadingComplete)
{
actualProgress = 1.0f;
}
// Use the minimum of steady progress and actual progress
// This ensures we don't show more progress than actual loading
float displayProgress = Mathf.Min(steadyProgress, actualProgress);
// Log the progress values for debugging
Debug.Log($"[InitialLoadingScreen] Progress - Default: {steadyProgress:F2}, Actual: {actualProgress:F2}, Display: {displayProgress:F2}");
// Directly set the progress bar fill amount without smoothing
if (progressBarImage != null)
{
progressBarImage.fillAmount = displayProgress;
}
// Check if the animation has completed
// Animation is complete when we've reached the minimum display time AND we're at 100% progress
if (steadyProgress >= 1.0f && displayProgress >= 1.0f)
{
_animationComplete = true;
Debug.Log("[InitialLoadingScreen] Animation complete");
break;
}
// Wait for the configured interval before updating again
yield return new WaitForSeconds(progressUpdateInterval);
}
// Ensure we end at 100% progress
if (progressBarImage != null)
{
progressBarImage.fillAmount = 1.0f;
Debug.Log("[InitialLoadingScreen] Final progress set to 1.0");
}
// Hide the screen if loading is also complete
if (_loadingComplete)
{
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(false);
Debug.Log("[InitialLoadingScreen] Animation AND loading complete, hiding screen");
// Invoke the callback when fully hidden
_onLoadingScreenFullyHidden?.Invoke();
OnLoadingScreenFullyHidden?.Invoke();
_onLoadingScreenFullyHidden = null;
}
}
_progressCoroutine = null;
}
/// <summary>
/// Called when the actual loading process is complete
/// </summary>
public void HideLoadingScreen()
{
Debug.Log("[InitialLoadingScreen] Loading complete, marking loading as finished");
// Mark that loading is complete
_loadingComplete = true;
// If animation is already complete, we can hide the screen now
if (_animationComplete)
{
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(false);
Debug.Log("[InitialLoadingScreen] Animation already complete, hiding screen immediately");
// Invoke the callback when fully hidden
_onLoadingScreenFullyHidden?.Invoke();
OnLoadingScreenFullyHidden?.Invoke();
_onLoadingScreenFullyHidden = null;
}
}
else
{
Debug.Log("[InitialLoadingScreen] Animation still in progress, waiting for it to complete");
// The coroutine will handle hiding when animation completes
}
}
/// <summary>
/// Waits until the loading screen is fully hidden before continuing
/// </summary>
/// <returns>Task that completes when the loading screen is hidden</returns>
public System.Threading.Tasks.Task WaitForLoadingScreenToHideAsync()
{
var tcs = new System.Threading.Tasks.TaskCompletionSource<bool>();
// If the loading screen is not active, complete immediately
if (!IsActive)
{
tcs.SetResult(true);
return tcs.Task;
}
// Store existing callback to chain it
Action existingCallback = _onLoadingScreenFullyHidden;
// Set new callback
_onLoadingScreenFullyHidden = () => {
// Call existing callback if any
existingCallback?.Invoke();
// Complete the task
tcs.SetResult(true);
};
return tcs.Task;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8968b564891a474baae157792b88e75f
timeCreated: 1760613320