Files
AppleHillsProduction/Assets/Scripts/UI/LoadingScreenController.cs
tschesky e27bb7bfb6 Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51)
# Lifecycle Management & Save System Revamp

## Overview
Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems.

## Core Architecture

### New Lifecycle System
- **`LifecycleManager`**: Centralized coordinator for all managed objects
- **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns
  - `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier)
  - `OnSceneReady()`: Scene-specific setup after managers ready
  - Replaces `BootCompletionService` (deleted)
- **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100)
- **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode

### Unified SaveID System
- Consistent format: `{ParentName}_{ComponentType}`
- Auto-registration via `AutoRegisterForSave = true`
- New `DebugSaveIds` editor tool for inspection

## Save/Load Improvements

### Enhanced State Management
- **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy
- **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring
- **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state
- **Fixed race conditions**: Proper initialization ordering prevents data corruption

## Interactable & Pickup System

- Migrated to `OnManagedAwake()` for consistent initialization
- Template method pattern for state restoration (`RestoreInteractionState()`)
- Fixed combination item save/load bugs (items in slots vs. follower hand)
- Dynamic spawning support for combined items on load
- **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead

##  UI System Changes

- **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour`
- **Fixed menu persistence bug**: Menus no longer reappear after scene transitions
- **Pause Menu**: Now reacts to all scene loads (not just first scene)
- **Orientation Enforcer**: Enforces per-scene via `SceneManagementService`
- **Loading Screen**: Integrated with new lifecycle

## ⚠️ Breaking Changes

1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority
2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead
3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently
4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour`

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Reviewed-on: #51
2025-11-07 15:38:31 +00:00

255 lines
9.7 KiB
C#

using System.Collections;
using System;
using Core;
using Core.Lifecycle;
using UnityEngine;
using UnityEngine.UI;
namespace UI
{
/// <summary>
/// Controls the loading screen UI display, progress updates, and timing
/// </summary>
public class LoadingScreenController : ManagedBehaviour
{
[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;
private static LoadingScreenController _instance;
/// <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;
/// <summary>
/// Singleton instance of the LoadingScreenController. No longer creates an instance if one doesn't exist.
/// </summary>
public static LoadingScreenController Instance => _instance;
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 45; // UI infrastructure, before UIPageController
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
_instance = this;
// Set up container reference early
if (loadingScreenContainer == null)
loadingScreenContainer = gameObject;
// Ensure the loading screen is initially hidden
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(false);
}
}
protected override void OnManagedAwake()
{
Logging.Debug("[LoadingScreenController] Initialized");
}
/// <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
Logging.Debug($"[LoadingScreen] 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;
Logging.Debug("[LoadingScreen] 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;
Logging.Debug("[LoadingScreen] Final progress set to 1.0");
}
// Hide the screen if loading is also complete
if (_loadingComplete)
{
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(false);
Logging.Debug("[LoadingScreen] Animation AND loading complete, hiding screen");
// Invoke the callback when fully hidden
_onLoadingScreenFullyHidden?.Invoke();
_onLoadingScreenFullyHidden = null;
}
}
_progressCoroutine = null;
}
/// <summary>
/// Called when the actual loading process is complete
/// </summary>
public void HideLoadingScreen()
{
Logging.Debug("[LoadingScreen] 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);
Logging.Debug("[LoadingScreen] Animation already complete, hiding screen immediately");
// Invoke the callback when fully hidden
_onLoadingScreenFullyHidden?.Invoke();
_onLoadingScreenFullyHidden = null;
}
}
else
{
Logging.Debug("[LoadingScreen] 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;
}
}
}