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]>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NightclubArcadia.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Finds objects that live in a different loaded scene.
|
||||
///
|
||||
/// Unity cannot serialize a reference across a scene boundary — the Inspector
|
||||
/// refuses it and any existing reference is nulled when the scenes are split. So a
|
||||
/// level object that needs the dialogue runner, or the player, has to resolve it at
|
||||
/// runtime. This is a lookup helper, not a service container: there is no
|
||||
/// registration step and nothing to keep in sync.
|
||||
///
|
||||
/// It is deliberately thin. FindFirstObjectByType is not cheap, so callers cache
|
||||
/// what they get in Awake rather than calling per frame, and an Inspector-assigned
|
||||
/// reference always wins — this only fills in what the split left null.
|
||||
/// </summary>
|
||||
public static class SceneServices
|
||||
{
|
||||
/// <summary>Inspector value if set, otherwise the first one in any loaded scene.</summary>
|
||||
public static T Resolve<T>(T assigned, Object context, string field) where T : Object
|
||||
{
|
||||
if (assigned != null)
|
||||
{
|
||||
return assigned;
|
||||
}
|
||||
|
||||
var found = Object.FindFirstObjectByType<T>();
|
||||
if (found == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[SceneServices] {context?.name}: could not resolve {field} ({typeof(T).Name}). " +
|
||||
"Is the Systems scene loaded?", context);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
static Transform player;
|
||||
|
||||
/// <summary>
|
||||
/// The player transform, by tag, cached. Null between scene loads if the
|
||||
/// systems scene has not come up yet, so callers must handle null.
|
||||
/// </summary>
|
||||
public static Transform Player
|
||||
{
|
||||
get
|
||||
{
|
||||
if (player == null)
|
||||
{
|
||||
var go = GameObject.FindGameObjectWithTag("Player");
|
||||
player = go != null ? go.transform : null;
|
||||
}
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Drop the cache. Call when unloading a scene that held the player.</summary>
|
||||
public static void Forget() => player = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user