Header
The top bar on every signed-in scene shows the player's currency balances and stamina. It updates by itself whenever the server confirms a change (a purchase, a mission, an upgrade, a daily grant); the header has no commands.
| You use | It gives you |
|---|---|
IProjectVHeaderPresenter | Observe the header |
ProjectVHeaderSnapshot | What to draw: balances and stamina |
ProjectVHeaderCurrencyValue | One balance and the slot it goes in |
ProjectVStaminaRecovery | Live stamina and its countdown |
ProjectVStaminaReading | Stamina at one moment, with the waits until the next point and until full |
ProjectVHeaderCurrencyCatalogAsset | Which currency each header slot shows |
The header has no screen contract; the top bar subscribes to the presenter.
Quick start​
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using SilverTiger.ProjectV.Unity.Presentation;
using SilverTiger.ProjectV.Unity.Presentation.Header;
using TMPro;
using UnityEngine;
using VContainer;
public sealed class TopBar : MonoBehaviour
{
[SerializeField] private TMP_Text coreDustText = null!;
[SerializeField] private TMP_Text staminaText = null!;
[SerializeField] private TMP_Text countdownText = null!;
private IProjectVHeaderPresenter header = null!;
private IDisposable? subscription;
private CancellationTokenSource? lifetime;
private ProjectVStaminaRecovery? recovery;
[Inject]
public void Construct(IProjectVHeaderPresenter presenter) => header = presenter;
private async void OnEnable()
{
lifetime = new CancellationTokenSource();
var token = lifetime.Token;
var headerSub = await header.SubscribeAsync(OnHeaderChanged);
if (token.IsCancellationRequested)
{
// The top bar closed while subscribing.
headerSub.Dispose();
return;
}
subscription = headerSub;
}
private void OnDisable()
{
subscription?.Dispose();
subscription = null;
lifetime?.Cancel();
lifetime?.Dispose();
lifetime = null;
recovery = null; // so a late update cannot bring back an old countdown
}
// May be called off the Unity main thread.
private void OnHeaderChanged(ProjectVHeaderSnapshot snapshot) => RenderAsync(snapshot).Forget();
private async UniTaskVoid RenderAsync(ProjectVHeaderSnapshot snapshot)
{
await UniTask.SwitchToMainThread();
if (this == null || !isActiveAndEnabled)
{
return;
}
foreach (var currency in snapshot.Currencies)
{
if (currency.ViewKey.Equals(ProjectVHeaderViewKeys.CoreDust))
{
coreDustText.text = currency.Amount.ToString();
}
}
recovery = snapshot.StaminaRecovery;
}
private void Update()
{
if (recovery == null)
{
return;
}
var reading = recovery.Read(DateTimeOffset.UtcNow);
staminaText.text = $"{reading.Stamina}/{reading.Maximum}";
countdownText.gameObject.SetActive(!reading.IsFull);
if (reading.UntilNextPoint is TimeSpan wait)
{
countdownText.text = wait.ToString(@"mm\:ss");
}
}
}
That is the whole top bar: subscribe, store, read stamina every frame. Never add a purchase or a mission reward to a balance yourself; the next snapshot carries the confirmed value.
SubscribeAsync listeners may run off the Unity main thread. Switch to the
main thread before touching Unity objects, as RenderAsync does.
What to draw​
The listener receives a
ProjectVHeaderSnapshot:
| Member | Type | Use it for |
|---|---|---|
Currencies | IReadOnlyList<ProjectVHeaderCurrencyValue> | One balance per header slot, in catalog order. Empty until HasEconomy |
StaminaRecovery | ProjectVStaminaRecovery? | Live stamina and the countdown; null until stamina is known |
Stamina | long? | The raw value the server last reported. Not live: use StaminaRecovery.Read(DateTimeOffset.UtcNow) to show stamina |
HasEconomy | bool | The balances are loaded |
HasStamina | bool | Stamina is loaded |
IsReady | bool | Both are loaded; the whole header can be shown |
Revision | long | Grows with every update. Not needed in UI |
header.Current holds the latest snapshot; check IsReady before showing it.
For each ProjectVHeaderCurrencyValue:
| Member | Use it for |
|---|---|
ViewKey | The slot to show it in. Compare with Equals against ProjectVHeaderViewKeys |
CurrencyId | The wallet currency, for example core_dust |
Amount | The balance, never negative; 0 when the player has none |
Currencies​
Which currency goes in which slot is set in an asset, not in code. A
ProjectVHeaderCurrencyCatalogAsset (create it from
Assets › Create › ProjectV › Backend V3 › Header Currency Catalog) holds
one row per slot: a view key and a currency ID, for example
header.core_dust → core_dust. Assign it to the root's
ProjectVPresentationLifetimeScope. In ProjectV it is
Assets/_Game/BackendV3/Settings/ProjectVHeaderCurrencyCatalog.asset.
The built-in slots are in
ProjectVHeaderViewKeys:
| Key | Slot |
|---|---|
Credit | header.credit |
Gold | header.gold |
CoreDust | header.core_dust |
Diamond | header.diamond |
Each view key may appear only once in the catalog; an empty, blank or
duplicate row fails when the scope is built. Without an asset, Currencies
stays empty.
Stamina countdown​
ProjectVStaminaReading Read(DateTimeOffset nowUtc)
| Parameter | Type | Value |
|---|---|---|
nowUtc | DateTimeOffset | DateTimeOffset.UtcNow |
Stamina keeps recovering between server updates. Read works out the value
at nowUtc locally, without asking the server, so call it every frame or
every second.
Returns a ProjectVStaminaReading:
| Member | Type | Use it for |
|---|---|---|
Stamina | long | The stamina to show now, including points recovered since the last update |
Maximum | long | The maximum |
UntilNextPoint | TimeSpan? | The countdown to the next point. null when full, above the maximum, or not recovering |
IsFull | bool | No point is recovering; hide the countdown |
UntilFull | TimeSpan? | How long until stamina is full; null when full or not recovering |
FullAtUtc | DateTimeOffset? | When stamina is full, in UTC; convert to local time to show it |
Purchased stamina can go above Maximum; then nothing recovers and
UntilNextPoint is null.
UntilFull and FullAtUtc help the player choose when to come back without
wasting recovery:
if (reading.UntilFull is TimeSpan untilFull && reading.FullAtUtc is DateTimeOffset fullAt)
{
// For example "Full in 3h 25m (21:30)".
fullText.text = $"Full in {(int)untilFull.TotalHours}h {untilFull.Minutes:00}m ({fullAt.ToLocalTime():HH:mm})";
}
A purchase, a mission charge or a higher maximum arrives as a new snapshot
with a new StaminaRecovery; store it and keep calling Read.
Rules​
- Subscribe once when the top bar is shown and dispose the subscription when it is hidden.
- Show stamina from
StaminaRecovery.Read, not fromStamina. - Never add or subtract balances or stamina yourself.
Common mistakes​
- Showing
Staminaas the live value. It is the last server value and does not count recovery. - Counting recovery yourself.
Readdoes it from the same server clock as the countdown. - Keeping the old
StaminaRecoveryafter the bar is hidden. Clear it inOnDisable, as the quick start does. - Matching slots by currency name in code. Use the catalog asset and
ProjectVHeaderViewKeys. - Touching Unity objects in the listener without switching threads.
UI binding helper​
header.SubscribeUiAsync(ProjectVUiBindingId.Header, apply, token) works
like SubscribeAsync and also marks in the SDK log when apply returned (see
UI subscriptions). It does not switch
threads; switch to the main thread yourself.