Scene References for Unity Chapter Loader SO

Summary

The user is developing a chapter-based scene loading system in Unity, inspired by games like Half-Life. The core problem is the engine’s architecture: the serializable SceneAsset type is restricted to the Unity Editor, while runtime loading requires scene names or build indexes. The user attempted a solution involving Editor-only serialized fields and an Inspector button to sync data, but rejected it as overly complex. The root cause is a mismatch between author-time (Editor) and runtime data requirements. The fix involves accepting the Editor-only pattern as a best practice, but implementing it cleanly to avoid manual button clicks and reduce script bloat.

Root Cause

The issue stems from two fundamental Unity constraints:

  • SceneAsset is Editor-Only: SceneAsset exists only in the Unity Editor assembly. It cannot be compiled into a standalone build, making it unusable for runtime logic.
  • Build Index Instability: Relying on SceneManager.LoadScene(int buildIndex) is dangerous. Reordering scenes in Build Settings invalidates hard-coded indexes, leading to runtime errors.
  • Strings are Brittle: Using raw string names for scenes prevents compile-time validation. A typo or renamed scene file results in a silent runtime failure.

Why This Happens in Real Systems

This friction exists because Unity separates the authoring phase (where designers need intuitive, type-safe references) from the execution phase (where the engine needs lightweight, performant identifiers like strings or integers).

  • Data Design Principle: You need dual-surface data. One surface for the Editor (visual, type-safe) and one for Runtime (serializable, simple).
  • Pipeline Automation: Senior engineers rarely rely on manual processes (like clicking a “Sync” button). If data transformation is required, it must be automated via AssetPostprocessors or OnValidate hooks to ensure the runtime data always matches the author-time intent.

Real-World Impact

  • Compilation Failures: Using SceneAsset in a script that is included in a build will cause the build to fail.
  • Refactoring Risk: If you rely on string syncing, any developer renaming a scene file risks breaking the Chapter System if they forget to manually update the ScriptableObject.
  • Scalability Issues: A system that requires “4 different scripts for one system” (as the user mentioned) increases cognitive load and maintenance overhead, violating the DRY (Don’t Repeat Yourself) principle and making onboarding new developers difficult.

Example or Code

The correct approach is to keep the visual SceneAsset fields in the Editor but automatically populate the runtime string fields using OnValidate. This removes the need for a custom Inspector button or a separate editor script.

Here is the optimized ChapterSO.cs:

using UnityEngine;

[System.Serializable]
public class ChapterData
{
    public string chapterName;
    public int chapterOrder;

    // Editor-only references (Visual)
    public SceneAsset startMapEditor;
    public SceneAsset endMapEditor;
    public SceneAsset[] includedMapsEditor;

    // Runtime data (Functional)
    public string startMapName;
    public string endMapName;
    public string[] includedMapNames;
}

[CreateAssetMenu(fileName = "NewChapter", menuName = "World Power/Chapter")]
public class ChapterSO : ScriptableObject
{
    public ChapterData data;

#if UNITY_EDITOR
    // This runs in the Editor whenever the asset is loaded or changed
    private void OnValidate()
    {
        // Auto-populate strings from SceneAssets
        if (data.startMapEditor != null)
            data.startMapName = data.startMapEditor.name;

        if (data.endMapEditor != null)
            data.endMapName = data.endMapEditor.name;

        if (data.includedMapsEditor != null && data.includedMapsEditor.Length > 0)
        {
            data.includedMapNames = new string[data.includedMapsEditor.Length];
            for (int i = 0; i < data.includedMapsEditor.Length; i++)
            {
                if (data.includedMapsEditor[i] != null)
                    data.includedMapNames[i] = data.includedMapsEditor[i].name;
            }
        }

        // Mark the asset dirty to ensure changes are saved
        UnityEditor.EditorUtility.SetDirty(this);
    }
#endif
}

How Senior Engineers Fix It

Senior engineers solve this by automating the bridge between Editor and Runtime data. The key is OnValidate.

  1. Embrace the Preprocessor: Use #if UNITY_EDITOR to ensure Editor-only code is stripped from builds. This keeps the build clean and performant.
  2. Automate Syncing: Instead of a button, use OnValidate(). This method fires whenever a variable changes in the Inspector. It ensures the string fields are always up to date with the SceneAsset references without user intervention.
  3. Encapsulate Logic: The custom Inspector approach mentioned by the user is viable, but OnValidate is cleaner for simple data syncing because it keeps the logic inside the data asset itself, rather than requiring a separate Editor class file.
  4. Runtime Loader: The loader should rely strictly on the strings generated by OnValidate. It should never touch SceneAsset.

Why Juniors Miss It

  • Lack of Awareness of OnValidate: Juniors often don’t know this lifecycle hook exists in ScriptableObject.
  • Fear of Preprocessor Directives: #if UNITY_EDITOR looks intimidating, but it is the standard tool for this problem.
  • Over-reliance on Manual Steps: Juniors often think in terms of “user actions” (clicking a button) rather than “data integrity” (automation).
  • Trying to Force Types: The user initially wanted to “store scenes directly,” not realizing that Scene references are fundamentally handled differently by the engine depending on context (Editor vs. Build).