Files
AppleHillsProduction/docs/onsceneready_not_called_analysis.md
2025-11-05 20:37:16 +01:00

5.8 KiB

OnSceneReady() Not Called - Root Cause Analysis & Fix

Problem

Two ManagedBehaviours were not receiving their OnSceneReady() callbacks:

  1. PuzzleManager (bootstrapped singleton in DontDestroyOnLoad)
  2. LevelSwitch (in-scene component)

Root Cause

The Design of OnSceneReady()

LifecycleManager.BroadcastSceneReady(sceneName) only calls OnSceneReady() on components IN the specific scene being loaded:

public void BroadcastSceneReady(string sceneName)
{
    foreach (var component in sceneReadyList)
    {
        if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
        {
            component.InvokeSceneReady(); // Only if compScene == sceneName!
        }
    }
}

When a component registers, LifecycleManager tracks which scene it belongs to:

var sceneName = component.gameObject.scene.name;
componentScenes[component] = sceneName;

PuzzleManager Issue

Registration Log:

[LifecycleManager] Registered PuzzleManager(Clone) (Scene: DontDestroyOnLoad)

The Problem:

  • PuzzleManager is in scene "DontDestroyOnLoad" (bootstrapped)
  • When AppleHillsOverworld loads, BroadcastSceneReady("AppleHillsOverworld") is called
  • Lifecycle manager only broadcasts to components where compScene == "AppleHillsOverworld"
  • PuzzleManager's compScene == "DontDestroyOnLoad"
  • Result: OnSceneReady() never called!

LevelSwitch Issue

LevelSwitch should work since it's IN the gameplay scene. However, we need to verify with debug logging to confirm:

  1. That it's being registered
  2. That the scene name matches
  3. That BroadcastSceneReady is being called with the correct scene name

Solution

For PuzzleManager (and all bootstrapped singletons)

Don't use OnSceneReady() - it only works for components IN the scene being loaded

Use SceneManagerService.SceneLoadCompleted event - fires for ALL scene loads

Before (Broken):

protected override void OnSceneReady()
{
    // Never called because PuzzleManager is in DontDestroyOnLoad!
    string sceneName = SceneManager.GetActiveScene().name;
    LoadPuzzlesForScene(sceneName);
}

After (Fixed):

protected override void OnManagedAwake()
{
    // Subscribe to scene load events - works for DontDestroyOnLoad components!
    if (SceneManagerService.Instance != null)
    {
        SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
    }
}

private void OnSceneLoadCompleted(string sceneName)
{
    LoadPuzzlesForScene(sceneName);
}

protected override void OnDestroy()
{
    if (SceneManagerService.Instance != null)
    {
        SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
    }
}

For LevelSwitch

Added comprehensive debug logging to trace the lifecycle:

  • Awake call confirmation
  • Scene name verification
  • OnManagedAwake call confirmation
  • OnSceneReady call confirmation

This will help identify if the issue is:

  • Registration not happening
  • Scene name mismatch
  • BroadcastSceneReady not being called

Design Guidelines

When to use OnSceneReady():

Scene-specific components (components that live IN a gameplay scene)

  • Level-specific initializers
  • Scene decorators
  • In-scene interactables (like LevelSwitch should be)

When NOT to use OnSceneReady():

Bootstrapped singletons (components in DontDestroyOnLoad)

  • PuzzleManager
  • InputManager
  • Any manager that persists across scenes

Alternative for bootstrapped components:

Subscribe to SceneManagerService.SceneLoadCompleted event

  • Fires for every scene load
  • Provides scene name as parameter
  • Works regardless of component's scene

Pattern Summary

Bootstrapped Singleton Pattern:

public class MyBootstrappedManager : ManagedBehaviour
{
    protected override void OnManagedAwake()
    {
        // Subscribe to scene events
        if (SceneManagerService.Instance != null)
        {
            SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
        }
    }
    
    private void OnSceneLoadCompleted(string sceneName)
    {
        // Handle scene load
    }
    
    protected override void OnDestroy()
    {
        // Unsubscribe
        if (SceneManagerService.Instance != null)
        {
            SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
        }
        base.OnDestroy();
    }
}

In-Scene Component Pattern:

public class MySceneComponent : ManagedBehaviour
{
    protected override void OnSceneReady()
    {
        // This WILL be called for in-scene components
        // Safe to initialize scene-specific stuff here
    }
}

Files Modified

  1. PuzzleManager.cs

    • Removed OnSceneReady() override
    • Added subscription to SceneLoadCompleted event in OnManagedAwake()
    • Added OnSceneLoadCompleted() callback
    • Added proper cleanup in OnDestroy()
  2. LevelSwitch.cs

    • Added comprehensive debug logging
    • Kept OnSceneReady() since it should work for in-scene components
    • Will verify with logs that it's being called

Expected Behavior After Fix

PuzzleManager:

[LifecycleManager] Registered PuzzleManager(Clone) (Scene: DontDestroyOnLoad)
[PuzzleManager] OnManagedAwake called
[SceneManagerService] Scene loaded: AppleHillsOverworld
[Puzzles] Scene loaded: AppleHillsOverworld, loading puzzle data

LevelSwitch:

[LevelSwitch] Awake called for LevelSwitch in scene AppleHillsOverworld
[LifecycleManager] Registered LevelSwitch (Scene: AppleHillsOverworld)
[LifecycleManager] Broadcasting SceneReady for scene: AppleHillsOverworld
[LevelSwitch] OnManagedAwake called for LevelSwitch
[LevelSwitch] OnSceneReady called for LevelSwitch

Status: PuzzleManager fixed. LevelSwitch debug logging added for verification.