Files
nightclub-arcadia/NightclubArcadia/Assets/Scripts/Core/GameBootstrap.cs
T
lennartandClaude Opus 5 40b4547c71 split the scene into Bootstrap, Systems and Level
The game now starts from Bootstrap.unity, which additively loads Systems (the
dialogue runner, skills, UI layer, camera rig and the player) and then a level
(geometry, light, navmesh, reveal cameras). The level becomes the active scene so
new objects and lighting land there.

The player moved into Systems. It had been parented under Room_101 — under level
geometry — and it is persistent content, not level content. Moving it also
removes most of the cross-scene breakage on its own.

Unity nulls any serialized reference that crosses a scene boundary. Measured
rather than guessed: the pre-split scene was pulled from git and its null
references diffed against the split result. Exactly seven were lost — both NPCs'
dialogueRunner, dialogueUI and player, plus the reveal camera's tracking target.
A first naive audit reported 210, which turned out to be pre-existing Unity
defaults like Image.m_Material; the baseline diff is what separated the two.

Each of the seven now resolves at runtime. SceneServices.Resolve fills in a null
Inspector reference by searching the loaded scenes, keeping an assigned value if
there is one; CinemachineFollowsPlayer binds the reveal camera once the player
exists. Both follow the pattern already used by CameraFramingVolume and by
NPCStandIn's player lookup, rather than introducing a new one.

Assets/Tests/PlayMode is new, and it immediately paid for itself. BootstrapTests
loads Bootstrap and asserts the whole game comes up; on its first run it caught a
real bug the split had introduced. The player's NavMeshAgent lives in Systems
while the NavMesh is baked into the level, so during load the agent exists
off-mesh and ResetPath logs an error. ClickToMoveController now guards on
agent.isOnNavMesh rather than a bare null check. No EditMode test could have seen
that, because none of it has run yet.

Two of those checks reach their types by name through reflection: NPCStandIn and
the Cinematics namespace live in Assembly-CSharp, and an asmdef test assembly
cannot reference the predefined assemblies. Per-area asmdefs remove the need.

Build settings list all three scenes with Bootstrap at index 0, which
LoadSceneAsync by name requires.

EditMode 42/42, PlayMode 5/5, YarnCheck 9 files / 35 nodes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 20:50:25 +02:00

66 lines
2.3 KiB
C#

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace NightclubArcadia.Core
{
/// <summary>
/// The entry point. Loads the persistent Systems scene, then a level, additively.
///
/// Splitting the game across scenes is what lets a level be edited without touching
/// the systems wiring, and stops a corrupted level from taking the dialogue runner,
/// the UI layer and the player with it. The cost is that serialized references
/// cannot cross a scene boundary — Unity nulls those — so the handful of level
/// objects that need a system resolve it at runtime instead. See SceneServices.
/// </summary>
public sealed class GameBootstrap : MonoBehaviour
{
[Tooltip("Persistent scene holding dialogue, skills, UI, the camera rig and the player.")]
[SerializeField] string systemsScene = "Systems";
[Tooltip("Level to open on start. Leave empty to load only the systems layer.")]
[SerializeField] string startLevel = "SC101_ConferenceHall";
IEnumerator Start()
{
yield return LoadIfNeeded(systemsScene);
if (!string.IsNullOrEmpty(startLevel))
{
yield return LoadIfNeeded(startLevel);
var level = SceneManager.GetSceneByName(startLevel);
if (level.IsValid() && level.isLoaded)
{
// The active scene decides where new objects land and which
// lighting settings apply, so it must be the level, not this one.
SceneManager.SetActiveScene(level);
}
}
}
static IEnumerator LoadIfNeeded(string sceneName)
{
if (string.IsNullOrEmpty(sceneName))
{
yield break;
}
var existing = SceneManager.GetSceneByName(sceneName);
if (existing.IsValid() && existing.isLoaded)
{
yield break;
}
var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
if (op == null)
{
Debug.LogError($"[GameBootstrap] '{sceneName}' is not in the build settings.");
yield break;
}
yield return op;
}
}
}