Chuyển tới nội dung chính

Header

Thanh trên cùng ở mọi scene đã đăng nhập hiện số dư tiền tệ và stamina (thể lực) của người chơi. Nó tự cập nhật mỗi khi server xác nhận một thay đổi (một lượt mua, một nhiệm vụ, một lần nâng cấp, một lần cấp phát hằng ngày); header không có command (lệnh) nào.

Bạn dùngNó cho bạn
IProjectVHeaderPresenterTheo dõi header
ProjectVHeaderSnapshotNhững gì cần vẽ: số dư và stamina
ProjectVHeaderCurrencyValueMột số dư và ô hiển thị của nó
ProjectVStaminaRecoveryStamina trực tiếp và đồng hồ đếm ngược
ProjectVStaminaReadingStamina tại một thời điểm, cùng thời gian chờ đến điểm tiếp theo và đến khi đầy
ProjectVHeaderCurrencyCatalogAssetMỗi ô của header hiện tiền tệ nào

Header không có screen contract; thanh trên cùng đăng ký nhận (subscribe) trực tiếp từ presenter.

Bắt đầu nhanh​

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");
}
}
}

Đó là toàn bộ thanh trên cùng: subscribe, lưu lại, đọc stamina mỗi frame. Không bao giờ tự cộng một lượt mua hay phần thưởng nhiệm vụ vào số dư; snapshot tiếp theo sẽ mang giá trị đã được xác nhận.

Main thread

Listener của SubscribeAsync có thể chạy ngoài main thread của Unity. Hãy chuyển về main thread trước khi đụng vào object của Unity, như RenderAsync.

Cần vẽ gì​

Listener nhận một ProjectVHeaderSnapshot:

MemberTypeDùng để
CurrenciesIReadOnlyList<ProjectVHeaderCurrencyValue>Mỗi ô header một số dư, theo thứ tự trong catalog. Rỗng cho đến khi HasEconomy
StaminaRecoveryProjectVStaminaRecovery?Stamina trực tiếp và đồng hồ đếm ngược; null cho đến khi biết stamina
Staminalong?Giá trị thô mà server báo lần gần nhất. Không phải giá trị trực tiếp: dùng StaminaRecovery.Read(DateTimeOffset.UtcNow) để hiện stamina
HasEconomyboolSố dư đã tải xong
HasStaminaboolStamina đã tải xong
IsReadyboolCả hai đã tải xong; có thể hiện toàn bộ header
RevisionlongTăng sau mỗi lần cập nhật. UI không cần dùng

header.Current chứa snapshot mới nhất; kiểm tra IsReady trước khi hiện nó.

Với mỗi ProjectVHeaderCurrencyValue:

MemberDùng để
ViewKeyÔ để hiện nó. So sánh bằng Equals với ProjectVHeaderViewKeys
CurrencyIdTiền tệ trong ví, ví dụ core_dust
AmountSố dư, không bao giờ âm; 0 khi người chơi không có

Tiền tệ​

Tiền tệ nào nằm ở ô nào được đặt trong một asset, không phải trong code. Một ProjectVHeaderCurrencyCatalogAsset (tạo từ Assets › Create › ProjectV › Backend V3 › Header Currency Catalog) chứa mỗi ô một dòng: một view key và một ID tiền tệ, ví dụ header.core_dust → core_dust. Gán nó vào ProjectVPresentationLifetimeScope của root. Trong ProjectV, asset này là Assets/_Game/BackendV3/Settings/ProjectVHeaderCurrencyCatalog.asset.

Các ô có sẵn nằm trong ProjectVHeaderViewKeys:

KeyÔ
Creditheader.credit
Goldheader.gold
CoreDustheader.core_dust
Diamondheader.diamond

Mỗi view key chỉ được xuất hiện một lần trong catalog; một dòng rỗng, toàn khoảng trắng hoặc trùng lặp sẽ gây lỗi khi scope được build. Không có asset thì Currencies luôn rỗng.

Đếm ngược stamina​

ProjectVStaminaReading Read(DateTimeOffset nowUtc)
Tham sốTypeGiá trị
nowUtcDateTimeOffsetDateTimeOffset.UtcNow

Stamina vẫn tiếp tục hồi giữa các lần server cập nhật. Read tính giá trị tại nowUtc ngay trên máy, không hỏi server, nên có thể gọi nó mỗi frame hoặc mỗi giây.

Trả về một ProjectVStaminaReading:

MemberTypeDùng để
StaminalongStamina cần hiện lúc này, gồm cả các điểm đã hồi từ lần cập nhật trước
MaximumlongGiá trị tối đa
UntilNextPointTimeSpan?Đếm ngược đến điểm tiếp theo. null khi đầy, vượt mức tối đa, hoặc không đang hồi
IsFullboolKhông có điểm nào đang hồi; ẩn đồng hồ đếm ngược
UntilFullTimeSpan?Còn bao lâu nữa thì stamina đầy; null khi đầy hoặc không đang hồi
FullAtUtcDateTimeOffset?Thời điểm stamina đầy, theo UTC; đổi sang giờ địa phương để hiện

Stamina mua thêm có thể vượt Maximum; khi đó không có gì hồi và UntilNextPoint là null.

UntilFull và FullAtUtc giúp người chơi chọn lúc quay lại mà không lãng phí lượng hồi:

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})";
}

Một lượt mua, một lần trừ stamina cho nhiệm vụ hay mức tối đa tăng lên sẽ đến dưới dạng snapshot mới với StaminaRecovery mới; lưu nó lại và tiếp tục gọi Read.

Quy tắc​

  • Subscribe một lần khi thanh trên cùng hiện ra và dispose (giải phóng) subscription khi nó bị ẩn.
  • Hiện stamina từ StaminaRecovery.Read, không phải từ Stamina.
  • Không bao giờ tự cộng hay trừ số dư hoặc stamina.

Lỗi thường gặp​

  • Hiện Stamina như giá trị trực tiếp. Đó là giá trị server gửi lần gần nhất và không tính phần đã hồi.
  • Tự tính phần hồi. Read làm việc này dựa trên cùng đồng hồ server với đồng hồ đếm ngược.
  • Giữ StaminaRecovery cũ sau khi thanh bị ẩn. Xoá nó trong OnDisable, như phần bắt đầu nhanh.
  • So khớp ô theo tên tiền tệ trong code. Dùng asset catalog và ProjectVHeaderViewKeys.
  • Đụng vào object của Unity trong listener mà không chuyển thread.

Helper gắn UI​

header.SubscribeUiAsync(ProjectVUiBindingId.Header, apply, token) hoạt động như SubscribeAsync và còn đánh dấu trong log của SDK khi apply đã chạy xong (xem UI subscription). Nó không chuyển thread; bạn tự chuyển về main thread.