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]>
66 lines
2.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|