Compare commits

..

2 Commits

Author SHA1 Message Date
Michal Pikulski
dcf8c8bb87 Add documentation 2025-11-11 13:32:02 +01:00
Michal Pikulski
9a6914b9bd Final touchups to the lifecycle management 2025-11-11 10:53:09 +01:00
55 changed files with 1365 additions and 464 deletions

3
.gitignore vendored
View File

@@ -104,3 +104,6 @@ InitTestScene*.unity*
.vscode/launch.json .vscode/launch.json
.vscode/settings.json .vscode/settings.json
.idea/.idea.AppleHillsProduction/.idea/indexLayout.xml .idea/.idea.AppleHillsProduction/.idea/indexLayout.xml
# WIP docs
/docs/wip/

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,156 +0,0 @@
fileFormatVersion: 2
guid: 57224356af3d045dbbf8b420e13b4b10
TextureImporter:
internalIDToNameTable:
- first:
213: 2364528222304962155
second: ramasjang_icon_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 2
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: ramasjang_icon_0
rect:
serializedVersion: 2
x: 4
y: 5
width: 334
height: 334
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: b668fdfe3ed70d020800000000000000
internalID: 2364528222304962155
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
ramasjang_icon_0: 2364528222304962155
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -725,9 +725,10 @@ GameObject:
m_Component: m_Component:
- component: {fileID: 3864057818161790164} - component: {fileID: 3864057818161790164}
- component: {fileID: 5271824036850954050} - component: {fileID: 5271824036850954050}
- component: {fileID: 8081783206361873868}
- component: {fileID: 5548642987123338363}
- component: {fileID: 3058107077406709872} - component: {fileID: 3058107077406709872}
- component: {fileID: 587711432829270645} - component: {fileID: 587711432829270645}
- component: {fileID: 4189849640380816173}
m_Layer: 5 m_Layer: 5
m_Name: Icon m_Name: Icon
m_TagString: Untagged m_TagString: Untagged
@@ -762,6 +763,53 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4599222264323240281} m_GameObject: {fileID: 4599222264323240281}
m_CullTransparentMesh: 1 m_CullTransparentMesh: 1
--- !u!114 &8081783206361873868
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4599222264323240281}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Texture: {fileID: 0}
m_UVRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: -1
--- !u!114 &5548642987123338363
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4599222264323240281}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2fd09147b9e9d42a48d6ddc915ddc3d2, type: 3}
m_Name:
m_EditorClassIdentifier: SkiaSharp.Unity::SkiaSharp.Unity.SkottiePlayerV2
lottieFile: {fileID: 4900000, guid: 50e22b5bb8a496840952f2563758c13c, type: 3}
customResolution: 0
resWidth: 250
resHeight: 250
stateName: Idle
resetAfterFinished: 0
autoPlay: 1
loop: 1
--- !u!114 &3058107077406709872 --- !u!114 &3058107077406709872
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -802,7 +850,7 @@ MonoBehaviour:
m_SelectedTrigger: Selected m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled m_DisabledTrigger: Disabled
m_Interactable: 1 m_Interactable: 1
m_TargetGraphic: {fileID: 0} m_TargetGraphic: {fileID: 8081783206361873868}
m_OnClick: m_OnClick:
m_PersistentCalls: m_PersistentCalls:
m_Calls: m_Calls:
@@ -832,36 +880,6 @@ MonoBehaviour:
m_EditorClassIdentifier: AppleHillsScripts::UI.HudMenuButton m_EditorClassIdentifier: AppleHillsScripts::UI.HudMenuButton
pagePrefab: {fileID: 1498581815400593087, guid: ccd858c7962d48147b0233c1bf1382f5, type: 3} pagePrefab: {fileID: 1498581815400593087, guid: ccd858c7962d48147b0233c1bf1382f5, type: 3}
buttonName: RamaSjang Button buttonName: RamaSjang Button
--- !u!114 &4189849640380816173
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4599222264323240281}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 2364528222304962155, guid: 57224356af3d045dbbf8b420e13b4b10, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &5113586844274188410 --- !u!1 &5113586844274188410
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -148,6 +148,8 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 6108475066390421500}
- {fileID: 6717870941799174515}
- {fileID: 4136733570236406132} - {fileID: 4136733570236406132}
m_Father: {fileID: 1315170081792486277} m_Father: {fileID: 1315170081792486277}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
@@ -400,8 +402,8 @@ MonoBehaviour:
m_EditorClassIdentifier: '::' m_EditorClassIdentifier: '::'
PageName: PageName:
transitionDuration: 0.3 transitionDuration: 0.3
rainbowIn: {fileID: 0} rainbowIn: {fileID: 5382650426034128680}
rainbowOut: {fileID: 0} rainbowOut: {fileID: 3983328028282460839}
gameLayoutContainer: {fileID: 904161782565348054} gameLayoutContainer: {fileID: 904161782565348054}
exitButton: {fileID: 8427602740714176801} exitButton: {fileID: 8427602740714176801}
rectMask: {fileID: 7425566603516801919} rectMask: {fileID: 7425566603516801919}
@@ -526,6 +528,99 @@ MonoBehaviour:
m_OnClick: m_OnClick:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
--- !u!1 &3983328028282460839
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6717870941799174515}
- component: {fileID: 6334910097310371923}
- component: {fileID: 8821021733826365168}
- component: {fileID: 9163925342151684341}
m_Layer: 5
m_Name: RainbowOut
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6717870941799174515
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3983328028282460839}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8293076336493130222}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6334910097310371923
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3983328028282460839}
m_CullTransparentMesh: 1
--- !u!114 &8821021733826365168
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3983328028282460839}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Texture: {fileID: 0}
m_UVRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: -1
--- !u!114 &9163925342151684341
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3983328028282460839}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2fd09147b9e9d42a48d6ddc915ddc3d2, type: 3}
m_Name:
m_EditorClassIdentifier: SkiaSharp.Unity::SkiaSharp.Unity.SkottiePlayerV2
lottieFile: {fileID: 4900000, guid: 589505308c5daf449800f30dd4b92ce7, type: 3}
customResolution: 0
resWidth: 250
resHeight: 250
stateName:
resetAfterFinished: 0
autoPlay: 0
loop: 0
--- !u!1 &4270065472017787841 --- !u!1 &4270065472017787841
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -841,6 +936,99 @@ MonoBehaviour:
m_OnClick: m_OnClick:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
--- !u!1 &5382650426034128680
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6108475066390421500}
- component: {fileID: 2398693306920598044}
- component: {fileID: 7920249735731934357}
- component: {fileID: 3566391948883171773}
m_Layer: 5
m_Name: RainbowIn
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6108475066390421500
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5382650426034128680}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8293076336493130222}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2398693306920598044
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5382650426034128680}
m_CullTransparentMesh: 1
--- !u!114 &7920249735731934357
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5382650426034128680}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1344c3c82d62a2a41a3576d8abb8e3ea, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Texture: {fileID: 0}
m_UVRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: -1
--- !u!114 &3566391948883171773
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5382650426034128680}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2fd09147b9e9d42a48d6ddc915ddc3d2, type: 3}
m_Name:
m_EditorClassIdentifier: SkiaSharp.Unity::SkiaSharp.Unity.SkottiePlayerV2
lottieFile: {fileID: 4900000, guid: 622be2ef9d5e27d45a9deaf7ed805f5f, type: 3}
customResolution: 0
resWidth: 250
resHeight: 250
stateName:
resetAfterFinished: 0
autoPlay: 0
loop: 0
--- !u!1 &5867455130109727138 --- !u!1 &5867455130109727138
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -11,6 +11,9 @@
"OptimizedRope", "OptimizedRope",
"AudioSourceEvents", "AudioSourceEvents",
"NewAssembly", "NewAssembly",
"SkiaSharp.Unity",
"SkiaSharp.Editor",
"SkiaSharp",
"Unity.Cinemachine" "Unity.Cinemachine"
], ],
"includePlatforms": [], "includePlatforms": [],

View File

@@ -30,8 +30,6 @@ namespace Bootstrap
private float _sceneLoadingProgress = 0f; private float _sceneLoadingProgress = 0f;
private LogVerbosity _logVerbosity = LogVerbosity.Warning; private LogVerbosity _logVerbosity = LogVerbosity.Warning;
// Run very early - need to set up loading screen before other systems initialize
public override int ManagedAwakePriority => 5;
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
@@ -83,10 +81,8 @@ namespace Bootstrap
Invoke(nameof(StartLoadingMainMenu), minDelayAfterBoot); Invoke(nameof(StartLoadingMainMenu), minDelayAfterBoot);
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Manual cleanup for events // Manual cleanup for events
if (initialLoadingScreen != null) if (initialLoadingScreen != null)
{ {

View File

@@ -37,8 +37,6 @@ namespace Cinematics
public PlayableDirector playableDirector; public PlayableDirector playableDirector;
public override int ManagedAwakePriority => 170; // Cinematic systems
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
// Set instance immediately (early initialization) // Set instance immediately (early initialization)

View File

@@ -15,8 +15,6 @@ namespace Cinematics
private float _holdStartTime; private float _holdStartTime;
private bool _isHolding; private bool _isHolding;
private bool _skipPerformed; private bool _skipPerformed;
public override int ManagedAwakePriority => 180; // Cinematic UI
internal override void OnManagedStart() internal override void OnManagedStart()
{ {
@@ -32,10 +30,8 @@ namespace Cinematics
Logging.Debug("[SkipCinematic] Initialized"); Logging.Debug("[SkipCinematic] Initialized");
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Clean up subscriptions // Clean up subscriptions
UnsubscribeFromCinematicsEvents(); UnsubscribeFromCinematicsEvents();
} }

View File

@@ -34,8 +34,6 @@ namespace Core
public event Action OnGamePaused; public event Action OnGamePaused;
public event Action OnGameResumed; public event Action OnGameResumed;
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 10; // Core infrastructure - runs early
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {

View File

@@ -47,8 +47,6 @@ namespace Core
// Broadcasts when any two items are successfully combined // Broadcasts when any two items are successfully combined
// Args: first item data, second item data, result item data // Args: first item data, second item data, result item data
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined; public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
public override int ManagedAwakePriority => 75; // Item registry
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
@@ -67,10 +65,8 @@ namespace Core
ClearAllRegistrations(); ClearAllRegistrations();
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Ensure we clean up any subscriptions from registered items when the manager is destroyed // Ensure we clean up any subscriptions from registered items when the manager is destroyed
ClearAllRegistrations(); ClearAllRegistrations();
} }

View File

@@ -6,12 +6,19 @@
/// </summary> /// </summary>
public enum LifecyclePhase public enum LifecyclePhase
{ {
/// <summary>
/// Called immediately during registration (during Awake).
/// Use for early initialization such as setting singleton instances.
/// NOT ordered - fires whenever Unity calls this component's Awake().
/// </summary>
ManagedAwake,
/// <summary> /// <summary>
/// Called once per component after bootstrap completes. /// Called once per component after bootstrap completes.
/// Guaranteed to be called after all bootstrap resources are loaded. /// Guaranteed to be called after all bootstrap resources are loaded.
/// For late-registered components, called immediately upon registration. /// For late-registered components, called immediately upon registration.
/// </summary> /// </summary>
ManagedAwake, ManagedStart,
/// <summary> /// <summary>
/// Called before a scene is unloaded. /// Called before a scene is unloaded.

View File

@@ -59,11 +59,11 @@ namespace Core.Lifecycle
#region State Flags #region State Flags
private bool isBootComplete = false; private bool isBootComplete;
private string currentSceneReady = ""; private string currentSceneReady = "";
// Scene loading state tracking // Scene loading state tracking
private bool isLoadingScene = false; private bool isLoadingScene;
private string sceneBeingLoaded = ""; private string sceneBeingLoaded = "";
private List<ManagedBehaviour> pendingSceneComponents = new List<ManagedBehaviour>(); private List<ManagedBehaviour> pendingSceneComponents = new List<ManagedBehaviour>();
@@ -120,17 +120,13 @@ namespace Core.Lifecycle
// Track which scene this component belongs to // Track which scene this component belongs to
componentScenes[component] = sceneName; componentScenes[component] = sceneName;
// ALWAYS add to managedAwakeList - this is the master list used for save/load // Add to all lifecycle lists (order of registration determines execution order)
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority); managedAwakeList.Add(component);
sceneUnloadingList.Add(component);
// Register for all scene lifecycle hooks sceneReadyList.Add(component);
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority); saveRequestedList.Add(component);
InsertSorted(sceneReadyList, component, component.SceneReadyPriority); restoreRequestedList.Add(component);
InsertSorted(saveRequestedList, component, component.SavePriority); destroyList.Add(component);
InsertSorted(restoreRequestedList, component, component.RestorePriority);
InsertSorted(destroyList, component, component.DestroyPriority);
// Call OnManagedAwake immediately after registration (early initialization hook)
try try
{ {
component.OnManagedAwake(); component.OnManagedAwake();
@@ -146,7 +142,7 @@ namespace Core.Lifecycle
// Check if we're currently loading a scene // Check if we're currently loading a scene
if (isLoadingScene && sceneName == sceneBeingLoaded) if (isLoadingScene && sceneName == sceneBeingLoaded)
{ {
// Batch this component - will be processed in priority order when scene load completes // Batch this component - will be processed when scene load completes
pendingSceneComponents.Add(component); pendingSceneComponents.Add(component);
LogDebug($"Batched component for scene load: {component.gameObject.name} (Scene: {sceneName})"); LogDebug($"Batched component for scene load: {component.gameObject.name} (Scene: {sceneName})");
} }
@@ -282,10 +278,7 @@ namespace Core.Lifecycle
LogDebug($"Processing {pendingSceneComponents.Count} batched components for scene: {sceneBeingLoaded}"); LogDebug($"Processing {pendingSceneComponents.Count} batched components for scene: {sceneBeingLoaded}");
// Sort by ManagedAwake priority (lower values first) // Call OnManagedStart in registration order
pendingSceneComponents.Sort((a, b) => a.ManagedAwakePriority.CompareTo(b.ManagedAwakePriority));
// Call OnManagedStart in priority order
foreach (var component in pendingSceneComponents) foreach (var component in pendingSceneComponents)
{ {
if (component == null) continue; if (component == null) continue;
@@ -294,7 +287,7 @@ namespace Core.Lifecycle
{ {
component.OnManagedStart(); component.OnManagedStart();
HandleAutoRegistrations(component); HandleAutoRegistrations(component);
LogDebug($"Processed batched component: {component.gameObject.name} (Priority: {component.ManagedAwakePriority})"); LogDebug($"Processed batched component: {component.gameObject.name}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -309,7 +302,7 @@ namespace Core.Lifecycle
} }
/// <summary> /// <summary>
/// Broadcast OnSceneUnloading to components in the specified scene (reverse priority order). /// Broadcast OnSceneUnloading to components in the specified scene.
/// </summary> /// </summary>
public void BroadcastSceneUnloading(string sceneName) public void BroadcastSceneUnloading(string sceneName)
{ {
@@ -336,8 +329,8 @@ namespace Core.Lifecycle
} }
/// <summary> /// <summary>
/// Broadcast OnSceneReady to components in the specified scene (priority order). /// Broadcast OnSceneReady to components in the specified scene.
/// If scene loading mode is active, processes batched components first. /// Processes batched components first, then calls OnSceneReady on all components in that scene.
/// </summary> /// </summary>
public void BroadcastSceneReady(string sceneName) public void BroadcastSceneReady(string sceneName)
{ {
@@ -621,42 +614,6 @@ namespace Core.Lifecycle
#endregion #endregion
#region Helper Methods #region Helper Methods
/// <summary>
/// Insert component into list maintaining sorted order by priority.
/// Uses binary search for efficient insertion.
/// </summary>
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);
}
/// <summary>
/// Get the priority value for a component based on which list it's in.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Log debug message if debug logging is enabled. /// Log debug message if debug logging is enabled.

View File

@@ -8,46 +8,6 @@ namespace Core.Lifecycle
/// </summary> /// </summary>
public abstract class ManagedBehaviour : MonoBehaviour public abstract class ManagedBehaviour : MonoBehaviour
{ {
#region Priority Properties
/// <summary>
/// Priority for OnManagedStart (lower values execute first).
/// Default: 100
/// </summary>
public virtual int ManagedAwakePriority => 100;
/// <summary>
/// Priority for OnSceneUnloading (executed in reverse: higher values execute first).
/// Default: 100
/// </summary>
public virtual int SceneUnloadingPriority => 100;
/// <summary>
/// Priority for OnSceneReady (lower values execute first).
/// Default: 100
/// </summary>
public virtual int SceneReadyPriority => 100;
/// <summary>
/// Priority for OnSaveRequested (executed in reverse: higher values execute first).
/// Default: 100
/// </summary>
public virtual int SavePriority => 100;
/// <summary>
/// Priority for OnRestoreRequested (lower values execute first).
/// Default: 100
/// </summary>
public virtual int RestorePriority => 100;
/// <summary>
/// Priority for OnManagedDestroy (executed in reverse: higher values execute first).
/// Default: 100
/// </summary>
public virtual int DestroyPriority => 100;
#endregion
#region Configuration Properties #region Configuration Properties
/// <summary> /// <summary>
@@ -67,14 +27,19 @@ namespace Core.Lifecycle
/// Unique identifier for this component in the save system. /// Unique identifier for this component in the save system.
/// Default: "SceneName/GameObjectName/ComponentType" /// Default: "SceneName/GameObjectName/ComponentType"
/// Override ONLY for special cases (e.g., singletons like "PlayerController", or custom IDs). /// Override ONLY for special cases (e.g., singletons like "PlayerController", or custom IDs).
/// Cached on first access to avoid runtime allocation.
/// </summary> /// </summary>
public virtual string SaveId public virtual string SaveId
{ {
get get
{ {
string sceneName = gameObject.scene.IsValid() ? gameObject.scene.name : "UnknownScene"; if (_cachedSaveId == null)
string componentType = GetType().Name; {
return $"{sceneName}/{gameObject.name}/{componentType}"; string sceneName = gameObject.scene.IsValid() ? gameObject.scene.name : "UnknownScene";
string componentType = GetType().Name;
_cachedSaveId = $"{sceneName}/{gameObject.name}/{componentType}";
}
return _cachedSaveId;
} }
} }
@@ -83,6 +48,7 @@ namespace Core.Lifecycle
#region Private Fields #region Private Fields
private bool _isRegistered; private bool _isRegistered;
private string _cachedSaveId;
#endregion #endregion
@@ -107,13 +73,16 @@ namespace Core.Lifecycle
/// <summary> /// <summary>
/// Unity OnDestroy - automatically unregisters and cleans up. /// Unity OnDestroy - automatically unregisters and cleans up.
/// IMPORTANT: Derived classes that override OnDestroy MUST call base.OnDestroy() /// SEALED: Cannot be overridden. Use OnManagedDestroy() for custom cleanup logic.
/// </summary> /// </summary>
protected virtual void OnDestroy() private void OnDestroy()
{ {
if (!_isRegistered) if (!_isRegistered)
return; return;
// Call managed destroy hook
OnManagedDestroy();
// Unregister from LifecycleManager // Unregister from LifecycleManager
if (LifecycleManager.Instance != null) if (LifecycleManager.Instance != null)
{ {
@@ -149,7 +118,7 @@ namespace Core.Lifecycle
/// <summary> /// <summary>
/// Called once per component after bootstrap completes. /// Called once per component after bootstrap completes.
/// GUARANTEE: Bootstrap resources are available, all managers are initialized. /// GUARANTEE: Bootstrap resources are available, all managers are initialized.
/// For boot-time components: Called during LifecycleManager.BroadcastManagedStart (priority ordered). /// For boot-time components: Called during LifecycleManager.BroadcastManagedStart (registration order).
/// For late-registered components: Called immediately upon registration (bootstrap already complete). /// For late-registered components: Called immediately upon registration (bootstrap already complete).
/// Use for initialization that depends on other systems. /// Use for initialization that depends on other systems.
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes. /// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
@@ -161,7 +130,6 @@ namespace Core.Lifecycle
/// <summary> /// <summary>
/// Called before the scene this component belongs to is unloaded. /// Called before the scene this component belongs to is unloaded.
/// Called in REVERSE priority order (higher values execute first).
/// Use for scene-specific cleanup. /// Use for scene-specific cleanup.
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes. /// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary> /// </summary>
@@ -172,7 +140,6 @@ namespace Core.Lifecycle
/// <summary> /// <summary>
/// Called after the scene this component belongs to has finished loading. /// Called after the scene this component belongs to has finished loading.
/// Called in priority order (lower values execute first).
/// Use for scene-specific initialization. /// Use for scene-specific initialization.
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes. /// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary> /// </summary>
@@ -312,7 +279,6 @@ namespace Core.Lifecycle
/// <summary> /// <summary>
/// Called during OnDestroy before component is destroyed. /// Called during OnDestroy before component is destroyed.
/// Called in REVERSE priority order (higher values execute first).
/// NOTE: Most cleanup is automatic (managed events, auto-registrations). /// NOTE: Most cleanup is automatic (managed events, auto-registrations).
/// Only override if you need custom cleanup logic. /// Only override if you need custom cleanup logic.
/// Internal visibility allows LifecycleManager to call directly. Override in derived classes. /// Internal visibility allows LifecycleManager to call directly. Override in derived classes.

View File

@@ -24,9 +24,6 @@ namespace AppleHills.Core
#endregion Singleton Setup #endregion Singleton Setup
// Very early initialization - QuickAccess should be available immediately
public override int ManagedAwakePriority => 5;
#region Manager Instances #region Manager Instances
// Core Managers // Core Managers

View File

@@ -43,8 +43,6 @@ namespace Core.SaveLoad
public event Action<string> OnLoadCompleted; public event Action<string> OnLoadCompleted;
public event Action OnParticipantStatesRestored; public event Action OnParticipantStatesRestored;
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 20; // After GameManager and SceneManagerService
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
@@ -95,10 +93,8 @@ namespace Core.SaveLoad
// ...existing code... // ...existing code...
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy(); // Important: call base to unregister from LifecycleManager
if (_instance == this) if (_instance == this)
{ {
_instance = null; _instance = null;

View File

@@ -42,10 +42,8 @@ namespace Core
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
if (_director != null) if (_director != null)
{ {
_director.stopped -= OnDirectorStopped; _director.stopped -= OnDirectorStopped;

View File

@@ -44,8 +44,6 @@ namespace Core
private LogVerbosity _logVerbosity = LogVerbosity.Debug; private LogVerbosity _logVerbosity = LogVerbosity.Debug;
private const string BootstrapSceneName = "BootstrapScene"; private const string BootstrapSceneName = "BootstrapScene";
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 15; // Core infrastructure, after GameManager
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
@@ -369,7 +367,7 @@ namespace Core
await LoadSceneAsync(newSceneName, progress); await LoadSceneAsync(newSceneName, progress);
CurrentGameplayScene = newSceneName; CurrentGameplayScene = newSceneName;
// PHASE 10: Broadcast scene ready - processes batched components in priority order, then calls OnSceneReady // PHASE 10: Broadcast scene ready - processes batched components, then calls OnSceneReady
Logging.Debug($"Broadcasting OnSceneReady for: {newSceneName}"); Logging.Debug($"Broadcasting OnSceneReady for: {newSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(newSceneName); LifecycleManager.Instance?.BroadcastSceneReady(newSceneName);

View File

@@ -18,9 +18,6 @@ namespace Core
public GameObject orientationPromptPrefab; public GameObject orientationPromptPrefab;
private LogVerbosity _logVerbosity = LogVerbosity.Warning; private LogVerbosity _logVerbosity = LogVerbosity.Warning;
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 70; // Platform-specific utility
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
// Set instance immediately (early initialization) // Set instance immediately (early initialization)
@@ -103,15 +100,13 @@ namespace Core
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
// Unsubscribe from events to prevent memory leaks // Unsubscribe from events to prevent memory leaks
if (SceneManagerService.Instance != null) if (SceneManagerService.Instance != null)
{ {
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted; SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
} }
base.OnDestroy(); // Important: call base
} }
/// <summary> /// <summary>

View File

@@ -43,8 +43,6 @@ namespace Data.CardSystem
public event Action<int> OnBoosterCountChanged; public event Action<int> OnBoosterCountChanged;
public event Action<CardData> OnPendingCardAdded; public event Action<CardData> OnPendingCardAdded;
public event Action<CardData> OnCardPlacedInAlbum; public event Action<CardData> OnCardPlacedInAlbum;
public override int ManagedAwakePriority => 60; // Data systems
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {

View File

@@ -32,9 +32,6 @@ namespace Dialogue
public bool IsCompleted { get; private set; } public bool IsCompleted { get; private set; }
public string CurrentSpeakerName => dialogueGraph?.speakerName; public string CurrentSpeakerName => dialogueGraph?.speakerName;
public override int ManagedAwakePriority => 150; // Dialogue systems
internal override void OnManagedStart() internal override void OnManagedStart()
{ {
// Get required components // Get required components
@@ -184,10 +181,8 @@ namespace Dialogue
return null; return null;
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Unregister from events // Unregister from events
if (PuzzleManager.Instance != null) if (PuzzleManager.Instance != null)
PuzzleManager.Instance.OnStepCompleted -= OnAnyPuzzleStepCompleted; PuzzleManager.Instance.OnStepCompleted -= OnAnyPuzzleStepCompleted;

View File

@@ -49,8 +49,6 @@ namespace Input
private ITouchInputConsumer defaultConsumer; private ITouchInputConsumer defaultConsumer;
private bool isHoldActive; private bool isHoldActive;
private LogVerbosity _logVerbosity = LogVerbosity.Warning; private LogVerbosity _logVerbosity = LogVerbosity.Warning;
public override int ManagedAwakePriority => 25; // Input infrastructure
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
@@ -106,7 +104,7 @@ namespace Input
SwitchInputOnSceneLoaded(sceneName); SwitchInputOnSceneLoaded(sceneName);
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
// Unsubscribe from SceneManagerService events // Unsubscribe from SceneManagerService events
if (SceneManagerService.Instance != null) if (SceneManagerService.Instance != null)
@@ -114,7 +112,6 @@ namespace Input
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted; SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
} }
base.OnDestroy();
// Input action cleanup happens automatically // Input action cleanup happens automatically
} }

View File

@@ -70,7 +70,6 @@ namespace Input
public override bool AutoRegisterForSave => true; public override bool AutoRegisterForSave => true;
// Scene-specific SaveId - each level has its own player state // Scene-specific SaveId - each level has its own player state
public override string SaveId => $"{gameObject.scene.name}/PlayerController"; public override string SaveId => $"{gameObject.scene.name}/PlayerController";
public override int ManagedAwakePriority => 100; // Player controller
internal override void OnManagedStart() internal override void OnManagedStart()
{ {

View File

@@ -41,9 +41,6 @@ namespace Interactions
// Action component system // Action component system
private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>(); private List<InteractionActionBase> _registeredActions = new List<InteractionActionBase>();
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 100; // Gameplay base classes
/// <summary> /// <summary>
/// Register an action component with this interactable /// Register an action component with this interactable

View File

@@ -287,10 +287,8 @@ namespace Interactions
ItemManager.Instance?.RegisterItemSlot(this); ItemManager.Instance?.RegisterItemSlot(this);
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Unregister from slot manager // Unregister from slot manager
ItemManager.Instance?.UnregisterItemSlot(this); ItemManager.Instance?.UnregisterItemSlot(this);
} }

View File

@@ -50,10 +50,8 @@ namespace Interactions
ItemManager.Instance?.RegisterPickup(this); ItemManager.Instance?.RegisterPickup(this);
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Unregister from ItemManager // Unregister from ItemManager
ItemManager.Instance?.UnregisterPickup(this); ItemManager.Instance?.UnregisterPickup(this);
} }

View File

@@ -80,10 +80,8 @@ namespace Levels
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
if (PuzzleManager.Instance != null) if (PuzzleManager.Instance != null)
{ {
PuzzleManager.Instance.OnAllPuzzlesComplete -= HandleAllPuzzlesComplete; PuzzleManager.Instance.OnAllPuzzlesComplete -= HandleAllPuzzlesComplete;

View File

@@ -13,6 +13,7 @@ using UI.Core;
using UnityEngine; using UnityEngine;
using UnityEngine.Events; using UnityEngine.Events;
using UnityEngine.Playables; using UnityEngine.Playables;
using Svg;
namespace Minigames.DivingForPictures namespace Minigames.DivingForPictures
{ {
@@ -103,7 +104,6 @@ namespace Minigames.DivingForPictures
public static DivingGameManager Instance => _instance; public static DivingGameManager Instance => _instance;
public override int ManagedAwakePriority => 190;
public override bool AutoRegisterPausable => true; // Automatic GameManager registration public override bool AutoRegisterPausable => true; // Automatic GameManager registration
internal override void OnManagedAwake() internal override void OnManagedAwake()
@@ -161,10 +161,8 @@ namespace Minigames.DivingForPictures
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy(); // Handles auto-unregister from GameManager
// Unsubscribe from events when the manager is destroyed // Unsubscribe from events when the manager is destroyed
PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken; PlayerCollisionBehavior.OnDamageTaken -= OnPlayerDamageTaken;
OnMonsterSpawned -= DoMonsterSpawned; OnMonsterSpawned -= DoMonsterSpawned;

View File

@@ -106,8 +106,6 @@ public class FollowerController : ManagedBehaviour
private bool _hasRestoredHeldItem; // Track if held item restoration completed private bool _hasRestoredHeldItem; // Track if held item restoration completed
private string _expectedHeldItemSaveId; // Expected saveId during restoration private string _expectedHeldItemSaveId; // Expected saveId during restoration
public override int ManagedAwakePriority => 110; // Follower after player
internal override void OnManagedStart() internal override void OnManagedStart()
{ {
_aiPath = GetComponent<AIPath>(); _aiPath = GetComponent<AIPath>();

View File

@@ -83,10 +83,8 @@ namespace PuzzleS
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
if (PuzzleManager.Instance != null && stepData != null) if (PuzzleManager.Instance != null && stepData != null)
{ {
PuzzleManager.Instance.UnregisterStepBehaviour(this); PuzzleManager.Instance.UnregisterStepBehaviour(this);

View File

@@ -93,8 +93,6 @@ namespace PuzzleS
// Track pending unlocks for steps that were unlocked before their behavior registered // Track pending unlocks for steps that were unlocked before their behavior registered
private HashSet<string> _pendingUnlocks = new HashSet<string>(); private HashSet<string> _pendingUnlocks = new HashSet<string>();
public override int ManagedAwakePriority => 80; // Puzzle systems
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
@@ -138,10 +136,8 @@ namespace PuzzleS
LoadPuzzlesForScene(sceneName); LoadPuzzlesForScene(sceneName);
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Unsubscribe from SceneManagerService events // Unsubscribe from SceneManagerService events
if (SceneManagerService.Instance != null) if (SceneManagerService.Instance != null)
{ {

View File

@@ -40,8 +40,6 @@ public class AudioManager : ManagedBehaviour, IPausable
/// </summary> /// </summary>
public static AudioManager Instance => _instance; public static AudioManager Instance => _instance;
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 30; // Audio infrastructure
public override bool AutoRegisterPausable => true; // Auto-register as IPausable public override bool AutoRegisterPausable => true; // Auto-register as IPausable
internal override void OnManagedAwake() internal override void OnManagedAwake()

View File

@@ -1,6 +1,7 @@
using System; using System;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using SkiaSharp.Unity;
using Input; using Input;
using AppleHills.Core; using AppleHills.Core;
using UI.Core; using UI.Core;
@@ -10,6 +11,8 @@ using UI;
public class AppSwitcher : UIPage public class AppSwitcher : UIPage
{ {
public GameObject rainbowIn;
public GameObject rainbowOut;
public GameObject gameLayoutContainer; public GameObject gameLayoutContainer;
public GameObject exitButton; public GameObject exitButton;
public RectMask2D rectMask; public RectMask2D rectMask;
@@ -17,6 +20,8 @@ public class AppSwitcher : UIPage
[Header("Slide Animation Settings")] [Header("Slide Animation Settings")]
public float slideDuration = 0.5f; public float slideDuration = 0.5f;
private SkottiePlayerV2 rainbowInPlayer;
private SkottiePlayerV2 rainbowOutPlayer;
private TweenBase slideInTween; private TweenBase slideInTween;
private TweenBase slideOutTween; private TweenBase slideOutTween;
@@ -25,13 +30,16 @@ public class AppSwitcher : UIPage
base.OnManagedAwake(); base.OnManagedAwake();
PageName = "AppSwitcher"; PageName = "AppSwitcher";
rainbowInPlayer = rainbowIn.GetComponent<SkottiePlayerV2>();
rainbowOutPlayer = rainbowOut.GetComponent<SkottiePlayerV2>();
if (rectMask == null) if (rectMask == null)
{ {
rectMask = GetComponent<RectMask2D>(); rectMask = GetComponent<RectMask2D>();
} }
// Initially hide // Initially hide both
rainbowIn.SetActive(true);
exitButton.SetActive(false); exitButton.SetActive(false);
} }
@@ -41,19 +49,27 @@ public class AppSwitcher : UIPage
gameLayoutContainer.SetActive(true); gameLayoutContainer.SetActive(true);
// Hide rainbow out, show rainbow in
rainbowOut.SetActive(false);
// Play animation on rainbow in without resetting state
rainbowInPlayer.PlayAnimation(true);
// Slide in animation - tween padding.left from 1700 to 0 // Slide in animation - tween padding.left from 1700 to 0
slideInTween = TweenPaddingLeft(1700f, 0f, Tween.EaseOut, () => slideInTween = TweenPaddingLeft(1700f, 0f, Tween.EaseOut, () =>
{ {
onComplete?.Invoke(); onComplete?.Invoke();
rainbowOut.SetActive(true);
exitButton.SetActive(true); exitButton.SetActive(true);
}); });
} }
protected override void DoTransitionOut(Action onComplete) protected override void DoTransitionOut(Action onComplete)
{ {
rainbowIn.SetActive(false);
// Play animation on rainbow out with resetting state
rainbowOutPlayer.PlayAnimation(true);
// Hide the exit button // Hide the exit button
exitButton.SetActive(false); exitButton.SetActive(false);
@@ -61,6 +77,7 @@ public class AppSwitcher : UIPage
// Slide out animation - tween padding.left from 0 to 1700 // Slide out animation - tween padding.left from 0 to 1700
slideOutTween = TweenPaddingLeft(0f, 1700f, Tween.EaseIn, () => { slideOutTween = TweenPaddingLeft(0f, 1700f, Tween.EaseIn, () => {
gameLayoutContainer.SetActive(false); gameLayoutContainer.SetActive(false);
rainbowIn.SetActive(true);
onComplete?.Invoke(); onComplete?.Invoke();
InputManager.Instance.SetInputMode(InputMode.GameAndUI); InputManager.Instance.SetInputMode(InputMode.GameAndUI);
}); });
@@ -93,10 +110,8 @@ public class AppSwitcher : UIPage
); );
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Clean up tweens // Clean up tweens
slideInTween?.Stop(); slideInTween?.Stop();
slideOutTween?.Stop(); slideOutTween?.Stop();

View File

@@ -149,7 +149,7 @@ namespace UI.CardSystem
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
// Unsubscribe from CardSystemManager // Unsubscribe from CardSystemManager
if (CardSystemManager.Instance != null) if (CardSystemManager.Instance != null)
@@ -181,9 +181,6 @@ namespace UI.CardSystem
// Clean up active cards // Clean up active cards
CleanupActiveCards(); CleanupActiveCards();
// Call base implementation
base.OnDestroy();
} }
private void OnExitButtonClicked() private void OnExitButtonClicked()

View File

@@ -70,16 +70,13 @@ namespace UI.CardSystem
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
// Unsubscribe from CardSystemManager events to prevent memory leaks // Unsubscribe from CardSystemManager events to prevent memory leaks
if (CardSystemManager.Instance != null) if (CardSystemManager.Instance != null)
{ {
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged; CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
} }
// Call base implementation
base.OnDestroy();
} }
/// <summary> /// <summary>

View File

@@ -76,10 +76,8 @@ namespace UI.CardSystem
gameObject.SetActive(false); gameObject.SetActive(false);
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Unsubscribe from dismiss button // Unsubscribe from dismiss button
if (_dismissButton != null) if (_dismissButton != null)
{ {

View File

@@ -307,7 +307,7 @@ namespace UI.CardSystem.DragDrop
} }
#endregion #endregion
protected override void OnDestroy() protected override void OnDestroy()
{ {
base.OnDestroy(); base.OnDestroy();

View File

@@ -106,7 +106,7 @@ namespace UI.CardSystem.DragDrop
base.OnDragEndedVisual(); base.OnDragEndedVisual();
// Card-specific visual effects when dragging ends // Card-specific visual effects when dragging ends
} }
protected override void OnDestroy() protected override void OnDestroy()
{ {
base.OnDestroy(); base.OnDestroy();

View File

@@ -15,9 +15,6 @@ namespace UI.Core
[Header("Page Settings")] [Header("Page Settings")]
public string PageName; public string PageName;
// UI pages load after UI infrastructure (UIPageController is priority 50)
public override int ManagedAwakePriority => 200;
// Events using System.Action instead of UnityEvents // Events using System.Action instead of UnityEvents
public event Action OnTransitionInStarted; public event Action OnTransitionInStarted;
public event Action OnTransitionInCompleted; public event Action OnTransitionInCompleted;

View File

@@ -37,8 +37,6 @@ namespace UI.Core
private PlayerInput _playerInput; private PlayerInput _playerInput;
private InputAction _cancelAction; private InputAction _cancelAction;
public override int ManagedAwakePriority => 50; // UI infrastructure
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
// Set instance immediately (early initialization) // Set instance immediately (early initialization)
@@ -50,10 +48,8 @@ namespace UI.Core
Logging.Debug("[UIPageController] Initialized"); Logging.Debug("[UIPageController] Initialized");
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Clean up cached instances // Clean up cached instances
foreach (var cachedPage in _prefabInstanceCache.Values) foreach (var cachedPage in _prefabInstanceCache.Values)
{ {

View File

@@ -52,9 +52,6 @@ namespace UI
/// Singleton instance of the LoadingScreenController. No longer creates an instance if one doesn't exist. /// Singleton instance of the LoadingScreenController. No longer creates an instance if one doesn't exist.
/// </summary> /// </summary>
public static LoadingScreenController Instance => _instance; public static LoadingScreenController Instance => _instance;
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 45; // UI infrastructure, before UIPageController
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {

View File

@@ -27,9 +27,6 @@ namespace UI
[SerializeField] private UnityEngine.UI.Button devOptionsButton; [SerializeField] private UnityEngine.UI.Button devOptionsButton;
[SerializeField] private GameObject mainOptionsContainer; [SerializeField] private GameObject mainOptionsContainer;
[SerializeField] private GameObject devOptionsContainer; [SerializeField] private GameObject devOptionsContainer;
// After UIPageController (50)
public override int ManagedAwakePriority => 55;
internal override void OnManagedAwake() internal override void OnManagedAwake()
{ {
@@ -76,10 +73,8 @@ namespace UI
// This only fires once for DontDestroyOnLoad objects, so we handle scene loads in OnManagedAwake // This only fires once for DontDestroyOnLoad objects, so we handle scene loads in OnManagedAwake
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Unsubscribe when destroyed // Unsubscribe when destroyed
if (SceneManagerService.Instance != null) if (SceneManagerService.Instance != null)
{ {

View File

@@ -172,10 +172,8 @@ namespace UI
} }
} }
protected override void OnDestroy() internal override void OnManagedDestroy()
{ {
base.OnDestroy();
// Unsubscribe from events // Unsubscribe from events
if (_uiPageController != null) if (_uiPageController != null)
{ {

View File

@@ -30,7 +30,6 @@ namespace UI.Tutorial
private bool _canAcceptInput; private bool _canAcceptInput;
private Coroutine _waitLoopCoroutine; private Coroutine _waitLoopCoroutine;
public override int ManagedAwakePriority => 200; // Tutorial runs late, after other systems
internal override void OnManagedStart() internal override void OnManagedStart()
{ {

View File

@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"com.ammariqais.skiaforunity": "https://github.com/ammariqais/SkiaForUnity.git?path=SkiaUnity/Assets/SkiaSharp",
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity", "com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity",
"com.moolt.packages.net": "git+https://github.com/Moolt/UnityAudioSourceEvents?path=Packages/AudioSourceEvents", "com.moolt.packages.net": "git+https://github.com/Moolt/UnityAudioSourceEvents?path=Packages/AudioSourceEvents",
"com.unity.2d.sprite": "1.0.0", "com.unity.2d.sprite": "1.0.0",

View File

@@ -1,5 +1,14 @@
{ {
"dependencies": { "dependencies": {
"com.ammariqais.skiaforunity": {
"version": "https://github.com/ammariqais/SkiaForUnity.git?path=SkiaUnity/Assets/SkiaSharp",
"depth": 0,
"source": "git",
"dependencies": {
"com.unity.nuget.newtonsoft-json": "3.2.0"
},
"hash": "11e82b71012bf8b4f68172080d6e4969d42120ef"
},
"com.coplaydev.unity-mcp": { "com.coplaydev.unity-mcp": {
"version": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity", "version": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity",
"depth": 0, "depth": 0,

View File

@@ -12,8 +12,8 @@ PlayerSettings:
targetDevice: 2 targetDevice: 2
useOnDemandResources: 0 useOnDemandResources: 0
accelerometerFrequency: 60 accelerometerFrequency: 60
companyName: DR companyName: DefaultCompany
productName: "DR \xC6blerup" productName: AppleHills
defaultCursor: {fileID: 0} defaultCursor: {fileID: 0}
cursorHotspot: {x: 0, y: 0} cursorHotspot: {x: 0, y: 0}
m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1}
@@ -142,7 +142,7 @@ PlayerSettings:
loadStoreDebugModeEnabled: 0 loadStoreDebugModeEnabled: 0
visionOSBundleVersion: 1.0 visionOSBundleVersion: 1.0
tvOSBundleVersion: 1.0 tvOSBundleVersion: 1.0
bundleVersion: 1.1 bundleVersion: 1.0.2
preloadedAssets: [] preloadedAssets: []
metroInputSource: 0 metroInputSource: 0
wsaTransparentSwapchain: 0 wsaTransparentSwapchain: 0
@@ -168,7 +168,7 @@ PlayerSettings:
Android: com.DefaultCompany.com.unity.template.mobile2D Android: com.DefaultCompany.com.unity.template.mobile2D
Lumin: com.DefaultCompany.com.unity.template.mobile2D Lumin: com.DefaultCompany.com.unity.template.mobile2D
Standalone: com.DefaultCompany.com.unity.template.mobile2D Standalone: com.DefaultCompany.com.unity.template.mobile2D
iPhone: dk.dr.ramasjang.aeblerup iPhone: com.DefaultCompany.com.unity.template.mobile2D
tvOS: com.DefaultCompany.com.unity.template.mobile2D tvOS: com.DefaultCompany.com.unity.template.mobile2D
buildNumber: buildNumber:
Standalone: 0 Standalone: 0
@@ -403,31 +403,25 @@ PlayerSettings:
m_SubKind: m_SubKind:
- m_BuildTarget: iPhone - m_BuildTarget: iPhone
m_Icons: m_Icons:
- m_Textures:
- {fileID: 2800000, guid: a8ae11f948f0cfa4791e954c315376ee, type: 3}
m_Width: 1024
m_Height: 1024
m_Kind: 4
m_SubKind: App Store
- m_Textures: [] - m_Textures: []
m_Width: 60 m_Width: 120
m_Height: 60 m_Height: 120
m_Kind: 2 m_Kind: 3
m_SubKind: iPhone m_SubKind: iPhone
- m_Textures: [] - m_Textures: []
m_Width: 40 m_Width: 80
m_Height: 40 m_Height: 80
m_Kind: 2 m_Kind: 3
m_SubKind: iPhone m_SubKind: iPhone
- m_Textures: [] - m_Textures: []
m_Width: 40 m_Width: 80
m_Height: 40 m_Height: 80
m_Kind: 2 m_Kind: 3
m_SubKind: iPad m_SubKind: iPad
- m_Textures: [] - m_Textures: []
m_Width: 20 m_Width: 40
m_Height: 20 m_Height: 40
m_Kind: 2 m_Kind: 3
m_SubKind: iPad m_SubKind: iPad
- m_Textures: [] - m_Textures: []
m_Width: 87 m_Width: 87
@@ -455,25 +449,30 @@ PlayerSettings:
m_Kind: 1 m_Kind: 1
m_SubKind: iPad m_SubKind: iPad
- m_Textures: [] - m_Textures: []
m_Width: 120 m_Width: 60
m_Height: 120 m_Height: 60
m_Kind: 3 m_Kind: 2
m_SubKind: iPhone m_SubKind: iPhone
- m_Textures: []
m_Width: 80
m_Height: 80
m_Kind: 3
m_SubKind: iPhone
- m_Textures: []
m_Width: 80
m_Height: 80
m_Kind: 3
m_SubKind: iPad
- m_Textures: [] - m_Textures: []
m_Width: 40 m_Width: 40
m_Height: 40 m_Height: 40
m_Kind: 3 m_Kind: 2
m_SubKind: iPhone
- m_Textures: []
m_Width: 40
m_Height: 40
m_Kind: 2
m_SubKind: iPad m_SubKind: iPad
- m_Textures: []
m_Width: 20
m_Height: 20
m_Kind: 2
m_SubKind: iPad
- m_Textures: []
m_Width: 1024
m_Height: 1024
m_Kind: 4
m_SubKind: App Store
- m_Textures: - m_Textures:
- {fileID: 2800000, guid: a8ae11f948f0cfa4791e954c315376ee, type: 3} - {fileID: 2800000, guid: a8ae11f948f0cfa4791e954c315376ee, type: 3}
m_Width: 180 m_Width: 180
@@ -508,9 +507,6 @@ PlayerSettings:
- m_BuildTarget: Android - m_BuildTarget: Android
m_StaticBatching: 1 m_StaticBatching: 1
m_DynamicBatching: 0 m_DynamicBatching: 0
- m_BuildTarget: iPhone
m_StaticBatching: 1
m_DynamicBatching: 0
m_BuildTargetShaderSettings: [] m_BuildTargetShaderSettings: []
m_BuildTargetGraphicsJobs: m_BuildTargetGraphicsJobs:
- m_BuildTarget: MacStandaloneSupport - m_BuildTarget: MacStandaloneSupport
@@ -840,7 +836,7 @@ PlayerSettings:
webEnableSubmoduleStrippingCompatibility: 0 webEnableSubmoduleStrippingCompatibility: 0
scriptingDefineSymbols: scriptingDefineSymbols:
Android: ENABLE_LOG Android: ENABLE_LOG
iPhone: iPhone: __UNIFIED__;__IOS__
additionalCompilerArguments: {} additionalCompilerArguments: {}
platformArchitecture: {} platformArchitecture: {}
scriptingBackend: scriptingBackend:

View File

@@ -0,0 +1,93 @@
# ManagedBehaviour System - Architecture Overview
**Version:** 2.0 <br>
**Updated:** 11.11.2025
---
## What is the ManagedBehaviour System?
Lifecycle orchestration framework that provides guaranteed execution order and deterministic lifecycle management for Unity components.
### Problems Solved
We've had quite a few things shoe-stringed together in various ways and dependant on references to each other.
Due to undefined initialization order - null references during access to yet uninitialized resources was becoming a problem.
### What You Get
- **Guaranteed Initialization Order** - Managers ready before components that depend on them
- **Deterministic Lifecycle Hooks** - Predictable callbacks at key moments
- **Automatic Registration** - No boilerplate for wiring up systems
- **Scene Lifecycle Events** - Built-in hooks for scene load/unload
- **Save/Load Coordination** - Centralized collection of save data
- **Bootstrap Integration** - Components know when bootstrap completes
---
## Architecture Principles
### 1. Centralized Orchestration
Single `LifecycleManager` singleton coordinates all lifecycle events. Components auto-register and receive callbacks at appropriate times.
### 2. Sealed Framework Methods
`Awake()` and `OnDestroy()` are sealed. Use `OnManagedAwake()` and `OnManagedDestroy()` instead. Prevents forgetting to call `base.Awake()`.
### 3. Two-Phase Initialization
- **Early (`OnManagedAwake()`)**: During Unity's Awake, before bootstrap. Use for singleton setup.
- **Late (`OnManagedStart()`)**: After bootstrap completes. All managers guaranteed ready.
### 4. Registration Order Execution
Execution follows Unity's natural Awake order. No priority numbers to manage.
### 5. Automatic Cleanup
Framework handles unregistration automatically. Override `OnManagedDestroy()` only if you need custom cleanup.
---
## Lifecycle Flow Diagrams
### Boot Sequence
![Boot Sequence](../media/boot_sequence.png)
### Scene Transition Flow
![Scene Transition Flow](../media/Scene_transition_flow.png)
### Component Lifecycle (Individual Component)
![Component Lifecycle](../media/component_lifecycle.png)
---
## Class Diagram
![Class Diagram](../media/class_diagram.png)
---
## Key Guarantees
### Guaranteed
1. **Bootstrap Completion** - `OnManagedStart()` always fires after bootstrap completes
2. **Manager Availability** - All manager singletons exist when `OnManagedStart()` is called
3. **Scene Lifecycle** - `OnSceneReady()` fires after scene load, `OnSceneUnloading()` before unload
4. **Automatic Registration** - Can't forget to register (Awake is sealed)
5. **Automatic Cleanup** - Can't forget to unregister (OnDestroy is sealed)
6. **Save/Load Coordination** - All save participants called in one pass
### Not Guaranteed
1. **Initialization Order Between Components** - `OnManagedAwake()` follows Unity's unpredictable Awake order
2. **Thread Safety** - All methods must run on main thread
3. **Performance** - Broadcasting to 1000+ components may have overhead
4. **SaveId Uniqueness** - Developer responsible for unique SaveIds

View File

@@ -0,0 +1,377 @@
# ManagedBehaviour System - Technical Reference
**Version:** 2.0 <br>
**Updated:** 11.11.2025
---
## Overview
The ManagedBehaviour system provides a deterministic, ordered lifecycle management framework for Unity MonoBehaviours. This document provides complete technical documentation of all classes, methods, and APIs.
---
## Core Classes
### ManagedBehaviour (Abstract Base Class)
**Namespace:** `Core.Lifecycle`
**Inherits:** `MonoBehaviour`
**Location:** `Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs` → [View Source](../../Assets/Scripts/Core/Lifecycle/ManagedBehaviour.cs)
Abstract base class that all managed components must inherit from. Provides automatic registration with LifecycleManager and ordered lifecycle callbacks.
#### Lifecycle Hook Methods
Override these `internal virtual` methods to customize component behavior at different lifecycle stages. Called automatically by `LifecycleManager`.
<details>
<summary>Click to see more details</summary>
##### `OnManagedAwake()`
```csharp
internal virtual void OnManagedAwake()
```
**Called:** During registration (within Unity's Awake phase)
**Execution Order:** Natural Unity script execution order (not guaranteed between components)
**Use For:**
- Setting singleton instances (`_instance = this`)
- Early GetComponent calls
- One-time initialization that doesn't depend on other systems
**Timing Guarantees:**
- ✅ GameObject and component exist
- ✅ Scene is loaded
- ❌ Other components may not be initialized yet
- ❌ Bootstrap may not be complete
##### `OnManagedStart()`
```csharp
internal virtual void OnManagedStart()
```
**Called:** After bootstrap completes (for boot components) or immediately after registration (for late-registered components)
**Execution Order:** Registration order
**Use For:**
- Initialization that depends on other managers
- Accessing singleton instances safely
- Setting up cross-system dependencies
**Timing Guarantees:**
- ✅ All managers are initialized
- ✅ Bootstrap resources are available
- ✅ Safe to access `GameManager.Instance`, `AudioManager.Instance`, etc.
##### `OnSceneUnloading()`
```csharp
internal virtual void OnSceneUnloading()
```
**Called:** Before scene unload
**Execution Order:** Registration order
**Use For:**
- Scene-specific cleanup
- Saving temporary scene state
**Timing Guarantees:**
- ✅ Scene is still loaded
- ✅ Other components in scene still exist
##### `OnSceneReady()`
```csharp
internal virtual void OnSceneReady()
```
**Called:** After scene load completes
**Execution Order:** Registration order
**Use For:**
- Scene-specific initialization
- Finding scene objects
- Setting up scene-specific state
**Timing Guarantees:**
- ✅ All scene GameObjects are loaded
- ✅ Batched components have received `OnManagedStart()`
##### `OnSceneSaveRequested()`
```csharp
internal virtual string OnSceneSaveRequested()
```
**Called:** Before scene unload during scene transitions
**Returns:** Serialized scene-specific data (JSON string), or `null` if nothing to save
**Use For:**
- Level progress
- Object positions
- Puzzle states
- Temporary scene data
**⚠️ Important:** Must return synchronously.
##### `OnSceneRestoreRequested(string serializedData)`
```csharp
internal virtual void OnSceneRestoreRequested(string serializedData)
```
**Called:** After scene load, during `OnSceneReady` phase
**Parameters:**
- `serializedData` - Previously saved data from `OnSceneSaveRequested()`
**Use For:**
- Restoring level progress
- Setting object positions
- Restoring puzzle states
**⚠️ Important:** Must execute synchronously. Do not use coroutines or async/await.
##### `OnSceneRestoreCompleted()`
```csharp
internal virtual void OnSceneRestoreCompleted()
```
**Called:** After all `OnSceneRestoreRequested()` calls complete
**Timing:** Always called after scene load, whether save data exists or not
**Use For:**
- Post-restore initialization
- First-time initialization (when no save data exists)
- Triggering events after state is restored
**Common Pattern:**
```csharp
internal override void OnSceneRestoreCompleted()
{
if (!_hasPlayed) // Check if this is first time
{
PlayIntroAudio();
_hasPlayed = true;
}
}
```
##### `OnGlobalSaveRequested()`
```csharp
internal virtual string OnGlobalSaveRequested()
```
**Called:** Once before save file is written to disk
**Returns:** Serialized global persistent data (JSON string), or `null`
**Use For:**
- Player inventory
- Unlocked features
- Card collections
- Persistent progression
**Timing:** Called once per game save (not per scene transition)
##### `OnGlobalRestoreRequested(string serializedData)`
```csharp
internal virtual void OnGlobalRestoreRequested(string serializedData)
```
**Called:** Once on game boot after save file is read
**Parameters:**
- `serializedData` - Previously saved data from `OnGlobalSaveRequested()`
**Use For:**
- Restoring player inventory
- Restoring unlocked features
- Restoring persistent progression
**Timing:** Called once on boot, not during scene transitions
##### `OnGlobalLoadCompleted()`
```csharp
internal virtual void OnGlobalLoadCompleted()
```
**Called:** Once on game boot after all global restore operations complete
**Use For:**
- Triggering UI updates after load
- Broadcasting load events
- Post-load initialization
##### `OnGlobalSaveStarted()`
```csharp
internal virtual void OnGlobalSaveStarted()
```
**Called:** Once before save file is written
**Use For:**
- Final validation before save
- Cleanup operations before save
##### `OnManagedDestroy()`
```csharp
internal virtual void OnManagedDestroy()
```
**Called:** During `OnDestroy`, before unregistration
**Execution Order:** Registration order
**Use For:**
- Unsubscribing from events
- Releasing resources
- Custom cleanup logic
**Note:** Most cleanup is automatic (auto-registrations are handled by framework).
</details>
#### Configuration Properties
Virtual properties that control automatic behaviors like pause registration and save system participation.
<details>
<summary>Click to see more details</summary>
##### `AutoRegisterPausable`
```csharp
public virtual bool AutoRegisterPausable => false;
```
**Type:** `bool`
**Default:** `false`
**Description:** If true and component implements `IPausable`, automatically registers with `GameManager.Instance` during initialization. Automatic unregistration occurs on destruction.
##### `AutoRegisterForSave`
```csharp
public virtual bool AutoRegisterForSave => false;
```
**Type:** `bool`
**Default:** `false`
**Description:** If true, component participates in the save/load system. Should override `OnSceneSaveRequested()` and `OnSceneRestoreRequested()` or global equivalents.
##### `SaveId`
```csharp
public virtual string SaveId { get; }
```
**Type:** `string`
**Default:** `"{SceneName}/{GameObjectName}/{ComponentType}"`
**Description:** Unique identifier for this component in the save system. Cached on first access for performance. Override for singletons (e.g., `"PlayerController"`) or custom IDs.
**⚠️ Warning:** GameObject name changes at runtime will NOT update the cached SaveId.
</details>
### Private Lifecycle Methods
<details>
<summary>Click to see more details</summary>
##### `Awake()`
```csharp
private void Awake()
```
**Visibility:** `private` (sealed, cannot be overridden)
**Called By:** Unity
**Description:** Automatically registers component with `LifecycleManager`. Calls `OnManagedAwake()` during registration.
**⚠️ Important:** This method is sealed. Use `OnManagedAwake()` for early initialization.
##### `OnDestroy()`
```csharp
private void OnDestroy()
```
**Visibility:** `private` (sealed, cannot be overridden)
**Called By:** Unity
**Description:** Calls `OnManagedDestroy()`, unregisters from `LifecycleManager`, and handles auto-unregistrations.
**⚠️ Important:** This method is sealed. Use `OnManagedDestroy()` for custom cleanup.
</details>
---
## LifecycleManager (Singleton Orchestrator)
**Namespace:** `Core.Lifecycle`
**Inherits:** `MonoBehaviour`
**Location:** `Assets/Scripts/Core/Lifecycle/LifecycleManager.cs` → [View Source](../../Assets/Scripts/Core/Lifecycle/LifecycleManager.cs)
Central orchestrator that manages all `ManagedBehaviour` components and broadcasts lifecycle events.
### Static Properties
##### `Instance`
Singleton instance. Created automatically by `CustomBoot` before bootstrap begins.
### Public Methods
Core methods for registration, lifecycle broadcasting, and save/restore operations. Most are called automatically by the framework.
<details>
<summary>Click to see more details</summary>
##### `Register(ManagedBehaviour component)`
```csharp
public void Register(ManagedBehaviour component)
```
Registers a component with the lifecycle system. **Called automatically from `ManagedBehaviour.Awake()`.**
##### `Unregister(ManagedBehaviour component)`
```csharp
public void Unregister(ManagedBehaviour component)
```
Unregisters a component. **Called automatically from `ManagedBehaviour.OnDestroy()`.**
##### `OnBootCompletionTriggered()`
```csharp
public void OnBootCompletionTriggered()
```
Called by `CustomBoot` after bootstrap completes. Broadcasts `OnManagedStart()` to all registered components.
##### `BeginSceneLoad(string sceneName)`
```csharp
public void BeginSceneLoad(string sceneName)
```
Activates scene loading batching mode. Called by `SceneManagerService` when loading a scene.
##### `BroadcastSceneReady(string sceneName)`
```csharp
public void BroadcastSceneReady(string sceneName)
```
Processes batched components and broadcasts `OnSceneReady()` to all components in the scene. Called by `SceneManagerService` after scene load.
##### `BroadcastSceneUnloading(string sceneName)`
```csharp
public void BroadcastSceneUnloading(string sceneName)
```
Broadcasts `OnSceneUnloading()` to all components in the specified scene. Called by `SceneManagerService` before scene unload.
##### `BroadcastSceneSaveRequested()`
```csharp
public Dictionary<string, string> BroadcastSceneSaveRequested()
```
Broadcasts `OnSceneSaveRequested()` to all components with `AutoRegisterForSave == true`. Returns dictionary of SaveId → serialized data.
##### `BroadcastSceneRestoreRequested(Dictionary<string, string> saveData)`
```csharp
public void BroadcastSceneRestoreRequested(Dictionary<string, string> saveData)
```
Distributes save data to components by matching `SaveId`, then broadcasts `OnSceneRestoreCompleted()`.
##### `BroadcastGlobalSaveRequested()`
```csharp
public Dictionary<string, string> BroadcastGlobalSaveRequested()
```
Broadcasts `OnGlobalSaveRequested()` to all components with `AutoRegisterForSave == true`. Returns dictionary of SaveId → serialized data.
##### `BroadcastGlobalRestoreRequested(Dictionary<string, string> saveData)`
```csharp
public void BroadcastGlobalRestoreRequested(Dictionary<string, string> saveData)
```
Distributes save data to components by matching `SaveId`, then broadcasts `OnGlobalLoadCompleted()`.
##### `BroadcastGlobalSaveStarted()`
```csharp
public void BroadcastGlobalSaveStarted()
```
Broadcasts `OnGlobalSaveStarted()` to all components with `AutoRegisterForSave == true`.
## LifecyclePhase (Enum)
**Namespace:** `Core.Lifecycle`
**Location:** `Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs` → [View Source](../../Assets/Scripts/Core/Lifecycle/LifecycleEnums.cs)
Defines the different lifecycle phases for documentation and tooling purposes.
```csharp
public enum LifecyclePhase
{
ManagedAwake, // During registration (Awake)
ManagedStart, // After bootstrap or late registration
SceneUnloading, // Before scene unload
SceneReady, // After scene load
SaveRequested, // Before scene unload (save)
RestoreRequested, // After scene load (restore)
ManagedDestroy // During OnDestroy
}
```
---

View File

@@ -0,0 +1,522 @@
# ManagedBehaviour System - Quick Start & Use Cases
**TL;DR:** Inherit from `ManagedBehaviour` instead of `MonoBehaviour`. Override lifecycle hooks instead of Awake/OnDestroy. Get guaranteed initialization order and automatic registration.
---
## Table of Contents
1. [Lifecycle Methods Summary](#lifecycle-methods-summary)
2. [Quick Reference - Common Use Cases](#quick-reference---common-use-cases)
3. [Getting Started Examples](#getting-started-examples)
4. [Detailed Use Cases](#detailed-use-cases)
5. [Common Patterns](#common-patterns)
6. [Migration Checklist](#migration-checklist)
7. [Troubleshooting](#troubleshooting)
8. [Best Practices](#best-practices)
9. [FAQ](#faq)
---
## Summary
**ManagedBehaviour in 3 Sentences:**
Inherit from `ManagedBehaviour` to get automatic lifecycle management with guaranteed initialization order. Use `OnManagedStart()` to safely access manager singletons, and use built-in save/load hooks for persistence. The framework handles registration and cleanup automatically - you just override the hooks you need.
**When to Use:**
- ✅ Singleton managers
- ✅ Components that access managers
- ✅ Components that need save/load
- ✅ Components that need scene lifecycle events
**When to Skip:**
- ❌ Simple self-contained components
- ❌ Third-party code (can't change base class)
- ❌ Performance-critical code with no dependencies
**But can I still use regular MonoBehaviour?!** <br>
_Yes!_ Only use ManagedBehaviour when you need its features (manager access, save/load, etc.)
---
## Lifecycle Methods Summary
1. **OnManagedAwake()** - Called during Unity's Awake phase; use for **internal setup** and GetComponent calls.
2. **OnManagedStart()** - Called after bootstrap completes; **safe to access** manager singletons.
3. **OnSceneReady()** - Called after scene finishes loading; use for scene-specific initialization.
4. **OnSceneUnloading()** - Called before scene unloads; use for scene cleanup.
5. **OnSceneSaveRequested()** - Returns serialized scene-specific data before scene transitions.
6. **OnSceneRestoreRequested(data)** - Receives saved scene data after scene loads.
7. **OnSceneRestoreCompleted()** - Called after all scene restore operations complete.
8. **OnGlobalSaveRequested()** - Returns serialized persistent data when game saves.
9. **OnGlobalRestoreRequested(data)** - Receives persistent data on game boot.
10. **OnGlobalLoadCompleted()** - Called after all global restore operations complete on boot.
11. **OnGlobalSaveStarted()** - Called before save file is written; use for pre-save validation.
12. **OnManagedDestroy()** - Called during OnDestroy; use for cleanup and event unsubscription.
---
## Quick Reference - Common Use Cases
### Access Manager Singleton Safely
```csharp
internal override void OnManagedStart()
{
GameManager.Instance.DoSomething(); // Safe - managers guaranteed ready
}
```
### Create Singleton Manager
```csharp
private static MyManager _instance;
public static MyManager Instance => _instance;
internal override void OnManagedAwake()
{
_instance = this;
}
```
### Save Scene Progress
```csharp
public override bool AutoRegisterForSave => true;
internal override string OnSceneSaveRequested()
{
return JsonUtility.ToJson(myData);
}
internal override void OnSceneRestoreRequested(string data)
{
myData = JsonUtility.FromJson<MyData>(data);
}
```
### Auto-Register as Pausable
```csharp
public class MyComponent : ManagedBehaviour, IPausable
{
public override bool AutoRegisterPausable => true;
public void Pause() { /* pause logic */ }
public void Resume() { /* resume logic */ }
}
```
### Scene-Specific Initialization
```csharp
internal override void OnSceneReady()
{
FindObjectsInScene();
InitializeLevel();
}
```
### Cleanup on Destroy
```csharp
internal override void OnManagedDestroy()
{
EventBus.OnSomething -= HandleEvent;
}
```
---
## Getting Started Examples
### 1. Basic Component
```csharp
using Core.Lifecycle;
public class MyComponent : ManagedBehaviour
{
internal override void OnManagedAwake()
{
// Early initialization (singleton setup, GetComponent)
Debug.Log("MyComponent awakened");
}
internal override void OnManagedStart()
{
// Late initialization (safe to access managers)
Debug.Log("MyComponent started - managers are ready");
}
}
```
### 2. Singleton Manager
```csharp
using Core.Lifecycle;
public class MyManager : ManagedBehaviour
{
private static MyManager _instance;
public static MyManager Instance => _instance;
internal override void OnManagedAwake()
{
_instance = this; // Set singleton early
}
internal override void OnManagedStart()
{
// All other managers are ready here
AudioManager.Instance.PlaySound("ManagerReady");
}
}
```
### 3. Component with Save/Load
```csharp
using Core.Lifecycle;
public class PuzzleComponent : ManagedBehaviour
{
public override bool AutoRegisterForSave => true;
public override string SaveId => "MyPuzzle"; // Custom ID
private bool _isSolved;
internal override string OnSceneSaveRequested()
{
return JsonUtility.ToJson(new { isSolved = _isSolved });
}
internal override void OnSceneRestoreRequested(string data)
{
var saveData = JsonUtility.FromJson<SaveData>(data);
_isSolved = saveData.isSolved;
}
[System.Serializable]
private class SaveData
{
public bool isSolved;
}
}
```
### 4. Component with Cleanup
```csharp
using Core.Lifecycle;
public class EventSubscriber : ManagedBehaviour
{
internal override void OnManagedStart()
{
GameManager.Instance.OnGamePaused += HandlePause;
}
internal override void OnManagedDestroy()
{
// Automatic cleanup
if (GameManager.Instance != null)
GameManager.Instance.OnGamePaused -= HandlePause;
}
private void HandlePause() { }
}
```
---
## Detailed Use Cases
### Use Case 1: Accessing Singleton Managers Safely
**Problem:** Accessing `GameManager.Instance` in `Awake()` might fail if GameManager hasn't initialized yet.
**Solution:**
```csharp
public class Player : ManagedBehaviour
{
// ❌ DON'T: Risky in OnManagedAwake (managers may not be ready)
internal override void OnManagedAwake()
{
// var settings = GameManager.Instance.GetSettings(); // RISKY!
}
// ✅ DO: Safe in OnManagedStart (managers guaranteed ready)
internal override void OnManagedStart()
{
var settings = GameManager.Instance.GetSettings(); // SAFE!
ApplySettings(settings);
}
}
```
---
### Use Case 2: Scene-Specific Initialization
**Problem:** Need to initialize something after a scene finishes loading.
**Solution:**
```csharp
public class LevelManager : ManagedBehaviour
{
internal override void OnSceneReady()
{
// Scene is fully loaded, all objects exist
FindObjectiveMarkers();
SpawnEnemies();
PlayLevelMusic();
}
}
```
---
### Use Case 3: Saving Player Progress
**Problem:** Need to save level progress when transitioning between scenes.
**Solution:**
```csharp
public class LevelProgress : ManagedBehaviour
{
public override bool AutoRegisterForSave => true;
public override string SaveId => "LevelProgress";
private int _checkpointIndex;
private float _timeElapsed;
internal override string OnSceneSaveRequested()
{
return JsonUtility.ToJson(new
{
checkpoint = _checkpointIndex,
time = _timeElapsed
});
}
internal override void OnSceneRestoreRequested(string data)
{
var save = JsonUtility.FromJson<SaveData>(data);
_checkpointIndex = save.checkpoint;
_timeElapsed = save.time;
}
internal override void OnSceneRestoreCompleted()
{
// Restore complete, trigger UI update
UpdateProgressUI();
}
}
```
---
### Use Case 4: Global Persistent Data (Inventory)
**Problem:** Need to save player inventory across all scenes.
**Solution:**
```csharp
public class InventoryManager : ManagedBehaviour
{
public override bool AutoRegisterForSave => true;
public override string SaveId => "Inventory";
private List<string> _items = new List<string>();
internal override string OnGlobalSaveRequested()
{
// Save to global save file (not scene-specific)
return JsonUtility.ToJson(new { items = _items });
}
internal override void OnGlobalRestoreRequested(string data)
{
// Restore from global save file on boot
var save = JsonUtility.FromJson<SaveData>(data);
_items = new List<string>(save.items);
}
internal override void OnGlobalLoadCompleted()
{
// All global data loaded, safe to initialize UI
RefreshInventoryUI();
}
}
```
---
### Use Case 5: Auto-Registering as Pausable
**Problem:** Component implements `IPausable` and needs to register with `GameManager`.
**Solution:**
```csharp
public class AnimatedCharacter : ManagedBehaviour, IPausable
{
public override bool AutoRegisterPausable => true;
// IPausable implementation
public void Pause()
{
// Pause animations
}
public void Resume()
{
// Resume animations
}
}
// No manual registration needed - automatic!
```
---
### Use Case 6: Scene Cleanup
**Problem:** Need to clean up scene-specific state before transitioning.
**Solution:**
```csharp
public class ParticleSpawner : ManagedBehaviour
{
private List<GameObject> _activeParticles = new List<GameObject>();
internal override void OnSceneUnloading()
{
// Clean up before scene unloads
foreach (var particle in _activeParticles)
{
if (particle != null)
Destroy(particle);
}
_activeParticles.Clear();
}
}
```
---
### Use Case 7: First-Time vs Restored State
**Problem:** Need to play intro animation only on first visit, not when restoring from save.
**Solution:**
```csharp
public class LevelIntro : ManagedBehaviour
{
public override bool AutoRegisterForSave => true;
private bool _hasPlayedIntro;
internal override string OnSceneSaveRequested()
{
return JsonUtility.ToJson(new { hasPlayed = _hasPlayedIntro });
}
internal override void OnSceneRestoreRequested(string data)
{
var save = JsonUtility.FromJson<SaveData>(data);
_hasPlayedIntro = save.hasPlayed;
}
internal override void OnSceneRestoreCompleted()
{
// This fires whether we restored or not
if (!_hasPlayedIntro)
{
PlayIntroAnimation();
_hasPlayedIntro = true;
}
}
}
```
---
## Common Patterns
### Pattern: Manager Singleton
```csharp
public class MyManager : ManagedBehaviour
{
private static MyManager _instance;
public static MyManager Instance => _instance;
internal override void OnManagedAwake()
{
_instance = this;
}
}
```
### Pattern: Event Subscription
```csharp
internal override void OnManagedStart()
{
EventBus.OnSomething += HandleEvent;
}
internal override void OnManagedDestroy()
{
EventBus.OnSomething -= HandleEvent;
}
```
### Pattern: Save/Restore
```csharp
public override bool AutoRegisterForSave => true;
internal override string OnSceneSaveRequested()
{
return JsonUtility.ToJson(myData);
}
internal override void OnSceneRestoreRequested(string data)
{
myData = JsonUtility.FromJson<MyData>(data);
}
```
### Pattern: Custom SaveId
```csharp
// For singletons or special cases
public override string SaveId => "PlayerInventory";
// For scene-specific instances
public override string SaveId => $"{gameObject.scene.name}/MySpecialObject";
```
## Troubleshooting
### "NullReferenceException when accessing Manager.Instance"
**Problem:** Accessing singleton in `OnManagedAwake()`
**Solution:** Move to `OnManagedStart()` where managers are guaranteed ready
### "My SaveId is colliding with another component"
**Problem:** Two components have same GameObject name and type
**Solution:** Override `SaveId` property with unique value
### "My component isn't receiving lifecycle callbacks"
**Problem:** Not inheriting from `ManagedBehaviour`
**Solution:** Ensure class inherits `ManagedBehaviour` (not plain `MonoBehaviour`)
### "Save data isn't persisting"
**Problem:** `AutoRegisterForSave` is false
**Solution:** Set `public override bool AutoRegisterForSave => true;`
### "OnSceneRestoreCompleted isn't firing"
**Problem:** Missing implementation
**Solution:** Override the method even if just logging for now
---
**Quick Links:**
- [Technical Reference](01_technical_reference.md) - Complete API documentation
- [Architecture Overview](02_architecture_overview.md) - System design and principles

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB