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
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AppleHills.Data.CardSystem;
|
||||
using Bootstrap;
|
||||
using Data.CardSystem;
|
||||
using Pixelplacement;
|
||||
using UI.Core;
|
||||
@@ -41,8 +40,10 @@ namespace UI.CardSystem
|
||||
private List<AlbumCardPlacementDraggable> _activeCards = new List<AlbumCardPlacementDraggable>();
|
||||
private const int MAX_VISIBLE_CARDS = 3;
|
||||
|
||||
private void Awake()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Make sure we have a CanvasGroup for transitions
|
||||
if (canvasGroup == null)
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
@@ -64,16 +65,7 @@ namespace UI.CardSystem
|
||||
// Set up booster pack button listeners
|
||||
SetupBoosterButtonListeners();
|
||||
|
||||
// Register for post-boot initialization
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
|
||||
// UI pages should start disabled
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
// Subscribe to CardSystemManager events
|
||||
// Subscribe to CardSystemManager events (managers are guaranteed to be initialized)
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
|
||||
@@ -84,6 +76,9 @@ namespace UI.CardSystem
|
||||
int initialCount = CardSystemManager.Instance.GetBoosterPackCount();
|
||||
UpdateBoosterButtons(initialCount);
|
||||
}
|
||||
|
||||
// UI pages should start disabled
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void SetupBoosterButtonListeners()
|
||||
@@ -102,7 +97,7 @@ namespace UI.CardSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
// Unsubscribe from CardSystemManager
|
||||
if (CardSystemManager.Instance != null)
|
||||
@@ -134,6 +129,9 @@ namespace UI.CardSystem
|
||||
|
||||
// Clean up active cards
|
||||
CleanupActiveCards();
|
||||
|
||||
// Call base implementation
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
private void OnExitButtonClicked()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bootstrap;
|
||||
using Core.Lifecycle;
|
||||
using Data.CardSystem;
|
||||
using Pixelplacement;
|
||||
using Pixelplacement.TweenSystem;
|
||||
@@ -12,7 +12,7 @@ namespace UI.CardSystem
|
||||
/// Can be reused across different UI elements that need to show numeric notifications
|
||||
/// Automatically syncs with CardSystemManager to display booster pack count
|
||||
/// </summary>
|
||||
public class BoosterNotificationDot : MonoBehaviour
|
||||
public class BoosterNotificationDot : ManagedBehaviour
|
||||
{
|
||||
[Header("UI References")]
|
||||
[SerializeField] private GameObject dotBackground;
|
||||
@@ -40,8 +40,10 @@ namespace UI.CardSystem
|
||||
|
||||
private TweenBase _activeTween;
|
||||
|
||||
private void Awake()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Store original scale for pulse animation
|
||||
if (dotBackground != null)
|
||||
{
|
||||
@@ -54,13 +56,7 @@ namespace UI.CardSystem
|
||||
countText.color = textColor;
|
||||
}
|
||||
|
||||
// Register for post-boot initialization
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
// Subscribe to CardSystemManager events
|
||||
// Subscribe to CardSystemManager events (managers are guaranteed to be initialized)
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
|
||||
@@ -76,13 +72,16 @@ namespace UI.CardSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
// Unsubscribe from CardSystemManager events to prevent memory leaks
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
|
||||
}
|
||||
|
||||
// Call base implementation
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -192,6 +192,20 @@ namespace UI.CardSystem
|
||||
_placedCard = albumCard;
|
||||
_isOccupiedPermanently = true;
|
||||
|
||||
// Resize the card to match the slot size (same as placed cards)
|
||||
RectTransform cardRect = albumCard.transform as RectTransform;
|
||||
RectTransform slotRect = transform as RectTransform;
|
||||
if (cardRect != null && slotRect != null)
|
||||
{
|
||||
// Set height to match slot height (AspectRatioFitter will handle width)
|
||||
float targetHeight = slotRect.rect.height;
|
||||
cardRect.sizeDelta = new Vector2(cardRect.sizeDelta.x, targetHeight);
|
||||
|
||||
// Ensure position and rotation are centered
|
||||
cardRect.localPosition = Vector3.zero;
|
||||
cardRect.localRotation = Quaternion.identity;
|
||||
}
|
||||
|
||||
// Register with AlbumViewPage for enlarge/shrink handling
|
||||
AlbumViewPage albumPage = FindObjectOfType<AlbumViewPage>();
|
||||
if (albumPage != null)
|
||||
|
||||
242
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs
Normal file
242
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Data.CardSystem;
|
||||
using Pixelplacement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI.CardSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton UI component for granting booster packs from minigames.
|
||||
/// Displays a booster pack with glow effect, waits for user to click continue,
|
||||
/// then animates the pack flying to bottom-left corner before granting the reward.
|
||||
/// </summary>
|
||||
public class MinigameBoosterGiver : MonoBehaviour
|
||||
{
|
||||
public static MinigameBoosterGiver Instance { get; private set; }
|
||||
|
||||
[Header("Visual References")]
|
||||
[SerializeField] private GameObject visualContainer;
|
||||
[SerializeField] private RectTransform boosterImage;
|
||||
[SerializeField] private RectTransform glowImage;
|
||||
[SerializeField] private Button continueButton;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private float hoverAmount = 20f;
|
||||
[SerializeField] private float hoverDuration = 1.5f;
|
||||
[SerializeField] private float glowPulseMin = 0.9f;
|
||||
[SerializeField] private float glowPulseMax = 1.1f;
|
||||
[SerializeField] private float glowPulseDuration = 1.2f;
|
||||
|
||||
[Header("Disappear Animation")]
|
||||
[SerializeField] private Vector2 targetBottomLeftOffset = new Vector2(100f, 100f);
|
||||
[SerializeField] private float disappearDuration = 0.8f;
|
||||
[SerializeField] private float disappearScale = 0.2f;
|
||||
|
||||
private Vector3 _boosterInitialPosition;
|
||||
private Vector3 _boosterInitialScale;
|
||||
private Vector3 _glowInitialScale;
|
||||
private Coroutine _currentSequence;
|
||||
private Action _onCompleteCallback;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Singleton pattern
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Debug.LogWarning("[MinigameBoosterGiver] Duplicate instance found. Destroying.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
// Cache initial values
|
||||
if (boosterImage != null)
|
||||
{
|
||||
_boosterInitialPosition = boosterImage.localPosition;
|
||||
_boosterInitialScale = boosterImage.localScale;
|
||||
}
|
||||
|
||||
if (glowImage != null)
|
||||
{
|
||||
_glowInitialScale = glowImage.localScale;
|
||||
}
|
||||
|
||||
// Setup button listener
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.onClick.AddListener(OnContinueClicked);
|
||||
}
|
||||
|
||||
// Start hidden
|
||||
if (visualContainer != null)
|
||||
{
|
||||
visualContainer.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.onClick.RemoveListener(OnContinueClicked);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public API to give a booster pack. Displays UI, starts animations, and waits for user interaction.
|
||||
/// </summary>
|
||||
/// <param name="onComplete">Optional callback when the sequence completes and pack is granted</param>
|
||||
public void GiveBooster(Action onComplete = null)
|
||||
{
|
||||
if (_currentSequence != null)
|
||||
{
|
||||
Debug.LogWarning("[MinigameBoosterGiver] Already running a sequence. Ignoring new request.");
|
||||
return;
|
||||
}
|
||||
|
||||
_onCompleteCallback = onComplete;
|
||||
_currentSequence = StartCoroutine(GiveBoosterSequence());
|
||||
}
|
||||
|
||||
private IEnumerator GiveBoosterSequence()
|
||||
{
|
||||
// Show the visual
|
||||
if (visualContainer != null)
|
||||
{
|
||||
visualContainer.SetActive(true);
|
||||
}
|
||||
|
||||
// Reset positions and scales
|
||||
if (boosterImage != null)
|
||||
{
|
||||
boosterImage.localPosition = _boosterInitialPosition;
|
||||
boosterImage.localScale = _boosterInitialScale;
|
||||
}
|
||||
|
||||
if (glowImage != null)
|
||||
{
|
||||
glowImage.localScale = _glowInitialScale;
|
||||
}
|
||||
|
||||
// Enable the continue button
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.interactable = true;
|
||||
}
|
||||
|
||||
// Start idle hovering animation on booster (ping-pong)
|
||||
if (boosterImage != null)
|
||||
{
|
||||
Vector3 hoverTarget = _boosterInitialPosition + Vector3.up * hoverAmount;
|
||||
Tween.LocalPosition(boosterImage, hoverTarget, hoverDuration, 0f, Tween.EaseLinear, Tween.LoopType.PingPong);
|
||||
}
|
||||
|
||||
// Start pulsing animation on glow (ping-pong scale)
|
||||
if (glowImage != null)
|
||||
{
|
||||
Vector3 glowPulseScale = _glowInitialScale * glowPulseMax;
|
||||
Tween.LocalScale(glowImage, glowPulseScale, glowPulseDuration, 0f, Tween.EaseOut, Tween.LoopType.PingPong);
|
||||
}
|
||||
|
||||
// Wait for button click (handled by OnContinueClicked)
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private void OnContinueClicked()
|
||||
{
|
||||
if (_currentSequence == null)
|
||||
{
|
||||
return; // Not in a sequence
|
||||
}
|
||||
|
||||
// Disable button to prevent double-clicks
|
||||
if (continueButton != null)
|
||||
{
|
||||
continueButton.interactable = false;
|
||||
}
|
||||
|
||||
// Stop the ongoing animations by stopping all tweens on these objects
|
||||
if (boosterImage != null)
|
||||
{
|
||||
Tween.Stop(boosterImage.GetInstanceID());
|
||||
}
|
||||
|
||||
if (glowImage != null)
|
||||
{
|
||||
Tween.Stop(glowImage.GetInstanceID());
|
||||
// Fade out the glow
|
||||
Tween.LocalScale(glowImage, Vector3.zero, disappearDuration * 0.5f, 0f, Tween.EaseInBack);
|
||||
}
|
||||
|
||||
// Start disappear animation
|
||||
StartCoroutine(DisappearSequence());
|
||||
}
|
||||
|
||||
private IEnumerator DisappearSequence()
|
||||
{
|
||||
if (boosterImage == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Calculate bottom-left corner position in local space
|
||||
RectTransform canvasRect = GetComponentInParent<Canvas>()?.GetComponent<RectTransform>();
|
||||
Vector3 targetPosition;
|
||||
|
||||
if (canvasRect != null)
|
||||
{
|
||||
// Convert bottom-left corner with offset to local position
|
||||
Vector2 bottomLeft = new Vector2(-canvasRect.rect.width / 2f, -canvasRect.rect.height / 2f);
|
||||
targetPosition = bottomLeft + targetBottomLeftOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback if no canvas found
|
||||
targetPosition = _boosterInitialPosition + new Vector3(-500f, -500f, 0f);
|
||||
}
|
||||
|
||||
// Tween to bottom-left corner
|
||||
Tween.LocalPosition(boosterImage, targetPosition, disappearDuration, 0f, Tween.EaseInBack);
|
||||
|
||||
// Scale down
|
||||
Vector3 targetScale = _boosterInitialScale * disappearScale;
|
||||
Tween.LocalScale(boosterImage, targetScale, disappearDuration, 0f, Tween.EaseInBack);
|
||||
|
||||
// Wait for animation to complete
|
||||
yield return new WaitForSeconds(disappearDuration);
|
||||
|
||||
// Grant the booster pack
|
||||
if (CardSystemManager.Instance != null)
|
||||
{
|
||||
CardSystemManager.Instance.AddBoosterPack(1);
|
||||
Debug.Log("[MinigameBoosterGiver] Booster pack granted!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[MinigameBoosterGiver] CardSystemManager not found, cannot grant booster pack.");
|
||||
}
|
||||
|
||||
// Hide the visual
|
||||
if (visualContainer != null)
|
||||
{
|
||||
visualContainer.SetActive(false);
|
||||
}
|
||||
|
||||
// Invoke completion callback
|
||||
_onCompleteCallback?.Invoke();
|
||||
_onCompleteCallback = null;
|
||||
|
||||
// Clear sequence reference
|
||||
_currentSequence = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs.meta
Normal file
12
Assets/Scripts/UI/CardSystem/MinigameBoosterGiver.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d8f3e9a2b4c5f6d1a8e9c0b3d4f5a6b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
Reference in New Issue
Block a user