Campaign
The Campaign screen lists the missions, shows what is locked, the best-run stars and milestones and what a run can drop, and starts a mission before the gameplay scene loads. Missions, locks, stamina costs and stars are decided by the server.
| You use | It gives you |
|---|---|
IProjectVCampaignScreen | The screen contract: observe the missions, start one, release it |
ProjectVCampaignView | What to draw: loading state, missions, total stars |
ProjectVCampaignMission | One mission: lock state, entry cost, stars, drop preview |
ProjectVCampaignMissionStars | A mission's star objectives, milestones and best run |
ProjectVResult | The outcome of a command |
The run itself is played by the gameplay scene; see Gameplay data.
Quick startโ
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SilverTiger.ProjectV.Unity.Presentation.Campaign;
using UnityEngine;
using VContainer;
public sealed class CampaignPage : MonoBehaviour
{
private IProjectVCampaignScreen campaign = null!;
private IDisposable? observation;
private bool starting;
[Inject]
public void Construct(IProjectVCampaignScreen screen) => campaign = screen;
private void OnEnable() => observation = campaign.Observe(Render);
private void OnDisable()
{
observation?.Dispose();
observation = null;
}
private void Render(ProjectVCampaignView view)
{
if (view.IsLoading)
{
ShowLoading(view.LoadError); // LoadError is null while the first load is running
return;
}
ShowMissions(view.Missions, view.TotalStars);
}
public async void OnStartClicked(ProjectVCampaignMission mission)
{
// A second tap would get the first tap's result; start and load only once.
if (starting)
{
return;
}
starting = true;
var result = await campaign.BeginMissionAsync(mission.MissionId.Value);
if (this == null)
{
return;
}
if (!result.IsSuccess)
{
starting = false;
ShowMessage(result.Message);
return;
}
var sceneName = FindScene(mission.GameplayKey.Value);
if (string.IsNullOrEmpty(sceneName))
{
// No scene for this mission: release the run so it costs nothing.
await campaign.CancelSceneLoadAsync();
starting = false;
return;
}
LoadScene(sceneName); // your scene loader; on failure call CancelSceneLoadAsync
}
private void ShowLoading(string? error) { }
private void ShowMissions(IReadOnlyList<ProjectVCampaignMission> missions, long totalStars) { }
private void ShowMessage(string message) { }
private string? FindScene(string gameplayKey) => null;
private void LoadScene(string sceneName) { }
}
That is the whole page: observe, draw, start a mission. After a run ends you do
nothing; when the player comes back, IsCompleted, Stars and stamina are
already up to date. Screen contracts
shows the same flow from ProjectV-Client.
What to drawโ
Render receives a ProjectVCampaignView:
| Member | Type | Use it for |
|---|---|---|
IsLoading | bool | True until the missions arrive the first time. Show a spinner |
LoadError | string? | Set when that first load failed. Show it with a Retry button that closes and reopens the page |
Missions | IReadOnlyList<ProjectVCampaignMission> | The missions, with the player's progress on each |
TotalStars | long | Stars earned across all missions |
Find(missionId) | ProjectVCampaignMission? | Look up one mission, for example to refresh an open mission popup |
For each ProjectVCampaignMission:
| To show | Read |
|---|---|
| Which scene to load | GameplayKey.Value, mapped to a scene in a table of your own |
| The entry cost | EntryStamina |
| The time limit | DurationSeconds |
| Completed | IsCompleted |
| A run is open | IsActive |
| Locked | IsLocked: closed, a required mission not completed, or too few total stars. Disable Start |
| Not open yet | IsClosed; a closed mission is always IsLocked |
| "Complete X first" | RequiredMissionIds |
| What a run can drop | DropPreview: Kind, ResourceId, Amount (the most a run can drop) |
| Stars | Stars (below) |
For Stars, a ProjectVCampaignMissionStars:
| To show | Read |
|---|---|
| "2/3 stars" | BestStars / MaxStars |
| The objectives | Objectives: ObjectiveId, Kind, Threshold; tick the ones in BestRunObjectiveIds |
| Milestone rewards | Milestones: StarsRequired, Rewards, IsClaimed |
| "Needs 12 stars" | RequiresTotalStars, with IsLocked while TotalStars is below it |
Objective Kind is one of complete_stage, duration_at_most_ms,
enemies_defeated_min, health_percent_min and stages_cleared_min;
Threshold is in the unit the kind names, and null for complete_stage. Use
the kind to choose the objective text. A mission without stars has
ProjectVCampaignMissionStars.None.
Match a mission to your art and text with MissionId.Value in a table of your
own; the SDK does not provide names or icons.
Start a missionโ
Task<ProjectVResult> BeginMissionAsync(string missionId)
| Parameter | Type | Value |
|---|---|---|
missionId | string | mission.MissionId.Value of a mission in the current view that is not locked |
The server starts a run and checks the entry stamina. Stamina is charged when the run ends, not now.
Returns a ProjectVResult:
IsSuccess | Message | What happened |
|---|---|---|
true | empty | The server started the run. Load the gameplay scene |
false | the reason, ready to show | The run did not start, or could not be confirmed (see below) |
When it fails โ show Message as it is. The common reasons:
| Situation | What the player sees |
|---|---|
| Not enough stamina | The server's message (code STAMINA_INSUFFICIENT) |
| The mission is locked | The server's message (code MISSION_RUN_LOCKED) |
| Too few total stars | The server's message (code MISSION_RUN_LOCKED_STARS) |
A null or blank missionId | "The request was not confirmed. Please try again." |
| No connection or a timeout | A network message. Do not start again automatically |
One start at a time, for all missions. A second tap while a start is running โ on the same mission or another one โ gets the first mission's result. Load the gameplay scene only once, for the mission that started; the result can also arrive after the page closed.
Release a run that cannot loadโ
Task<ProjectVResult> CancelSceneLoadAsync()
Call it after a successful BeginMissionAsync when your table has no scene
for the mission's GameplayKey, or when the scene fails to load. A released
run costs no stamina.
Returns a ProjectVResult:
IsSuccess | Message | What happened |
|---|---|---|
true | empty | The server released the run. The mission list redraws by itself |
false | the reason, ready to show | The run was not released; show Message and let the player try again |
A second call while one is running gets the same result.
Stamina and starsโ
- Entry stamina is charged when the run ends: a win, a loss or a give-up. A restart in the gameplay scene, a released run, or a killed or crashed app costs nothing.
- The server scores stars when the run ends. Campaign shows
BestStarsand claimed milestones; the gameplay scene shows the stars of the run it just played. - Show locks from
IsLockedandStars.IsLocked. Never work them out from local progress; the server refuses to start a locked mission anyway. - For QA, the staging backend can unlock every mission; Campaign then shows them all unlocked.
Common mistakesโ
- Loading the gameplay scene twice. A joined tap can resolve after the load already started. Keep a flag and load once.
- Tapping a second mission while the first is starting. It returns the first mission's result; do not load the second mission's scene for it.
- Leaving a started run open when the scene cannot load. Call
CancelSceneLoadAsync. - Taking stamina off the display at Start. Stamina is charged when the run ends; the Header updates by itself.
- Working out locks or stars from local data. Use the view.
- Using the result after the page closed. Check
this != nullafterawait. - Not disposing the observation. Keep the
IDisposablefromObserveand dispose it inOnDisableorOnDestroy.
Presenterโ
IProjectVCampaignPresenter
is the lower-level interface behind the screen contract. Use it only if you
need what the screen does not give:
| Member | Use it for |
|---|---|
SubscribeAsync(listener) | Every ProjectVCampaignSnapshot: Status (Empty, Refreshing, Ready, Beginning, Failed), Recovery, Data |
Current.Error | A full ProjectVCampaignError with Code and RequestId, for example to choose your own text per code |
RefreshAsync(token) | Load the missions (returns at once when already loaded) |
BeginAsync(missionId, token) | Presenter form of BeginMissionAsync |
CancelSceneLoadAdmissionAsync(token) | Presenter form of CancelSceneLoadAsync |
The commands return a ProjectVCampaignCommandResult.
SubscribeAsync listeners may run off the Unity main thread; switch to the
main thread before touching Unity objects. The screen's Observe always
renders on the main thread.