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]>
62 lines
2.2 KiB
C#
62 lines
2.2 KiB
C#
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;
|
|
}
|
|
}
|