Startup
Startup runs on the Login scene. It checks that the server accepts this game
build, signs the player back in from an earlier play when it can, and loads
the player's game data. Your Login scene shows one screen per step and opens
the game when startup reports Ready.
| You use | It gives you |
|---|---|
IProjectVStartupPresenter | Start startup and observe every step |
ProjectVStartupSnapshot | What to draw: the current step and why it stopped |
ProjectVStartupStatus | The steps, one screen or message each |
ProjectVStartupError | Why startup stopped: Category, Code, RequestId, LocalizationKey |
ProjectVStartupCommandResult | The outcome of StartAsync |
Startup has no screen contract. The Login scene uses the presenter directly, together with the Authentication presenter for the sign-in buttons.
Quick start
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using SilverTiger.ProjectV.Unity.Presentation.Startup;
using UnityEngine;
using VContainer;
public sealed class LoginStartupView : MonoBehaviour
{
private IProjectVStartupPresenter startup = null!;
private IDisposable? subscription;
private CancellationTokenSource? lifetime;
[Inject]
public void Construct(IProjectVStartupPresenter presenter) => startup = presenter;
private async void OnEnable()
{
lifetime = new CancellationTokenSource();
var token = lifetime.Token;
var startupSub = await startup.SubscribeAsync(OnStartupChanged);
if (token.IsCancellationRequested)
{
// The page closed while subscribing.
startupSub.Dispose();
return;
}
subscription = startupSub;
// Call this once your own content (Addressables and so on) has loaded.
await startup.StartAsync(contentReady: true, token);
}
private void OnDisable()
{
subscription?.Dispose();
subscription = null;
lifetime?.Cancel();
lifetime?.Dispose();
lifetime = null;
}
// May be called off the Unity main thread.
private void OnStartupChanged(ProjectVStartupSnapshot snapshot) => RenderAsync(snapshot).Forget();
private async UniTaskVoid RenderAsync(ProjectVStartupSnapshot snapshot)
{
await UniTask.SwitchToMainThread();
if (this == null)
{
return;
}
if (snapshot.IsReady)
{
OpenLobby();
return;
}
if (snapshot.RequiresLogin)
{
ShowSignInOptions();
return;
}
if (snapshot.IsBlocking)
{
ShowBlocking(snapshot.Status, snapshot.Error?.LocalizationKey);
return;
}
ShowLoading(snapshot.Status);
}
// Retry button on the blocking screen.
public async void OnRetryClicked()
{
if (lifetime != null)
{
await startup.StartAsync(contentReady: true, lifetime.Token);
}
}
private void OpenLobby() { }
private void ShowSignInOptions() { }
private void ShowBlocking(ProjectVStartupStatus status, string? localizationKey) { }
private void ShowLoading(ProjectVStartupStatus status) { }
}
That is the whole startup flow: subscribe, start, draw each step. After the
player signs in, call StartAsync(true, token) again and the listener walks
through Hydrating to Ready.
SubscribeAsync listeners may run off the Unity main thread. Switch to the
main thread before touching Unity objects, as RenderAsync does with
UniTask.SwitchToMainThread().
What to draw
The listener receives a
ProjectVStartupSnapshot:
| Member | Type | Use it for |
|---|---|---|
Status | ProjectVStartupStatus | The current step; pick the screen from the table below |
IsReady | bool | Status is Ready: open the game |
RequiresLogin | bool | Status is AwaitingLogin: show the sign-in options |
IsBlocking | bool | Startup stopped and cannot go on by itself. Show the matching message |
Error | ProjectVStartupError? | Why startup stopped, or null. LocalizationKey picks the message, RequestId goes in a support report |
startup.Current holds the same snapshot; it starts as WaitingForContent.
For each ProjectVStartupStatus:
| Status | What to show |
|---|---|
WaitingForContent | Your loading screen; startup has not begun |
Admitting | "Connecting…" |
RestoringSession | The loading screen; the player is being signed back in |
AwaitingLogin | The sign-in options (Guest, Google, Apple, password) |
Hydrating | The loading screen; the player's game data is loading |
Ready | Nothing; open the game |
Maintenance | A maintenance message; the player tries again later |
UpdateRequired | Ask the player to update from the store |
ContractMismatch | Ask the player to update the game |
Offline | A no-connection message with a Retry button |
Failed | An error message with a Retry button |
The last five are the blocking statuses (IsBlocking is true).
Start startup
Task<ProjectVStartupCommandResult> StartAsync(bool contentReady, CancellationToken cancellationToken)
| Parameter | Type | Value |
|---|---|---|
contentReady | bool | true once the game's own content has loaded. false starts nothing when startup is not already running |
cancellationToken | CancellationToken | Stops your wait for the result. Startup itself keeps running and keeps publishing to the listener |
Returns a
ProjectVStartupCommandResult:
IsSuccess | Snapshot.Status | What happened |
|---|---|---|
true | Ready | The player is signed in and the game data is loaded. Open the game |
true | AwaitingLogin | No saved sign-in. Show the sign-in options |
true | WaitingForContent | You passed contentReady: false and startup was not running; nothing started |
false | a blocking status | Startup stopped. Show the message for that status (see below) |
- Calling
StartAsyncwhile startup is running joins that run and returns its result; it does not start a second one, even withcontentReady: false. - Call
StartAsync(true, token)again to retry after a blocking status, and after a successful sign-in. - When your token fires first, the result is
Failedwith theCancelledcategory. That result is only yours: the listener andCurrentkeep following the real run.
When it stops — show the message for the status, using
Error.LocalizationKey (projectv.startup.<category>) when Error is set:
| Situation | Status | What the player sees |
|---|---|---|
| The server is under maintenance | Maintenance | A maintenance message. Retry later |
| This game version is too old | UpdateRequired | Ask the player to update, with a store link |
| This build and the server do not match | ContractMismatch | Ask the player to update. In development it means the Client build and the backend environment are out of step; it is not a Client bug |
| The server cannot be reached | Offline | A no-connection message with Retry (Error.Category is Network or Unavailable) |
| Anything else | Failed | An error message with Retry. Codes include PROJECTV_STARTUP_BACKEND_FAILED and PROJECTV_STARTUP_UNEXPECTED_FAILURE |
| Your token fired before startup finished | Failed (result only) | Nothing; the page is closing |
| A saved sign-in was no longer accepted | AwaitingLogin with Error set | The sign-in options. IsSuccess is still true |
Never open the game past a blocking status.
Rules
- Pass
contentReady: trueonly when the game's own content is loaded. - Navigate on
IsReady, never on a successful sign-in alone. - Startup does not queue commands like the other presenters: every call joins the one run.
Error.Codeis for branching and support, not text for the player. Show the message forLocalizationKey, andRequestIdin a support panel.
Common mistakes
- Treating a cancelled
StartAsyncas stopping startup. It only stops your wait; draw from the listener, not only from the result. - Calling
StartAsyncfrom every button. Call it once when the scene opens, again after sign-in, and from Retry. - Touching Unity objects in the listener without switching threads. Listeners may run off the main thread.
- Opening the Lobby straight after a sign-in. Call
StartAsync(true, token)and wait forReadyfirst, so the player's data is loaded. - Not disposing the subscription. Dispose it in
OnDisableorOnDestroy.
UI binding helper
startup.SubscribeUiAsync(ProjectVUiBindingId.LoginStartup, apply, token)
works like SubscribeAsync and also marks in the SDK log when your apply
returned (see UI subscriptions and
ProjectVUiSubscriptions).
It does not switch threads: apply runs on the thread that delivers the
update, so switch to the main thread yourself.