Add distinction between managed awake and managed start
This commit is contained in:
@@ -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
|
||||
///
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 943b203cde5343c68a6278c111fce2ed
|
||||
timeCreated: 1757508162
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f67e06e997f642509ba61ea12b0f793e
|
||||
timeCreated: 1757503955
|
||||
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
8
Assets/Settings/Build Profiles.meta
Normal file
8
Assets/Settings/Build Profiles.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 53ec386bba82c1748886a5beb8468ecf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/Settings/Build Profiles/iOS.asset
Normal file
48
Assets/Settings/Build Profiles/iOS.asset
Normal 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
|
||||
8
Assets/Settings/Build Profiles/iOS.asset.meta
Normal file
8
Assets/Settings/Build Profiles/iOS.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 110a4eabb37dbaa428e55c751696cd1e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
680
docs/wip/managed_behavior.md
Normal file
680
docs/wip/managed_behavior.md
Normal 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. 🚀
|
||||
Reference in New Issue
Block a user