Add distinction between managed awake and managed start

This commit is contained in:
Michal Pikulski
2025-11-10 15:52:53 +01:00
parent c4d356886f
commit 7565b189b9
48 changed files with 990 additions and 1639 deletions

View File

@@ -1,4 +1,4 @@
using UnityEditor;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using Core.Lifecycle;
@@ -12,7 +12,7 @@ namespace Editor.Lifecycle
/// Editor-only bootstrap that ensures OnSceneReady is triggered when playing directly from a scene in Unity Editor.
///
/// PROBLEM: When you press Play in the editor without going through the scene manager:
/// - CustomBoot runs and triggers OnBootCompletionTriggered (which broadcasts OnManagedAwake)
/// - CustomBoot runs and triggers OnBootCompletionTriggered (which broadcasts OnManagedStart)
/// - But BroadcastSceneReady is NEVER called for the initial scene
/// - Components in the scene never receive their OnSceneReady() callback
///

View File

@@ -1,153 +0,0 @@
using Interactions;
using PuzzleS;
using UnityEditor;
using UnityEngine;
namespace Editor
{
public class ItemPrefabEditorWindow : EditorWindow
{
private GameObject _selectedGameObject;
private InteractableBase _interactable;
private PickupItemData _pickupData;
private PuzzleStepSO _objectiveData;
private UnityEditor.Editor _soEditor;
private string _pickupSoFolderPath = "Assets/Data/Items";
private string _puzzleSoFolderPath = "Assets/Data/Puzzles";
private enum ItemType { None, Pickup, ItemSlot }
private ItemType _itemType = ItemType.None;
[MenuItem("AppleHills/Item Prefab Editor")]
public static void ShowWindow()
{
var window = GetWindow<ItemPrefabEditorWindow>("Item Prefab Editor");
window.minSize = new Vector2(400, 400);
}
private void OnEnable()
{
Selection.selectionChanged += Repaint;
}
private void OnDisable()
{
Selection.selectionChanged -= Repaint;
}
private void OnGUI()
{
_selectedGameObject = null;
_interactable = null;
if (Selection.activeGameObject != null)
{
_selectedGameObject = Selection.activeGameObject;
_interactable = _selectedGameObject.GetComponent<InteractableBase>();
}
else if (Selection.activeObject is GameObject go)
{
_selectedGameObject = go;
_interactable = go.GetComponent<InteractableBase>();
}
if (_selectedGameObject == null || _interactable == null)
{
EditorGUILayout.HelpBox("Select a GameObject or prefab with an InteractableBase component to edit.", MessageType.Info);
return;
}
EditorGUILayout.LabelField("Editing: ", _selectedGameObject.name, EditorStyles.boldLabel);
EditorGUILayout.Space();
// Determine current type
bool hasPickup = _selectedGameObject.GetComponent<Pickup>() != null;
bool hasSlot = _selectedGameObject.GetComponent<ItemSlot>() != null;
if (hasSlot) _itemType = ItemType.ItemSlot;
else if (hasPickup) _itemType = ItemType.Pickup;
else _itemType = ItemType.None;
// Item type selection
var newType = (ItemType)EditorGUILayout.EnumPopup("Item Type", _itemType);
if (newType != _itemType)
{
// Remove both, then add selected
PrefabEditorUtility.RemoveComponent<Pickup>(_selectedGameObject);
PrefabEditorUtility.RemoveComponent<ItemSlot>(_selectedGameObject);
if (newType == ItemType.Pickup)
PrefabEditorUtility.AddOrGetComponent<Pickup>(_selectedGameObject);
else if (newType == ItemType.ItemSlot)
PrefabEditorUtility.AddOrGetComponent<ItemSlot>(_selectedGameObject);
_itemType = newType;
}
// ObjectiveStepBehaviour
bool hasObjective = _selectedGameObject.GetComponent<ObjectiveStepBehaviour>() != null;
bool addObjective = EditorGUILayout.Toggle("ObjectiveStepBehaviour", hasObjective);
if (addObjective && !hasObjective)
{
PrefabEditorUtility.AddOrGetComponent<ObjectiveStepBehaviour>(_selectedGameObject);
}
else if (!addObjective && hasObjective)
{
PrefabEditorUtility.RemoveComponent<ObjectiveStepBehaviour>(_selectedGameObject);
}
// Pickup Data (for Pickup or ItemSlot)
if (_itemType == ItemType.Pickup || _itemType == ItemType.ItemSlot)
{
var pickup = _selectedGameObject.GetComponent<Pickup>();
_pickupData = pickup.itemData;
EditorGUILayout.LabelField("Pickup Data:", EditorStyles.boldLabel);
_pickupData = (PickupItemData)EditorGUILayout.ObjectField("PickupItemData", _pickupData, typeof(PickupItemData), false);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_pickupSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_pickupSoFolderPath = PrefabEditorUtility.SelectFolder(_pickupSoFolderPath, "Data/Items");
}
EditorGUILayout.EndHorizontal();
if (_pickupData == null && GUILayout.Button("Create New PickupItemData"))
{
_pickupData = PrefabEditorUtility.CreateScriptableAsset<PickupItemData>(_selectedGameObject.name + "_pickup", _pickupSoFolderPath);
}
if (_pickupData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _pickupData);
pickup.itemData = _pickupData;
}
}
// Objective Data
if (addObjective)
{
var obj = _selectedGameObject.GetComponent<ObjectiveStepBehaviour>();
_objectiveData = obj.stepData;
EditorGUILayout.LabelField("Objective Data:", EditorStyles.boldLabel);
_objectiveData = (PuzzleStepSO)EditorGUILayout.ObjectField("PuzzleStepSO", _objectiveData, typeof(PuzzleStepSO), false);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_puzzleSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_puzzleSoFolderPath = PrefabEditorUtility.SelectFolder(_puzzleSoFolderPath, "Data/Puzzles");
}
EditorGUILayout.EndHorizontal();
if (_objectiveData == null && GUILayout.Button("Create New PuzzleStepSO"))
{
_objectiveData = PrefabEditorUtility.CreateScriptableAsset<PuzzleStepSO>(_selectedGameObject.name + "_puzzle", _puzzleSoFolderPath);
}
if (_objectiveData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _objectiveData);
obj.stepData = _objectiveData;
}
}
if (GUI.changed)
{
EditorUtility.SetDirty(_selectedGameObject);
PrefabUtility.RecordPrefabInstancePropertyModifications(_selectedGameObject);
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 943b203cde5343c68a6278c111fce2ed
timeCreated: 1757508162

View File

@@ -1,168 +0,0 @@
using UnityEditor;
using UnityEngine;
using System.IO;
using Interactions;
using PuzzleS;
namespace Editor
{
public class PrefabCreatorWindow : EditorWindow
{
private string _prefabName = "NewPrefab";
private string _saveFolderPath = "Assets/Prefabs/Items";
private string _pickupSoFolderPath = "Assets/Data/Items";
private string _puzzleSoFolderPath = "Assets/Data/Puzzles";
private bool _addObjective;
private PickupItemData _pickupData;
private PuzzleStepSO _objectiveData;
private UnityEditor.Editor _soEditor;
private enum ItemType { None, Pickup, ItemSlot }
private ItemType _itemType = ItemType.None;
private bool _createNext = false;
[MenuItem("AppleHills/Item Prefab Creator")]
public static void ShowWindow()
{
var window = GetWindow<PrefabCreatorWindow>("Prefab Creator");
window.minSize = new Vector2(400, 400);
// Set default paths if not already set
if (string.IsNullOrEmpty(window._saveFolderPath))
window._saveFolderPath = "Assets/Prefabs/Items";
if (string.IsNullOrEmpty(window._pickupSoFolderPath))
window._pickupSoFolderPath = "Assets/Data/Items";
if (string.IsNullOrEmpty(window._puzzleSoFolderPath))
window._puzzleSoFolderPath = "Assets/Data/Puzzles";
}
private void OnGUI()
{
EditorGUILayout.LabelField("Prefab Creator", EditorStyles.boldLabel);
_prefabName = EditorGUILayout.TextField("Prefab Name", _prefabName);
// Prefab save folder
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save Folder");
EditorGUILayout.SelectableLabel(_saveFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_saveFolderPath = PrefabEditorUtility.SelectFolder(_saveFolderPath, "Prefabs/Items");
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Add Components:", EditorStyles.boldLabel);
// Item type selection
var newType = (ItemType)EditorGUILayout.EnumPopup("Item Type", _itemType);
if (newType != _itemType)
{
_itemType = newType;
}
bool addObjective = EditorGUILayout.Toggle("ObjectiveStepBehaviour", _addObjective);
_addObjective = addObjective;
EditorGUILayout.Space();
// Pickup Data (for Pickup or ItemSlot)
if (_itemType == ItemType.Pickup || _itemType == ItemType.ItemSlot)
{
EditorGUILayout.LabelField("Pickup Data:", EditorStyles.boldLabel);
_pickupData = (PickupItemData)EditorGUILayout.ObjectField("PickupItemData", _pickupData, typeof(PickupItemData), false);
// Pickup SO save folder
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_pickupSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_pickupSoFolderPath = PrefabEditorUtility.SelectFolder(_pickupSoFolderPath, "Data/Items");
}
EditorGUILayout.EndHorizontal();
if (_pickupData == null && GUILayout.Button("Create New PickupItemData"))
{
_pickupData = PrefabEditorUtility.CreateScriptableAsset<PickupItemData>(_prefabName + "_pickup", _pickupSoFolderPath);
}
if (_pickupData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _pickupData);
}
}
// Objective Data
if (_addObjective)
{
EditorGUILayout.LabelField("Objective Data:", EditorStyles.boldLabel);
_objectiveData = (PuzzleStepSO)EditorGUILayout.ObjectField("PuzzleStepSO", _objectiveData, typeof(PuzzleStepSO), false);
// Puzzle SO save folder
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_puzzleSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_puzzleSoFolderPath = PrefabEditorUtility.SelectFolder(_puzzleSoFolderPath, "Data/Puzzles");
}
EditorGUILayout.EndHorizontal();
if (_objectiveData == null && GUILayout.Button("Create New PuzzleStepSO"))
{
_objectiveData = PrefabEditorUtility.CreateScriptableAsset<PuzzleStepSO>(_prefabName + "_puzzle", _puzzleSoFolderPath);
}
if (_objectiveData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _objectiveData);
}
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUI.enabled = !string.IsNullOrEmpty(_prefabName) && !string.IsNullOrEmpty(_saveFolderPath);
if (GUILayout.Button("Create Prefab", GUILayout.Height(28)))
{
CreatePrefab();
}
_createNext = GUILayout.Toggle(_createNext, "Create Next", GUILayout.Width(100), GUILayout.Height(28));
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
}
private void CreatePrefab()
{
var go = new GameObject(_prefabName);
// Note: No need to add InteractableBase separately - Pickup and ItemSlot inherit from it
go.AddComponent<BoxCollider>();
int interactableLayer = LayerMask.NameToLayer("Interactable");
if (interactableLayer != -1)
go.layer = interactableLayer;
go.AddComponent<SpriteRenderer>();
if (_itemType == ItemType.Pickup)
{
var pickup = go.AddComponent<Pickup>();
pickup.itemData = _pickupData;
}
else if (_itemType == ItemType.ItemSlot)
{
var slot = go.AddComponent<ItemSlot>();
slot.itemData = _pickupData;
}
if (_addObjective)
{
var obj = go.AddComponent<ObjectiveStepBehaviour>();
obj.stepData = _objectiveData;
}
string prefabPath = Path.Combine(_saveFolderPath, _prefabName + ".prefab").Replace("\\", "/");
var prefab = PrefabUtility.SaveAsPrefabAsset(go, prefabPath);
DestroyImmediate(go);
AssetDatabase.Refresh();
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
EditorUtility.DisplayDialog("Prefab Created", $"Prefab saved to {prefabPath}", "OK");
if (_createNext)
{
_prefabName = "NewPrefab";
_pickupData = null;
_objectiveData = null;
_itemType = ItemType.None;
_addObjective = false;
_soEditor = null;
GUI.FocusControl(null);
Repaint();
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f67e06e997f642509ba61ea12b0f793e
timeCreated: 1757503955

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
using System;
using System;
using AppleHills.Core.Settings;
using UnityEngine;
using Core;
@@ -33,10 +33,8 @@ namespace Bootstrap
// Run very early - need to set up loading screen before other systems initialize
public override int ManagedAwakePriority => 5;
protected override void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // Register with LifecycleManager
LogDebugMessage("BootSceneController.Awake() - Initializing loading screen DURING bootstrap");
// Validate loading screen exists
@@ -71,11 +69,11 @@ namespace Bootstrap
}
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
LogDebugMessage("BootSceneController.OnManagedAwake() - Boot is GUARANTEED complete, starting scene loading");
LogDebugMessage("BootSceneController.OnManagedStart() - Boot is GUARANTEED complete, starting scene loading");
// Boot is GUARANTEED complete at this point - that's the whole point of OnManagedAwake!
// Boot is GUARANTEED complete at this point - that's the whole point of OnManagedStart!
// No need to subscribe to OnBootCompleted or check CustomBoot.Initialised
_bootComplete = true;
_currentPhase = LoadingPhase.SceneLoading;

View File

@@ -39,15 +39,13 @@ namespace Cinematics
public override int ManagedAwakePriority => 170; // Cinematic systems
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
Logging.Debug("[CinematicsManager] Initialized");
}

View File

@@ -19,7 +19,7 @@ namespace Cinematics
public override int ManagedAwakePriority => 180; // Cinematic UI
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Reset the progress bar
if (radialProgressBar != null)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using AppleHills.Core.Interfaces;
using AppleHills.Core.Settings;
@@ -37,14 +37,12 @@ namespace Core
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 10; // Core infrastructure - runs early
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Create settings providers - must happen in Awake so other managers can access settings in their ManagedAwake
// Create settings providers - must happen in OnManagedAwake so other managers can access settings in their ManagedStart
SettingsProvider.Instance.gameObject.name = "Settings Provider";
DeveloperSettingsProvider.Instance.gameObject.name = "Developer Settings Provider";
@@ -57,9 +55,9 @@ namespace Core
_managerLogVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().gameManagerLogVerbosity;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Settings are already initialized in Awake()
// Settings are already initialized in OnManagedAwake()
// This is available for future initialization that depends on other managers
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
using Interactions;
@@ -50,15 +50,13 @@ namespace Core
public override int ManagedAwakePriority => 75; // Item registry
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
Logging.Debug("[ItemManager] Initialized");
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
@@ -123,7 +123,24 @@ namespace Core.Lifecycle
// ALWAYS add to managedAwakeList - this is the master list used for save/load
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
// Handle ManagedAwake timing based on boot state
// Register for all scene lifecycle hooks
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
InsertSorted(sceneReadyList, component, component.SceneReadyPriority);
InsertSorted(saveRequestedList, component, component.SavePriority);
InsertSorted(restoreRequestedList, component, component.RestorePriority);
InsertSorted(destroyList, component, component.DestroyPriority);
// Call OnManagedAwake immediately after registration (early initialization hook)
try
{
component.InvokeManagedAwake();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
}
// Handle OnManagedStart timing based on boot state
if (isBootComplete)
{
// Check if we're currently loading a scene
@@ -136,27 +153,20 @@ namespace Core.Lifecycle
else
{
// Truly late registration (component enabled after scene is ready)
// Call OnManagedAwake immediately since boot already completed
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
// Call OnManagedStart immediately since boot already completed
LogDebug($"Late registration: Calling OnManagedStart immediately for {component.gameObject.name}");
try
{
component.InvokeManagedAwake();
component.InvokeManagedStart();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
Debug.LogError($"[LifecycleManager] Error in OnManagedStart for {component.gameObject.name}: {ex}");
}
}
}
// If boot not complete, component stays in list and will be processed by BroadcastManagedAwake()
// Register for all scene lifecycle hooks
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
InsertSorted(sceneReadyList, component, component.SceneReadyPriority);
InsertSorted(saveRequestedList, component, component.SavePriority);
InsertSorted(restoreRequestedList, component, component.RestorePriority);
InsertSorted(destroyList, component, component.DestroyPriority);
// If boot not complete, component stays in list and will be processed by BroadcastManagedStart()
// If this scene is already ready (and we're not in loading mode), call OnSceneReady immediately
if (!isLoadingScene && currentSceneReady == sceneName)
@@ -202,7 +212,7 @@ namespace Core.Lifecycle
/// <summary>
/// Called by CustomBoot when boot completes.
/// Broadcasts ManagedAwake to all registered components.
/// Broadcasts ManagedStart to all registered components.
/// </summary>
public void OnBootCompletionTriggered()
{
@@ -210,16 +220,16 @@ namespace Core.Lifecycle
return;
LogDebug("=== Boot Completion Triggered ===");
BroadcastManagedAwake();
BroadcastManagedStart();
isBootComplete = true;
}
/// <summary>
/// Broadcast OnManagedAwake to all registered components (priority ordered).
/// Broadcast OnManagedStart to all registered components (priority ordered).
/// </summary>
private void BroadcastManagedAwake()
private void BroadcastManagedStart()
{
LogDebug($"Broadcasting ManagedAwake to {managedAwakeList.Count} components");
LogDebug($"Broadcasting ManagedStart to {managedAwakeList.Count} components");
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
@@ -230,12 +240,12 @@ namespace Core.Lifecycle
try
{
component.InvokeManagedAwake();
component.InvokeManagedStart();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
Debug.LogError($"[LifecycleManager] Error in OnManagedStart for {component.gameObject.name}: {ex}");
}
}
@@ -275,20 +285,20 @@ namespace Core.Lifecycle
// Sort by ManagedAwake priority (lower values first)
pendingSceneComponents.Sort((a, b) => a.ManagedAwakePriority.CompareTo(b.ManagedAwakePriority));
// Call OnManagedAwake in priority order
// Call OnManagedStart in priority order
foreach (var component in pendingSceneComponents)
{
if (component == null) continue;
try
{
component.InvokeManagedAwake();
component.InvokeManagedStart();
HandleAutoRegistrations(component);
LogDebug($"Processed batched component: {component.gameObject.name} (Priority: {component.ManagedAwakePriority})");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for batched component {component.gameObject.name}: {ex}");
Debug.LogError($"[LifecycleManager] Error in OnManagedStart for batched component {component.gameObject.name}: {ex}");
}
}

View File

@@ -1,5 +1,4 @@
using System;
using UnityEngine;
using UnityEngine;
namespace Core.Lifecycle
{
@@ -12,7 +11,7 @@ namespace Core.Lifecycle
#region Priority Properties
/// <summary>
/// Priority for OnManagedAwake (lower values execute first).
/// Priority for OnManagedStart (lower values execute first).
/// Default: 100
/// </summary>
public virtual int ManagedAwakePriority => 100;
@@ -85,6 +84,7 @@ namespace Core.Lifecycle
// Public wrappers to invoke protected lifecycle methods
public void InvokeManagedAwake() => OnManagedAwake();
public void InvokeManagedStart() => OnManagedStart();
public void InvokeSceneUnloading() => OnSceneUnloading();
public void InvokeSceneReady() => OnSceneReady();
public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
@@ -108,9 +108,9 @@ namespace Core.Lifecycle
/// <summary>
/// Unity Awake - automatically registers with LifecycleManager.
/// IMPORTANT: Derived classes that override Awake MUST call base.Awake()
/// SEALED: Cannot be overridden. Use OnManagedAwake() for early initialization or OnManagedStart() for late initialization.
/// </summary>
protected virtual void Awake()
private void Awake()
{
if (LifecycleManager.Instance != null)
{
@@ -153,17 +153,28 @@ namespace Core.Lifecycle
#region Managed Lifecycle Hooks
/// <summary>
/// Called once per component after bootstrap completes.
/// GUARANTEE: Bootstrap resources are available, all managers are initialized.
/// For boot-time components: Called during LifecycleManager.BroadcastManagedAwake (priority ordered).
/// For late-registered components: Called immediately upon registration (bootstrap already complete).
/// Replaces the old Awake + InitializePostBoot pattern.
/// Called immediately during registration (during Awake).
/// Use for early initialization such as setting singleton instances.
/// TIMING: Fires during component's Awake(), no execution order guarantees between components.
/// NOT priority-ordered - fires whenever Unity calls this component's Awake().
/// </summary>
protected virtual void OnManagedAwake()
{
// Override in derived classes
}
/// <summary>
/// Called once per component after bootstrap completes.
/// GUARANTEE: Bootstrap resources are available, all managers are initialized.
/// For boot-time components: Called during LifecycleManager.BroadcastManagedStart (priority ordered).
/// For late-registered components: Called immediately upon registration (bootstrap already complete).
/// Use for initialization that depends on other systems.
/// </summary>
protected virtual void OnManagedStart()
{
// Override in derived classes
}
/// <summary>
/// Called before the scene this component belongs to is unloaded.
/// Called in REVERSE priority order (higher values execute first).

View File

@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using Cinematics;
using Core;
using Core.Lifecycle;
@@ -129,15 +129,13 @@ namespace AppleHills.Core
#region Lifecycle Methods
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// QuickAccess has minimal initialization
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
@@ -46,11 +46,9 @@ namespace Core.SaveLoad
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 20; // After GameManager and SceneManagerService
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Initialize critical state immediately
@@ -58,7 +56,7 @@ namespace Core.SaveLoad
IsRestoringState = false;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
Logging.Debug("[SaveLoadManager] Initialized");

View File

@@ -30,9 +30,9 @@ namespace Core
// Enable save/load participation
public override bool AutoRegisterForSave => true;
protected override void Awake()
protected override void OnManagedAwake()
{
base.Awake();
base.OnManagedAwake();
_director = GetComponent<PlayableDirector>();
if (_director != null)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AppleHills.Core.Settings;
@@ -47,11 +47,9 @@ namespace Core
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 15; // Core infrastructure, after GameManager
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Initialize current scene tracking - critical for scene management
@@ -65,10 +63,10 @@ namespace Core
}
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Set up loading screen reference and events
// This must happen in ManagedAwake because LoadingScreenController instance needs to be set first
// This must happen in ManagedStart because LoadingScreenController instance needs to be set first
_loadingScreen = LoadingScreenController.Instance;
SetupLoadingScreenEvents();

View File

@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using AppleHills.Core.Settings;
using Core.Lifecycle;
using Settings;
@@ -21,20 +21,18 @@ namespace Core
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 70; // Platform-specific utility
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Load verbosity settings early (GameManager sets up settings in its Awake)
// Load verbosity settings early (GameManager sets up settings in its OnManagedAwake)
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
LogDebugMessage("Initialized");
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Subscribe to SceneManagerService to enforce orientation on every scene load
if (SceneManagerService.Instance != null)

View File

@@ -1,11 +1,12 @@
using UnityEngine;
using Pixelplacement;
using System;
using System.Collections;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine.Audio;
public class PicnicBehaviour : MonoBehaviour
public class PicnicBehaviour : ManagedBehaviour
{
[Header("Random Call Settings")]
public float getDistractedMin = 2f;
@@ -17,7 +18,8 @@ public class PicnicBehaviour : MonoBehaviour
private Animator animator;
[Header("The FakeChocolate to destroy!")]
[SerializeField] private GameObject fakeChocolate; // Assign in Inspector
[SerializeField] private GameObject fakeChocolate;
[SerializeField] private GameObject realChocolate;
private AppleAudioSource _audioSource;
public AudioResource distractedAudioClips;
@@ -25,32 +27,44 @@ public class PicnicBehaviour : MonoBehaviour
public AudioResource feederClips;
public AudioResource moanerClips;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
StartCoroutine(StateCycleRoutine());
}
// Save system configuration
public override bool AutoRegisterForSave => true;
void Awake()
// Runtime state tracking
private bool _fakeChocolateDestroyed;
protected override void OnManagedAwake()
{
stateMachine = GetComponent<AppleMachine>();
animator = GetComponent<Animator>();
_audioSource = GetComponent<AppleAudioSource>();
}
protected override void OnSceneRestoreCompleted()
{
if (_fakeChocolateDestroyed)
{
DestroyChocolateObjects();
}
else
{
StartCoroutine(StateCycleRoutine());
}
}
private IEnumerator StateCycleRoutine()
{
while (true)
{
// Distracted state
float distractedWait = Random.Range(getDistractedMin, getDistractedMax);
float distractedWait = UnityEngine.Random.Range(getDistractedMin, getDistractedMax);
stateMachine.ChangeState("Picnic PPL Distracted");
animator.SetBool("theyDistracted", true);
_audioSource.Stop();
yield return new WaitForSeconds(distractedWait);
// Chilling state
float chillingWait = Random.Range(getFlirtyMin, getFlirtyMax);
float chillingWait = UnityEngine.Random.Range(getFlirtyMin, getFlirtyMax);
stateMachine.ChangeState("Picnic PPL Chilling");
animator.SetBool("theyDistracted", false);
_audioSource.Stop();
@@ -58,27 +72,33 @@ public class PicnicBehaviour : MonoBehaviour
}
}
void StopAudio()
{
_audioSource.Stop();
}
public void triedToStealChocolate()
{
_audioSource.Stop();
animator.SetTrigger("theyAngry");
//stateMachine.ChangeState("Picnic PPL Angry");
Logging.Debug("Hey! Don't steal my chocolate!");
_audioSource.audioSource.resource = angryAudioClips;
_audioSource.Play(0);
}
public void destroyFakeChocolate()
{
_fakeChocolateDestroyed = true;
Destroy(fakeChocolate);
Destroy(realChocolate);
}
private void DestroyChocolateObjects()
{
if (fakeChocolate != null)
{
Destroy(fakeChocolate);
fakeChocolate = null; // Optional: clear reference
fakeChocolate = null;
}
if (realChocolate != null)
{
realChocolate.SetActive(true);
}
}
@@ -100,5 +120,33 @@ public class PicnicBehaviour : MonoBehaviour
_audioSource.Play(0);
}
protected override string OnSceneSaveRequested()
{
var state = new PicnicBehaviourState { fakeChocolateDestroyed = _fakeChocolateDestroyed };
return JsonUtility.ToJson(state);
}
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData)) return;
try
{
var state = JsonUtility.FromJson<PicnicBehaviourState>(serializedData);
if (state != null)
{
_fakeChocolateDestroyed = state.fakeChocolateDestroyed;
}
}
catch (Exception ex)
{
Debug.LogWarning($"[PicnicBehaviour] Failed to restore state: {ex.Message}");
}
}
}
[Serializable]
public class PicnicBehaviourState
{
public bool fakeChocolateDestroyed;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
@@ -47,18 +47,16 @@ namespace Data.CardSystem
public override int ManagedAwakePriority => 60; // Data systems
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Load card definitions from Addressables, then register with save system
LoadCardDefinitionsFromAddressables();
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
Logging.Debug("[CardSystemManager] Initialized");
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Generic;
using Core;
using Core.Lifecycle;
using Interactions;
@@ -37,7 +35,7 @@ namespace Dialogue
public override int ManagedAwakePriority => 150; // Dialogue systems
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Get required components
appleAudioSource = GetComponent<AppleAudioSource>();

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
@@ -52,11 +52,9 @@ namespace Input
public override int ManagedAwakePriority => 25; // Input infrastructure
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Load verbosity settings early
@@ -89,10 +87,10 @@ namespace Input
SwitchInputOnSceneLoaded(SceneManager.GetActiveScene().name);
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Subscribe to scene load events from SceneManagerService
// This must happen in ManagedAwake because SceneManagerService instance needs to be set first
// This must happen in ManagedStart because SceneManagerService instance needs to be set first
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;

View File

@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using Pathfinding;
using AppleHills.Core.Settings;
using Core;
@@ -73,7 +73,7 @@ namespace Input
public override string SaveId => $"{gameObject.scene.name}/PlayerController";
public override int ManagedAwakePriority => 100; // Player controller
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
aiPath = GetComponent<AIPath>();
artTransform = transform.Find("CharacterArt");

View File

@@ -85,9 +85,9 @@ namespace Interactions
}
}
protected override void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // SaveableInteractable registration
base.OnManagedAwake(); // SaveableInteractable registration
// Setup visuals
if (iconRenderer == null)

View File

@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using System;
using System.Linq;
using Core;
@@ -32,9 +32,9 @@ namespace Interactions
public event Action<PickupItemData> OnItemPickedUp;
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
protected override void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // Register with save system
base.OnManagedAwake(); // Register with save system
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
@@ -44,8 +44,9 @@ namespace Interactions
// Always register with ItemManager, even if picked up
// This allows the save/load system to find held items when restoring state
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
base.OnManagedStart();
ItemManager.Instance?.RegisterPickup(this);
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using AppleHills.Core.Settings;
using Core;
using Input;
@@ -21,9 +21,9 @@ namespace Levels
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
protected override void Awake()
protected override void OnManagedAwake()
{
base.Awake();
base.OnManagedAwake();
Logging.Debug($"[LevelSwitch] Awake called for {gameObject.name} in scene {gameObject.scene.name}");
@@ -36,9 +36,9 @@ namespace Levels
ApplySwitchData();
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
Logging.Debug($"[LevelSwitch] OnManagedAwake called for {gameObject.name}");
Logging.Debug($"[LevelSwitch] OnManagedStart called for {gameObject.name}");
}
protected override void OnSceneReady()

View File

@@ -1,4 +1,4 @@
using System;
using System;
using AppleHills.Core.Settings;
using Core;
using Input;
@@ -45,9 +45,9 @@ namespace Levels
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
protected override void Awake()
protected override void OnManagedAwake()
{
base.Awake();
base.OnManagedAwake();
switchActive = true;
if (iconRenderer == null)
@@ -64,10 +64,9 @@ namespace Levels
ApplySwitchData();
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
base.OnManagedAwake();
base.OnManagedStart();
// If startUnlocked is true, always start active
if (startUnlocked)
{

View File

@@ -1,4 +1,4 @@
using AppleHills.Core.Interfaces;
using AppleHills.Core.Interfaces;
using AppleHills.Core.Settings;
using Cinematics;
using Core;
@@ -107,10 +107,8 @@ namespace Minigames.DivingForPictures
public override int ManagedAwakePriority => 190;
public override bool AutoRegisterPausable => true; // Automatic GameManager registration
protected override void Awake()
protected override void OnManagedAwake()
{
base.Awake();
if (_instance == null)
{
_instance = this;
@@ -121,7 +119,7 @@ namespace Minigames.DivingForPictures
}
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;

View File

@@ -1,4 +1,4 @@
using Interactions;
using Interactions;
using UnityEngine;
using Pathfinding;
using Utils;
@@ -108,7 +108,7 @@ public class FollowerController : ManagedBehaviour
public override int ManagedAwakePriority => 110; // Follower after player
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
_aiPath = GetComponent<AIPath>();
// Find art prefab and animator

View File

@@ -16,7 +16,7 @@ namespace PuzzleS
// Save system configuration
public override bool AutoRegisterForSave => true;
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Initialize after all managers are ready
}

View File

@@ -1,4 +1,4 @@
using Input;
using Input;
using Interactions;
using UnityEngine;
using Core;
@@ -32,7 +32,7 @@ namespace PuzzleS
// Enum for tracking proximity state (simplified to just Close and Far)
public enum ProximityState { Close, Far }
protected override void Awake()
protected override void OnManagedAwake()
{
_interactable = GetComponent<InteractableBase>();
@@ -56,14 +56,10 @@ namespace PuzzleS
Logging.Warning($"[Puzzles] Indicator prefab for {stepData?.stepId} does not implement IPuzzlePrompt");
}
}
base.Awake();
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
base.OnManagedAwake();
// Register with PuzzleManager - safe to access .Instance here
if (stepData != null && PuzzleManager.Instance != null)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -96,15 +96,13 @@ namespace PuzzleS
public override int ManagedAwakePriority => 80; // Puzzle systems
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Initialize settings reference
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();

View File

@@ -20,15 +20,13 @@ public class AppleAudioSource : ManagedBehaviour
public int sourcePriority;
protected override void Awake()
{
base.Awake();
audioSource = GetComponent<AudioSource>();
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
protected override void OnManagedAwake()
{
audioSource = GetComponent<AudioSource>();
}
protected override void OnManagedStart()
{
AudioManager.Instance.RegisterNewAudioSource(this);
_audioMixer = AudioManager.Instance.audioMixer;
InitializeAudioSource();

View File

@@ -44,15 +44,13 @@ public class AudioManager : ManagedBehaviour, IPausable
public override int ManagedAwakePriority => 30; // Audio infrastructure
public override bool AutoRegisterPausable => true; // Auto-register as IPausable
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Initialize lists if they were not set in inspector
criticalVOSources = criticalVOSources ?? new List<AppleAudioSource>();
@@ -72,7 +70,7 @@ public class AudioManager : ManagedBehaviour, IPausable
}
else
{
Logging.Warning("[AudioManager] QuickAccess.Instance is null during OnManagedAwake. Some audio references may remain unset.");
Logging.Warning("[AudioManager] QuickAccess.Instance is null during OnManagedStart. Some audio references may remain unset.");
}
// Diagnostic

View File

@@ -23,7 +23,7 @@ public class BushAudioController : ManagedBehaviour
// Start is called once before the first execution of Update after the MonoBehaviour is created
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
_eventSource = VOPlayer.audioSource.RequestEventHandlers();
_eventSource.AudioStopped += PlayBirdCounter;

View File

@@ -10,7 +10,7 @@ public class PulverAudioController : ManagedBehaviour
private FollowerController followerController;
public ItemManager itemManager;
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
followerController = GetComponent<FollowerController>();
followerController.PulverIsCombining.AddListener(PulverIsCombining);

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
using Core;
@@ -44,10 +44,8 @@ namespace UI.CardSystem
private List<AlbumCardPlacementDraggable> _activeCards = new List<AlbumCardPlacementDraggable>();
private const int MAX_VISIBLE_CARDS = 3;
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
base.OnManagedAwake();
// Discover zone tabs from container
DiscoverZoneTabs();

View File

@@ -1,4 +1,4 @@
using Core.Lifecycle;
using Core.Lifecycle;
using Data.CardSystem;
using Pixelplacement;
using Pixelplacement.TweenSystem;
@@ -40,10 +40,8 @@ namespace UI.CardSystem
private TweenBase _activeTween;
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
base.OnManagedAwake();
// Store original scale for pulse animation
if (dotBackground != null)
{

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Core;
using Core.Lifecycle;
@@ -39,15 +39,13 @@ namespace UI.Core
public override int ManagedAwakePriority => 50; // UI infrastructure
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
Logging.Debug("[UIPageController] Initialized");
}

View File

@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using System;
using Core;
using Core.Lifecycle;
@@ -56,11 +56,9 @@ namespace UI
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 45; // UI infrastructure, before UIPageController
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Set up container reference early
@@ -74,7 +72,7 @@ namespace UI
}
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
Logging.Debug("[LoadingScreenController] Initialized");
}

View File

@@ -31,11 +31,9 @@ namespace UI
// After UIPageController (50)
public override int ManagedAwakePriority => 55;
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Ensure we have a CanvasGroup for transitions
@@ -51,9 +49,9 @@ namespace UI
gameObject.SetActive(false);
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Subscribe to scene-dependent events - must be in OnManagedAwake, not OnSceneReady
// Subscribe to scene-dependent events - must be in OnManagedStart, not OnSceneReady
// because PauseMenu is in DontDestroyOnLoad and OnSceneReady only fires once
if (SceneManagerService.Instance != null)
{

View File

@@ -115,14 +115,14 @@ namespace UI
private UIPageController _uiPageController;
private AppSwitcher _appSwitcherComponent;
private new void Awake()
protected override void OnManagedAwake()
{
base.Awake();
if (Instance != null)
{
Destroy(this);
return;
}
// Set instance immediately (early initialization)
_instance = this;
// Get UIPageController on same GameObject
@@ -135,7 +135,7 @@ namespace UI
InitializeReferences();
}
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Subscribe to UIPageController page changes for auto HUD management
if (_uiPageController != null)

View File

@@ -32,7 +32,7 @@ namespace UI.Tutorial
public override int ManagedAwakePriority => 200; // Tutorial runs late, after other systems
protected override void OnManagedAwake()
protected override void OnManagedStart()
{
// Ensure prompt is hidden initially (even before tutorial initialization)
if (tapPrompt != null)

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 53ec386bba82c1748886a5beb8468ecf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
m_Name: iOS
m_EditorClassIdentifier: UnityEditor.dll::UnityEditor.Build.Profile.BuildProfile
m_AssetVersion: 1
m_BuildTarget: 9
m_Subtarget: 0
m_PlatformId: ad48d16a66894befa4d8181998c3cb09
m_PlatformBuildProfile:
rid: 3475452038477774988
m_OverrideGlobalSceneList: 0
m_Scenes: []
m_ScriptingDefines: []
m_PlayerSettingsYaml:
m_Settings: []
references:
version: 2
RefIds:
- rid: 3475452038477774988
type: {class: iOSPlatformSettings, ns: UnityEditor.iOS, asm: UnityEditor.iOS.Extensions}
data:
m_Development: 0
m_ConnectProfiler: 0
m_BuildWithDeepProfilingSupport: 0
m_AllowDebugging: 0
m_WaitForManagedDebugger: 0
m_ManagedDebuggerFixedPort: 0
m_ExplicitNullChecks: 0
m_ExplicitDivideByZeroChecks: 0
m_ExplicitArrayBoundsChecks: 0
m_CompressionType: -1
m_InstallInBuildFolder: 0
m_InsightsSettingsContainer:
m_BuildProfileEngineDiagnosticsState: 2
m_iOSXcodeBuildConfig: 1
m_SymlinkSources: 0
m_PreferredXcode:
m_SymlinkTrampoline: 0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 110a4eabb37dbaa428e55c751696cd1e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,680 @@
# ManagedBehaviour System - Architecture Review
**Date:** November 10, 2025
**Reviewer:** Senior System Architect
**Status:** Analysis Complete - Awaiting Implementation Decisions
---
## Executive Summary
The ManagedBehaviour system is a **well-designed lifecycle orchestration framework** that successfully provides guaranteed execution order and lifecycle management for Unity MonoBehaviours. The core architecture is sound, but there are **several code quality issues** that should be addressed to improve maintainability, reduce cognitive overhead, and enhance developer experience.
**Overall Assessment:** ✅ Good foundation, needs refinement
**Complexity Rating:** Medium-High (could be simplified)
**Developer-Friendliness:** Medium (confusion points exist)
---
## 1. System Architecture Analysis
### Core Components
```
CustomBoot (Static Bootstrap)
↓ Creates
LifecycleManager (Singleton Orchestrator)
↓ Registers & Broadcasts to
ManagedBehaviour (Abstract Base Class)
↓ Inherited by
Concrete Game Components (AudioManager, InputManager, etc.)
```
### Lifecycle Flow
**Boot Phase:**
1. `[RuntimeInitializeOnLoadMethod]``CustomBoot.Initialise()`
2. `LifecycleManager.CreateInstance()` (before bootstrap)
3. Components register via `Awake()``LifecycleManager.Register()`
4. Bootstrap completes → `OnBootCompletionTriggered()`
5. `BroadcastManagedAwake()` → All components receive `OnManagedAwake()` in priority order
**Scene Transition Phase:**
1. `BeginSceneLoad(sceneName)` - Batching mode activated
2. New components register during scene load → Added to pending batch
3. `BroadcastSceneReady()` → Process batched components, then broadcast `OnSceneReady()`
**Save/Load Phase:**
- Scene saves: `BroadcastSceneSaveRequested()``OnSceneSaveRequested()`
- Global saves: `BroadcastGlobalSaveRequested()``OnGlobalSaveRequested()`
- Restores: Similar pattern with `OnSceneRestoreRequested()` and `OnGlobalRestoreRequested()`
### ✅ What Works Well
1. **Guaranteed Execution Order**: Priority-based sorted lists ensure deterministic execution
2. **Separation of Concerns**: Bootstrap, scene lifecycle, and save/load are clearly separated
3. **Automatic Registration**: Components auto-register in `Awake()`, reducing boilerplate
4. **Late Registration Support**: Components that spawn after boot/scene load are handled correctly
5. **Scene Batching**: Smart batching during scene load prevents premature initialization
6. **Auto-registration Features**: `AutoRegisterPausable` and `AutoRegisterForSave` reduce manual wiring
---
## 2. Problematic Code & Complexity Issues
### 🔴 CRITICAL: The `new` Keyword Pattern
**Location:** All singleton components inheriting from ManagedBehaviour
```csharp
// Current pattern in 16+ files
private new void Awake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
_instance = this;
}
```
**Problems:**
1. **Misleading Syntax**: `new` keyword hides the base method rather than overriding it
2. **Fragile**: If a derived class forgets `base.Awake()`, registration silently fails
3. **Inconsistent**: Some files use `private new`, some use `protected override` (confusing)
4. **Comment Dependency**: Requires "CRITICAL" comments because the pattern is error-prone
**Why It's Used:**
- `ManagedBehaviour.Awake()` is marked `protected virtual`
- Singletons need to set `_instance` in `Awake()` before `OnManagedAwake()` is called
- They use `new` to hide the base `Awake()` while still calling it
**Recommendation:** Change `ManagedBehaviour.Awake()` to be `private` and non-virtual. Introduce a new virtual hook like `OnBeforeRegister()` or `OnEarlyAwake()` that runs before registration. This eliminates the need for the `new` keyword pattern.
---
### 🟡 MEDIUM: Invoke Methods Bloat
**Location:** `ManagedBehaviour.cs` lines 89-99
```csharp
// Public wrappers to invoke protected lifecycle methods
public void InvokeManagedAwake() => OnManagedAwake();
public void InvokeSceneUnloading() => OnSceneUnloading();
public void InvokeSceneReady() => OnSceneReady();
public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
public void InvokeSceneRestoreCompleted() => OnSceneRestoreCompleted();
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
public void InvokeManagedDestroy() => OnManagedDestroy();
public void InvokeGlobalLoadCompleted() => OnGlobalLoadCompleted();
public void InvokeGlobalSaveStarted() => OnGlobalSaveStarted();
```
**Problems:**
1. **Code Duplication**: 11 one-liner wrapper methods
2. **Maintenance Burden**: Every new lifecycle hook requires a public wrapper
3. **Leaky Abstraction**: Exposes internal lifecycle to external callers (should only be LifecycleManager)
**Alternative Solutions:**
1. **Make lifecycle methods internal**: Use `internal virtual` instead of `protected virtual` - LifecycleManager can call directly (same assembly)
2. **Reflection**: Use reflection to invoke methods (performance cost, but cleaner API)
3. **Interface Segregation**: Break into multiple interfaces (IBootable, ISceneAware, ISaveable) - more flexible but more complex
**Recommendation:** Use `internal virtual` for lifecycle methods. LifecycleManager and ManagedBehaviour are in the same assembly (`Core.Lifecycle` namespace), so `internal` access is perfect. This eliminates all 11 wrapper methods.
---
### 🟡 MEDIUM: OnDestroy Pattern Confusion
**Location:** Multiple derived classes
**Current State:** Inconsistent override patterns
```csharp
// Pattern 1: Most common (correct)
protected override void OnDestroy()
{
base.OnDestroy(); // Unregisters from LifecycleManager
// Custom cleanup
if (SceneManagerService.Instance != null)
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
}
// Pattern 2: SaveLoadManager (also correct, but verbose)
protected override void OnDestroy()
{
base.OnDestroy(); // Important: call base to unregister from LifecycleManager
if (_instance == this)
_instance = null;
}
```
**Problems:**
1. **Manual Cleanup Required**: Developers must remember to call `base.OnDestroy()`
2. **No OnManagedDestroy Usage**: `OnManagedDestroy()` exists but is rarely used (only 1 reference in SceneManagerService)
3. **Redundant Comments**: Multiple files have comments reminding to call base (fragile pattern)
**Root Cause:** `OnManagedDestroy()` is called from within `ManagedBehaviour.OnDestroy()`, but most developers override `OnDestroy()` directly instead of using `OnManagedDestroy()`.
**Recommendation:**
- Make `OnDestroy()` sealed in `ManagedBehaviour` (or private)
- Encourage use of `OnManagedDestroy()` for all cleanup logic
- Document the difference clearly: `OnManagedDestroy()` = custom cleanup, `OnDestroy()` = framework cleanup (hands-off)
---
### 🟡 MEDIUM: AutoRegisterPausable Mechanism
**Location:** `ManagedBehaviour.cs` line 57, `LifecycleManager.cs` lines 599-609
```csharp
// ManagedBehaviour
public virtual bool AutoRegisterPausable => false;
// LifecycleManager
private void HandleAutoRegistrations(ManagedBehaviour component)
{
if (component.AutoRegisterPausable && component is AppleHills.Core.Interfaces.IPausable pausable)
{
if (GameManager.Instance != null)
{
GameManager.Instance.RegisterPausableComponent(pausable);
LogDebug($"Auto-registered IPausable: {component.gameObject.name}");
}
}
}
```
**Problems:**
1. **Tight Coupling**: `LifecycleManager` has a direct dependency on `GameManager` and `IPausable` interface
2. **Hidden Dependency**: Not obvious from LifecycleManager that it depends on GameManager existing
3. **Single Purpose**: Only handles one type of auto-registration (pausable), but there could be more
4. **Unregister in Base Class**: `ManagedBehaviour.OnDestroy()` also handles pausable unregistration (split responsibility)
**Alternative Approaches:**
1. **Event-Based**: Fire an event after `OnManagedAwake()` that GameManager listens to
2. **Reflection/Attributes**: Use attributes like `[AutoRegisterPausable]` and scan for them
3. **Remove Feature**: Components can register themselves in `OnManagedAwake()` (one line of code)
**Recommendation:** Consider removing `AutoRegisterPausable`. It saves one line of code (`GameManager.Instance.RegisterPausableComponent(this)`) but adds complexity. Most components that implement `IPausable` will want to register anyway, and explicit is better than implicit.
---
### 🟡 MEDIUM: Priority Property Repetition
**Location:** `ManagedBehaviour.cs` lines 13-50
```csharp
// 6 nearly identical priority properties
public virtual int ManagedAwakePriority => 100;
public virtual int SceneUnloadingPriority => 100;
public virtual int SceneReadyPriority => 100;
public virtual int SavePriority => 100;
public virtual int RestorePriority => 100;
public virtual int DestroyPriority => 100;
```
**Problems:**
1. **Repetitive**: 6 properties that do essentially the same thing
2. **Overhead**: Most components only care about 1-2 priorities (usually `ManagedAwakePriority`)
3. **Cognitive Load**: Developers must understand all 6 priorities even if they only use one
**Is This Over-Engineered?**
- **Pro**: Provides fine-grained control over each lifecycle phase
- **Con**: In practice, most components use default (100) for everything except `ManagedAwakePriority`
- **Con**: Save/Restore priorities are rarely customized (mostly manager-level components)
**Recommendation:** Consider consolidating to 2-3 priorities:
- `Priority` (general, affects ManagedAwake, SceneReady, Save, Restore)
- `UnloadPriority` (affects SceneUnloading, Destroy - reverse order)
- Alternatively: Use attributes like `[LifecyclePriority(Phase.ManagedAwake, 20)]` for granular control only when needed
---
### 🟢 MINOR: GetPriorityForList Helper Method
**Location:** `LifecycleManager.cs` lines 639-649
```csharp
private int GetPriorityForList(ManagedBehaviour component, List<ManagedBehaviour> list)
{
if (list == managedAwakeList) return component.ManagedAwakePriority;
if (list == sceneUnloadingList) return component.SceneUnloadingPriority;
if (list == sceneReadyList) return component.SceneReadyPriority;
if (list == saveRequestedList) return component.SavePriority;
if (list == restoreRequestedList) return component.RestorePriority;
if (list == destroyList) return component.DestroyPriority;
return 100;
}
```
**Problems:**
1. **Brittle**: Relies on reference equality checks (works but fragile)
2. **Default Value**: Returns 100 if no match (could hide bugs)
**Recommendation:** Use a dictionary or enum-based lookup. Better yet, if priorities are consolidated (see above), this method becomes simpler or unnecessary.
---
### 🟢 MINOR: InsertSorted Performance
**Location:** `LifecycleManager.cs` lines 620-638
```csharp
private void InsertSorted(List<ManagedBehaviour> list, ManagedBehaviour component, int priority)
{
// Simple linear insertion for now (can optimize with binary search later if needed)
int index = 0;
for (int i = 0; i < list.Count; i++)
{
int existingPriority = GetPriorityForList(list[i], list);
if (priority < existingPriority)
{
index = i;
break;
}
index = i + 1;
}
list.Insert(index, component);
}
```
**Problems:**
1. **O(n) insertion**: Linear search for insertion point
2. **Comment Admits It**: "can optimize with binary search later if needed"
**Is This a Problem?**
- Probably not: Registration happens during Awake/scene load (not runtime-critical)
- Typical projects have 10-100 managed components per scene (O(n) is fine)
- Premature optimization warning: Don't fix unless proven bottleneck
**Recommendation:** Leave as-is unless profiling shows it's a problem. Add a comment explaining why linear is acceptable.
---
### 🟢 MINOR: SaveId Generation Logic
**Location:** `ManagedBehaviour.cs` lines 70-78
```csharp
public virtual string SaveId
{
get
{
string sceneName = gameObject.scene.IsValid() ? gameObject.scene.name : "UnknownScene";
string componentType = GetType().Name;
return $"{sceneName}/{gameObject.name}/{componentType}";
}
}
```
**Problems:**
1. **Runtime Allocation**: Allocates a new string every time it's accessed
2. **Mutable Path Components**: GameObject name can change at runtime (breaks save system)
3. **Collision Risk**: Two objects with same name + type in same scene = collision
**Is This a Problem?**
- Yes: Save IDs should be stable and unique
- No: Most components override this for singletons (e.g., `"PlayerController"`)
**Recommendation:**
1. Cache the SaveId in Awake (don't regenerate on every call)
2. Add validation warnings if GameObject name changes after registration
3. Consider GUID-based IDs for instance-based components (prefabs spawned at runtime)
---
## 3. Code Style Issues
### 🎨 Region Overuse
**Location:** Both `ManagedBehaviour.cs` and `LifecycleManager.cs`
**Current State:**
- `ManagedBehaviour.cs`: 6 regions (Priority Properties, Configuration, Public Accessors, Private Fields, Unity Lifecycle, Managed Lifecycle)
- `LifecycleManager.cs`: 8 regions (Singleton, Lifecycle Lists, Tracking Dictionaries, State Flags, Unity Lifecycle, Registration, Broadcast Methods, Auto-Registration, Helper Methods)
**Opinion:**
- **Pro**: Helps organize long files
- **Con**: Regions are a code smell suggesting the file is doing too much
- **Modern Practice**: Prefer smaller, focused classes over heavily regioned classes
**Recommendation:** Regions are acceptable for these orchestrator classes, but consider:
- Moving priority properties to a separate struct/class (`LifecyclePriorities`)
- Moving auto-registration logic to a separate service
---
### 🎨 Documentation Quality
**Overall:** ✅ Excellent!
**Strengths:**
- Comprehensive XML comments on all public/protected members
- Clear "GUARANTEE" and "TIMING" sections in lifecycle hook docs
- Good use of examples and common patterns
- Warnings about synchronous vs async behavior
**Minor Issues:**
1. Some comments are overly verbose (e.g., `OnSceneRestoreCompleted` has a paragraph explaining async guarantees)
2. "IMPORTANT:" and "GUARANTEE:" prefixes could be standardized
**Recommendation:** Keep the thorough documentation style. Consider extracting complex documentation into markdown files (like you have in `docs/`) and linking to them.
---
### 🎨 Naming Conventions
**Mostly Consistent:**
- Protected methods: `OnManagedAwake()`, `OnSceneReady()`
- Invoke wrappers: `InvokeManagedAwake()`, `InvokeSceneReady()`
- Priority properties: `ManagedAwakePriority`, `SceneReadyPriority`
**Inconsistencies:**
- `OnBootCompletionTriggered()` (passive voice) vs `BroadcastManagedAwake()` (active voice)
- `currentSceneReady` (camelCase field) vs `_instance` (underscore prefix)
**Recommendation:** Minor cleanup pass for consistency (not critical).
---
## 4. Missing Features / Potential Enhancements
### 🔧 No Update/FixedUpdate Management
**Observation:** The system manages initialization and shutdown, but not per-frame updates.
**Question:** Should `ManagedBehaviour` provide ordered Update loops?
**Trade-offs:**
- **Pro**: Could guarantee update order (e.g., InputManager before PlayerController)
- **Con**: Adds performance overhead (one dispatch loop per frame)
- **Con**: Unity's native Update is very optimized (hard to beat)
**Recommendation:** Don't add unless there's a proven need. Most update-order issues can be solved with ScriptExecutionOrder settings.
---
### 🔧 No Pause/Resume Lifecycle Hooks
**Observation:** `IPausable` exists and is auto-registered, but there are no lifecycle hooks for pause/resume events.
**Current State:** Components must implement `IPausable` interface and handle pause/resume manually.
**Potential Enhancement:**
```csharp
protected virtual void OnGamePaused() { }
protected virtual void OnGameResumed() { }
```
**Recommendation:** Consider adding if pause/resume is a common pattern. Alternatively, components can subscribe to GameManager events in `OnManagedAwake()`.
---
### 🔧 Limited Error Recovery
**Observation:** If a component throws in `OnManagedAwake()`, the error is logged but the system continues.
**Current State:**
```csharp
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
}
```
**Trade-offs:**
- **Pro**: Resilient - one component failure doesn't crash the game
- **Con**: Silent failures can be hard to debug
- **Con**: Components might be in invalid state if initialization failed
**Recommendation:** Consider adding:
1. A flag on component: `HasInitialized` / `InitializationFailed`
2. An event: `OnComponentInitializationFailed(component, exception)`
3. Editor-only hard failures (#if UNITY_EDITOR throw; #endif)
---
## 5. Behavioral Correctness
### ✅ Execution Order: CORRECT
**Boot Components:**
1. All components register during their `Awake()` (sorted by AwakePriority)
2. Bootstrap completes
3. `OnBootCompletionTriggered()``BroadcastManagedAwake()`
4. Components receive `OnManagedAwake()` in priority order (20, 25, 30, 100, etc.)
**Late Registration (Component enabled after boot):**
1. Component's `Awake()` calls `Register()`
2. If boot complete: `OnManagedAwake()` called immediately
3. Component is added to all lifecycle lists
**Scene Load:**
1. `BeginSceneLoad("SceneName")` - batching mode ON
2. Scene loads → New components register → Added to pending batch
3. `BroadcastSceneReady("SceneName")`
4. Batched components processed in priority order → `OnManagedAwake()` called
5. All components (batched + existing) receive `OnSceneReady()`
**Assessment:** ✅ Logic is sound and well-tested
---
### ✅ Save/Load: CORRECT
**Scene Save (During Transition):**
1. `BroadcastSceneSaveRequested()` iterates all components with `AutoRegisterForSave == true`
2. Calls `OnSceneSaveRequested()` → Collects returned data
3. Returns `Dictionary<saveId, serializedData>`
**Scene Restore:**
1. `BroadcastSceneRestoreRequested(saveData)` distributes data by SaveId
2. Calls `OnSceneRestoreRequested(data)` for matching components
3. Calls `BroadcastSceneRestoreCompleted()` → All components receive `OnSceneRestoreCompleted()`
**Global Save/Load:** Same pattern but uses `OnGlobalSaveRequested()` / `OnGlobalRestoreRequested()`
**Assessment:** ✅ Separation of scene vs global state is clean
---
### ⚠️ Unregister Timing: POTENTIAL ISSUE
**Scenario:** Component is destroyed during `BroadcastManagedAwake()`
**Current Protection:**
```csharp
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
foreach (var component in componentsCopy)
{
if (component == null) continue; // Null check protects against destroyed objects
component.InvokeManagedAwake();
}
```
**Protection Mechanism:** Copies list before iteration + null checks
**Assessment:** ✅ Handles collection modification correctly
**Minor Issue:** If a component is destroyed, it still remains in `managedAwakeList` copy (but null check prevents execution). The real list is cleaned up when `Unregister()` is called from `OnDestroy()`.
---
### ⚠️ AutoRegisterPausable Unregister: ASYMMETRY
**Registration:**
- Happens in `LifecycleManager.HandleAutoRegistrations()` (after `OnManagedAwake()`)
**Unregistration:**
- Happens in `ManagedBehaviour.OnDestroy()` directly
```csharp
if (AutoRegisterPausable && this is IPausable pausable)
{
GameManager.Instance?.UnregisterPausableComponent(pausable);
}
```
**Issue:** Registration and unregistration logic is split between two classes.
**Recommendation:** Move unregistration to LifecycleManager for symmetry. Call `InvokeManagedDestroy()` before automatic cleanup.
---
## 6. Developer Experience Issues
### 😕 Confusion Point: Awake vs OnManagedAwake
**Common Question:** "When should I use `Awake()` vs `OnManagedAwake()`?"
**Answer:**
- `Awake()`: Set singleton instance, early initialization (before bootstrap)
- `OnManagedAwake()`: Initialization that depends on other systems (after bootstrap)
**Problem:** Requires understanding of bootstrap sequencing (not obvious to new developers)
**Recommendation:**
1. Improve docs with flowchart diagram
2. Add validation: If component accesses `GameManager.Instance` in `Awake()`, warn that it should be in `OnManagedAwake()`
---
### 😕 Confusion Point: OnDestroy vs OnManagedDestroy
**Current Usage:** Only 1 file uses `OnManagedDestroy()` (SceneManagerService)
**Most files override `OnDestroy()`:**
```csharp
protected override void OnDestroy()
{
base.OnDestroy(); // Must remember this!
// Custom cleanup
}
```
**Problem:** The `OnManagedDestroy()` hook exists but isn't being used as intended.
**Recommendation:**
1. Make `OnDestroy()` sealed (force use of `OnManagedDestroy()`)
2. Or deprecate `OnManagedDestroy()` entirely (seems redundant)
---
### 😕 Confusion Point: SaveId Customization
**Default Behavior:** `"SceneName/GameObjectName/ComponentType"`
**Comment Says:** "Override ONLY for special cases (e.g., singletons like 'PlayerController', or custom IDs)"
**Reality:** Many components don't realize they need custom SaveIds until save data collides.
**Recommendation:**
1. Add editor validation: Detect duplicate SaveIds and show warnings
2. Better yet: Generate GUIDs for components that don't override SaveId
3. Document the collision risks more prominently
---
## 7. Recommendations Summary
### 🔴 High Priority (Fix These)
1. **Eliminate the `new` keyword pattern:**
- Make `ManagedBehaviour.Awake()` private and non-virtual
- Add `protected virtual void OnPreRegister()` hook for singletons to set instances
- Reduces fragility and removes "CRITICAL" comment dependency
2. **Seal `OnDestroy()` or deprecate `OnManagedDestroy()`:**
- Current dual pattern confuses developers
- Choose one approach and enforce it
3. **Fix AutoRegister asymmetry:**
- Move unregistration to LifecycleManager for symmetry
- Or remove AutoRegisterPausable entirely (explicit > implicit)
---
### 🟡 Medium Priority (Should Do)
4. **Replace Invoke wrappers with `internal virtual` methods:**
- Eliminates 11 one-liner methods
- Cleaner API surface
5. **Consolidate priority properties:**
- Most components only customize one priority
- Reduce to 2-3 priorities or use attributes
6. **Cache SaveId:**
- Don't regenerate on every access
- Validate uniqueness in editor
---
### 🟢 Low Priority (Nice to Have)
7. **Improve developer documentation:**
- Add flowchart for lifecycle phases
- Create visual diagram of execution order
- Add common pitfalls section
8. **Add editor validation:**
- Warn if SaveId collisions detected
- Warn if base.OnDestroy() not called
- Warn if GameManager accessed in Awake()
9. **Performance optimization:**
- Binary search for InsertSorted (only if profiling shows need)
- Cache priority lookups
---
## 8. Final Verdict
### What You Asked For:
1.**Thorough analysis of the code** - Complete
2.**Summary of logic and expected behavior** - Confirmed correct
3.**Problematic code identification** - 7 issues found (3 high, 4 medium)
4.**Code style improvements** - Documentation, regions, naming reviewed
### Overall Assessment:
**Architecture:** ✅ Solid
**Implementation:** ⚠️ Needs refinement
**Developer Experience:** ⚠️ Can be improved
The system **behaves as expected** and provides real value (guaranteed execution order, clean lifecycle hooks, save/load integration). However, there are **code smell issues** that increase complexity and cognitive load:
- The `new` keyword pattern is fragile
- Invoke wrapper bloat
- Dual OnDestroy patterns
- AutoRegister coupling
These are **fixable without major refactoring**. The core architecture doesn't need to change.
### Is It Over-Engineered?
**No, but it's close to the line.**
- 6 priority properties = probably too granular (most are unused)
- 11 invoke wrappers = definitely unnecessary (use `internal virtual`)
- AutoRegisterPausable = debatable (saves 1 line of code, adds coupling)
- Batching system = justified (prevents race conditions during scene load)
- Priority-sorted lists = justified (core value proposition)
### Tight, Developer-Friendly, Not Over-Engineered Code:
You're **80% there**. The fixes I've outlined will get you to **95%**. The remaining 5% is personal preference (e.g., regions vs no regions).
---
## Next Steps
**Before I implement anything:**
1. Which of these issues do you want fixed? (All high priority? Some medium?)
2. Do you want me to make the changes, or just provide guidance?
3. Any architectural decisions you want to discuss first? (e.g., keep or remove AutoRegisterPausable?)
I'm ready to execute once you provide direction. 🚀