Skip to main content

Scene integration

The SDK root in the Login scene provides every screen contract and presenter. Each game scene only registers its own UI components in a scope derived from an SDK base class, and receives what it needs by injection.

1. Assembly referencesโ€‹

Code in Assembly-CSharp sees the SDK types without any reference. A game .asmdef references SilverTiger.ProjectV.Unity, plus VContainer when it injects:

{
"name": "ProjectV.Game.Presentation",
"references": [
"SilverTiger.ProjectV.Unity",
"VContainer",
"UniTask"
]
}

Do not reference other SDK assemblies, SilverTiger.CrossSDK.* or Nakama from game code.

2. One root in the Login sceneโ€‹

The Login scene contains the one SDK root created by Setup. It stays alive when other scenes load. Never add another root, and never register screen contracts or presenters yourself in a scene scope.

3. Login scene scopeโ€‹

Derive the Login scope from ProjectVLoginSceneScope and register the Login UI:

using SilverTiger.ProjectV.Unity.Presentation;
using VContainer;
using VContainer.Unity;

public sealed class LoginSceneScope : ProjectVLoginSceneScope
{
protected override void ConfigureLogin(IContainerBuilder builder)
{
builder.RegisterComponentInHierarchy<PopupLogin>();
}
}

4. Authenticated scene scopesโ€‹

Lobby, Shop, Character, Alliance and Campaign derive from ProjectVAuthenticatedSceneScope. When nobody is signed in (for example when the scene is opened directly in the Editor), the base class sends the player to Login.

using SilverTiger.ProjectV.Unity.Presentation;
using UnityEngine;
using UnityEngine.SceneManagement;
using VContainer;
using VContainer.Unity;

public sealed class LobbySceneScope : ProjectVAuthenticatedSceneScope
{
protected override void ConfigureAuthenticated(IContainerBuilder builder)
{
builder.RegisterComponentInHierarchy<LobbyUIManager>();
builder.RegisterComponentInHierarchy<HeaderUIManager>();
}

protected override void DisableScenePresentation()
{
var lobby = FindFirstObjectByType<LobbyUIManager>(FindObjectsInactive.Include);
if (lobby != null)
{
lobby.enabled = false;
}
}

protected override void NavigateToLogin(string sceneName)
{
SceneManager.LoadScene(sceneName);
}
}

DisableScenePresentation must disable every UI script of the scene before going to Login, so none of them sends a command. Override LoginSceneName only if the Login scene is not called Login.

5. Inject into viewsโ€‹

Ask for the screen contract (or the presenter, for screens without one) in an [Inject] method:

using System;
using SilverTiger.ProjectV.Unity.Presentation.Lobby;
using UnityEngine;
using VContainer;

public sealed class LobbyUIManager : MonoBehaviour
{
private IProjectVLobbyScreen lobby = null!;

[Inject]
public void Construct(IProjectVLobbyScreen screen)
{
lobby = screen ?? throw new ArgumentNullException(nameof(screen));
}
}

Do not look them up with FindObjectOfType, create them with new or keep them in static fields.

6. Startup sequence on Loginโ€‹

Subscribe to Authentication and Startup snapshots
-> StartAsync(contentReady)
Ready -> go to Lobby (session restored)
AwaitingLogin -> show login choices
Maintenance / UpdateRequired / ContractMismatch / Offline / Failed -> blocking UI
-> authenticate (Guest, password, Google or Apple)
-> StartAsync(contentReady) again
-> Ready -> go to Lobby

Pass contentReady: true only after the game's own content (Addressables, localization) has loaded; until then startup waits in WaitingForContent. Never let the player past a blocking status.

7. Mission sceneโ€‹

Campaign calls IProjectVCampaignScreen.BeginMissionAsync(missionId); when it succeeds, the game loads the gameplay scene. That scene must contain exactly one component implementing IProjectVGameplayDataConsumer (components on inactive objects count too). The SDK calls its BindGameplayData(presenter, startingData) with the run the server started, and UnbindGameplayData when the run ends. If the scene cannot load, call CancelSceneLoadAsync so the run is released. See Gameplay data.

8. Page lifetimeโ€‹

Call Observe (or SubscribeAsync) when a page opens and dispose that exact IDisposable when it closes. Never poll from Update().

Continue with the Presenter reference and Presentation recipes.