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ùng | Nó cho bạn |
|---|---|
IProjectVHeaderPresenter | Theo dõi header |
ProjectVHeaderSnapshot | Những gì cần vẽ: số dư và stamina |
ProjectVHeaderCurrencyValue | Một số dư và ô hiển thị của nó |
ProjectVStaminaRecovery | Stamina trực tiếp và đồng hồ đếm ngược |
ProjectVStaminaReading | Stamina tại một thời điểm, cùng thời gian chờ đến điểm tiếp theo và đến khi đầy |
ProjectVHeaderCurrencyCatalogAsset | Mỗ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.
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:
| Member | Type | Dùng để |
|---|---|---|
Currencies | IReadOnlyList<ProjectVHeaderCurrencyValue> | Mỗi ô header một số dư, theo thứ tự trong catalog. Rỗng cho đến khi HasEconomy |
StaminaRecovery | ProjectVStaminaRecovery? | Stamina trực tiếp và đồng hồ đếm ngược; null cho đến khi biết stamina |
Stamina | long? | 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 |
HasEconomy | bool | Số dư đã tải xong |
HasStamina | bool | Stamina đã tải xong |
IsReady | bool | Cả hai đã tải xong; có thể hiện toàn bộ header |
Revision | long | Tă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:
| Member | Dùng để |
|---|---|
ViewKey | Ô để hiện nó. So sánh bằng Equals với ProjectVHeaderViewKeys |
CurrencyId | Tiền tệ trong ví, ví dụ core_dust |
Amount | Số 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 | Ô |
|---|---|
Credit | header.credit |
Gold | header.gold |
CoreDust | header.core_dust |
Diamond | header.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ố | Type | Giá trị |
|---|---|---|
nowUtc | DateTimeOffset | DateTimeOffset.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:
| Member | Type | Dùng để |
|---|---|---|
Stamina | long | Stamina 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 |
Maximum | long | Giá trị tối đa |
UntilNextPoint | TimeSpan? | Đế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 |
IsFull | bool | Không có điểm nào đang hồi; ẩn đồng hồ đếm ngược |
UntilFull | TimeSpan? | Còn bao lâu nữa thì stamina đầy; null khi đầy hoặc không đang hồi |
FullAtUtc | DateTimeOffset? | 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
Staminanhư 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.
Readlàm việc này dựa trên cùng đồng hồ server với đồng hồ đếm ngược. - Giữ
StaminaRecoverycũ sau khi thanh bị ẩn. Xoá nó trongOnDisable, 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.