Dữ liệu gameplay
Scene nhiệm vụ chạy lượt chơi; server quyết định phần thưởng và sao khi lượt chơi kết thúc. Scene nhận dữ liệu khởi đầu của lượt chơi, báo lại những gì xảy ra (kỹ năng đã dùng, kẻ địch có thưởng đã bị hạ, kết quả, máu cuối cùng), và chỉ hiện phần thưởng mà server trả về.
| Bạn dùng | Nó cho bạn |
|---|---|
IProjectVGameplayDataConsumer | Bạn implement nó một lần cho mỗi scene nhiệm vụ; SDK gọi nó cùng lượt chơi |
IProjectVGameplayDataPresenter | Lượt chơi: ghi lại lượt dùng kỹ năng, kết thúc lượt chơi, chơi lại |
IProjectVGameplayDataPolicy | Các hàm hỗ trợ trên cùng object: kiểm tra asset kỹ năng, theo dõi kẻ địch có thưởng, dựng kết quả giúp bạn |
ProjectVGameplayStartingDataView | Những gì cần spawn: nhân vật, chỉ số, kỹ năng, đồng minh (ally), kẻ địch có thưởng |
ProjectVGameplayDataResult | Kết quả của một lần gọi: phần thưởng và sao, hoặc lỗi |
Ở đây không có screen contract. Lượt chơi bắt đầu trên màn
Campaign bằng BeginMissionAsync; sau đó SDK tìm consumer của
bạn trong scene đã tải.
Bắt đầu nhanh
using System.Collections.Generic;
using System.Threading;
using SilverTiger.ProjectV.Unity.Presentation;
using SilverTiger.ProjectV.Unity.Presentation.Character;
using SilverTiger.ProjectV.Unity.Presentation.Gameplay;
using UnityEngine;
public sealed class MissionDataBinding : MonoBehaviour, IProjectVGameplayDataConsumer
{
private IProjectVGameplayDataPolicy? run;
private ProjectVGameplayStartingDataView? data;
private readonly Dictionary<Component, ProjectVGameplayDropRegistration> drops = new();
public void BindGameplayData(IProjectVGameplayDataPresenter presenter, ProjectVGameplayStartingDataView startingData)
{
run = presenter as IProjectVGameplayDataPolicy;
data = startingData;
if (run == null || !SkillAssetsMatch(run))
{
_ = presenter.CancelRunAsync(CancellationToken.None); // costs no stamina
ShowError("This mission cannot start.");
return;
}
SpawnCharacter(startingData.ActiveCharacterId, startingData.Stats);
SpawnAllies(startingData.Allies);
PlaceRewardEnemies(startingData.Drops); // put each SlotId on its enemy
}
public void UnbindGameplayData(IProjectVGameplayDataPresenter presenter)
{
if (ReferenceEquals(run, presenter))
{
run = null;
data = null;
drops.Clear();
}
}
// A reward enemy became active; spawnCount changes on every reuse from the pool.
public void OnRewardEnemyActivated(Component enemy, string slotId, long spawnCount)
{
if (run != null && run.RegisterDropOwner(enemy, slotId, spawnCount, out var registration).IsSuccess)
{
drops[enemy] = registration;
}
}
public void OnRewardEnemyDied(Component enemy)
{
if (run != null && drops.TryGetValue(enemy, out var registration))
{
run.RecordDropDefeated(registration);
}
}
public void OnRewardEnemyDeactivated(Component enemy)
{
if (run != null && drops.Remove(enemy, out var registration))
{
run.UnregisterDropOwner(registration); // a recorded death still counts
}
}
public void OnSkillUsed(ProjectVGameplayDataSkillSlot slot)
{
if (run == null || data == null)
{
return;
}
foreach (var cast in data.CastSkills)
{
if (cast.Slot == slot)
{
run.RecordSkillActivated(cast.SkillId, slot);
}
}
}
// Win, lose or give up. A Retry button on the result screen calls this again.
public async void OnRunEnded(ProjectVGameplayDataOutcome outcome, long kills, float health, int armor, long experience)
{
if (run == null || !run.OwnsAuthoritativeRun)
{
return;
}
run.ObserveOutcome(outcome, kills);
var result = await run.CompleteObservedAsync(health, armor, experience, CancellationToken.None);
if (this == null)
{
return;
}
if (!result.IsSuccess)
{
ShowRetry(result.SafeMessage); // keep the result screen open
return;
}
ShowRewards(result.Rewards, result.MailedRewards, result.Stars);
}
// Pause > Restart: ends the run without a result and without spending stamina.
public async void OnRestartClicked()
{
if (run == null)
{
return;
}
var result = await run.CancelRunAsync(CancellationToken.None);
if (this != null && result.IsSuccess)
{
ReturnToCampaign();
}
}
private bool SkillAssetsMatch(IProjectVGameplayDataPolicy policy)
{
if (!policy.ValidateSkillCatalog(ReadSkillAssetIds()).IsSuccess)
{
return false;
}
foreach (var slot in new[] { ProjectVGameplayDataSkillSlot.Character, ProjectVGameplayDataSkillSlot.Weapon })
{
var (level, skillId) = PickSkillAsset(slot);
if (!policy.ValidateCastMapping(slot, level, skillId).IsSuccess)
{
return false;
}
}
return true;
}
private IReadOnlyList<ProjectVGameplaySkillId> ReadSkillAssetIds() => new List<ProjectVGameplaySkillId>();
private (int, ProjectVGameplaySkillId) PickSkillAsset(ProjectVGameplayDataSkillSlot slot) => default;
private void SpawnCharacter(string characterId, ProjectVCharacterStats stats) { }
private void SpawnAllies(IReadOnlyList<ProjectVGameplayAllyView> allies) { }
private void PlaceRewardEnemies(IReadOnlyList<ProjectVGameplayDropView> rewardEnemies) { }
private void ShowError(string message) { }
private void ShowRetry(string message) { }
private void ShowRewards(IReadOnlyList<ProjectVGameplayRewardView> rewards,
IReadOnlyList<ProjectVGameplayRewardView> mailed, ProjectVGameplayStarResultView? stars) { }
private void ReturnToCampaign() { }
}
Đó là toàn bộ phần binding: spawn từ dữ liệu khởi đầu, báo lại những gì xảy ra, kết thúc lượt chơi, hiện những gì server trả về. Bạn không lọc kẻ địch có thưởng, không tự tính luật của stage hay dựng số liệu thống kê; SDK làm việc đó từ những gì bạn báo.
Vòng đời
Campaign: BeginMissionAsync(missionId) succeeds -> your game loads the gameplay scene
SDK finds the one IProjectVGameplayDataConsumer in that scene
SDK -> consumer.BindGameplayData(presenter, startingData) (Unity main thread)
during play: RegisterDropOwner / RecordDropDefeated / RecordSkillActivated
end of run: ObserveOutcome, then CompleteObservedAsync (win, lose or give up)
or: CancelRunAsync (Pause > Restart)
SDK -> consumer.UnbindGameplayData(presenter) (scene unloads or run ends)
Những gì bạn nhận được
BindGameplayData nhận một
ProjectVGameplayStartingDataView.
Mọi thứ trong đó được chốt khi lượt chơi bắt đầu; thay đổi trên màn Character
sau đó không ảnh hưởng tới lượt chơi.
| Member | Dùng để |
|---|---|
ActiveCharacterId | Nhân vật cần spawn. Spawn nhân vật này, không phải nhân vật chọn ở local |
ActiveCharacterLevel | Cấp của nhân vật đó, từ 1 trở lên |
Stats | Mọi chỉ số cho lượt chơi này: máu, tấn công, giáp, năng lượng, hồi phục, tốc độ di chuyển và tốc độ đánh, tầm đánh. Áp dụng tất cả thay cho giá trị trong asset local |
MaximumHealth, MaximumArmor | Giá trị tối đa lúc bắt đầu (cùng máu và giáp với Stats) |
SkillCatalogIds | ID cây kỹ năng của nhân vật, để khớp với asset kỹ năng của bạn |
Skills | Bậc cây kỹ năng cho lượt chơi này (SkillId, Rank) |
CastSkills | Kỹ năng Weapon và Character: Slot, Level (từ 1 trở lên) và đúng SkillId |
Allies | Đồng đội tham gia lượt chơi: CharacterId, Level, Experience, Stats, CastSkills. Rỗng khi không có ai |
Drops | Kẻ địch có thưởng: SlotId, Stage, Rewards. Gắn mỗi SlotId lên kẻ địch tương ứng trong scene; hiện Rewards làm vật rơi của nó |
Bind lượt chơi
void BindGameplayData(IProjectVGameplayDataPresenter presenter, ProjectVGameplayStartingDataView startingData)
| Tham số | Kiểu | Giá trị |
|---|---|---|
presenter | IProjectVGameplayDataPresenter | Lượt chơi. Giữ lại; cast sang IProjectVGameplayDataPolicy để dùng các hàm hỗ trợ |
startingData | ProjectVGameplayStartingDataView | Những gì cần spawn (ở trên) |
SDK gọi nó một lần cho mỗi lượt chơi, trên Unity main thread, khi scene đã tải xong. Thiết lập nhân vật, kỹ năng và kẻ địch có thưởng ở đây.
| Tình huống | Điều gì xảy ra |
|---|---|
BindGameplayData ném exception | Lượt chơi bị huỷ và UnbindGameplayData được gọi |
| Không component nào trong scene implement consumer | Không có gì được bind và một lỗi được log: "An admitted gameplay scene must contain exactly one data binding consumer." |
| Nhiều component implement nó | Không có gì được bind và một lỗi được log: "An admitted gameplay scene contains multiple data binding consumers." |
Component trên object inactive cũng được tính. Chỉ giữ đúng một.
Unbind lượt chơi
void UnbindGameplayData(IProjectVGameplayDataPresenter presenter)
Được gọi khi scene unload hoặc lượt chơi kết thúc. Ngừng dùng presenter: từ lúc này mọi lần gọi trên nó đều thất bại với "The gameplay data binding no longer belongs to the active run."
Kiểm tra asset kỹ năng
ProjectVGameplayDataResult ValidateSkillCatalog(IReadOnlyList<ProjectVGameplaySkillId> skillIds)
ProjectVGameplayDataResult ValidateCastMapping(ProjectVGameplayDataSkillSlot slot, int level, ProjectVGameplaySkillId skillId)
| Tham số | Giá trị |
|---|---|
skillIds | Các skill ID đọc từ asset kỹ năng của bạn; mỗi ID một lần, mỗi ID đều nằm trong SkillCatalogIds |
slot, level, skillId | Slot, cùng cấp và skill ID của asset bạn chọn cho slot đó |
Gọi cả hai trước khi gameplay bắt đầu, gọi ValidateCastMapping cho cả hai
slot. Nếu một trong hai thất bại, đừng bắt đầu lượt chơi. ValidateSkillCatalog
ném ArgumentNullException khi list là null.
| Tình huống | SafeMessage |
|---|---|
| Lượt chơi thiếu kỹ năng Weapon hoặc Character | "The admitted character must freeze both active cast skills." |
Một ID bị trùng hoặc không có trong SkillCatalogIds | "Authored skills must map uniquely to the admitted catalog." |
| List rỗng | "The authored gameplay skill catalog is empty." |
| Cấp hoặc skill ID của asset không khớp với slot | "The authored cast level and UUID must match the admitted slot." |
Theo dõi kẻ địch có thưởng
ProjectVGameplayDataResult RegisterDropOwner(object owner, string slotId, long activationLease, out ProjectVGameplayDropRegistration registration)
ProjectVGameplayDataResult RecordDropDefeated(ProjectVGameplayDropRegistration registration)
ProjectVGameplayDataResult UnregisterDropOwner(ProjectVGameplayDropRegistration registration)
| Tham số | Giá trị |
|---|---|
owner | Object kẻ địch, ví dụ component của nó; lần nào cũng là cùng một object |
slotId | SlotId bạn đã gắn lên kẻ địch, lấy từ Drops |
activationLease | Một số thay đổi mỗi lần kẻ địch này được kích hoạt, ví dụ bộ đếm spawn |
registration | Được gán khi thành công; giữ lại cho hai lần gọi còn lại |
Đăng ký kẻ địch có thưởng mỗi lần nó được kích hoạt (đăng ký lại với
activationLease mới khi một kẻ địch trong pool được dùng lại), ghi lại lúc nó
chết, và huỷ đăng ký khi nó bị deactivate. RegisterDropOwner ném
ArgumentNullException khi owner là null.
| Tình huống | Điều gì xảy ra |
|---|---|
slotId không xác định | RegisterDropOwner thất bại (code PROJECTV_GAMEPLAY_DROP_SLOT_UNKNOWN) |
| Hai kẻ địch dùng chung một slot, hoặc một kẻ địch đăng ký hai slot | Thất bại (code PROJECTV_GAMEPLAY_DROP_SLOT_OWNER_DUPLICATE), và các slot đó bị loại khỏi kết quả, kể cả khi đã ghi nhận lần chết |
| Registration từ một lần kích hoạt cũ hơn | RecordDropDefeated và UnregisterDropOwner thất bại (code PROJECTV_GAMEPLAY_DROP_LEASE_STALE) |
| Lần chết đã được ghi nhận rồi | RecordDropDefeated thất bại (code PROJECTV_GAMEPLAY_DROP_ALREADY_DEFEATED) |
| Kẻ địch bị deactivate sau khi đã chết | UnregisterDropOwner không huỷ lần chết đã ghi; nó vẫn được tính |
| Kết quả đã được gửi | RegisterDropOwner và RecordDropDefeated thất bại: "The gameplay completion is already fixed." |
Các lỗi này không cần thông báo cho người chơi; log chúng ra trong lúc bạn dựng scene.
Ghi lại lượt dùng kỹ năng
ProjectVGameplayDataResult RecordSkillActivated(ProjectVGameplaySkillId skillId, ProjectVGameplayDataSkillSlot slot)
| Tham số | Giá trị |
|---|---|
skillId | SkillId của slot tương ứng trong CastSkills |
slot | Character hoặc Weapon |
Gọi nó mỗi lần người chơi dùng kỹ năng. Nó ném
ArgumentOutOfRangeException khi slot không xác định.
| Tình huống | SafeMessage |
|---|---|
Kỹ năng không khớp với slot trong CastSkills | "This skill is not admitted for this slot." |
ObserveOutcome đã được gọi | "The observed run outcome no longer accepts skill activations." |
Báo kết quả
ProjectVGameplayDataResult ObserveOutcome(ProjectVGameplayDataOutcome outcome, long? enemiesDefeated)
| Tham số | Giá trị |
|---|---|
outcome | Succeeded (thắng), Failed (thua) hoặc Abandoned (bỏ cuộc) |
enemiesDefeated | Số kẻ địch đã hạ, từ 0 đến 1024; null khi bạn không đếm. Khi đó các mục tiêu sao cần số kẻ địch đã hạ sẽ thất bại |
Gọi nó khi lượt chơi kết thúc, trước CompleteObservedAsync. Nó khoá lượt
chơi: từ đó trở đi RecordSkillActivated và CancelRunAsync bị từ chối.
| Tình huống | SafeMessage |
|---|---|
| Kết quả không xác định, hoặc số kẻ địch đã hạ nằm ngoài 0–1024 | "The gameplay outcome or kill count is invalid." |
| Kết quả đã được gửi với outcome khác | "The gameplay completion is already fixed." (gửi lại cùng outcome thì thành công) |
Gửi phần thưởng đã nhặt
ProjectVGameplayDataResult SetClientRewards(IReadOnlyList<ProjectVGameplayClientRewardView> rewards)
Chỉ dùng cho trust client rewards. Gọi nó trước
CompleteObservedAsync; lần gọi sau thay thế list cũ. Nó thất bại với "The
gameplay completion is already fixed." khi kết quả đã được gửi, và ném
ArgumentNullException khi list là null hoặc ArgumentException khi có phần tử
null.
Kết thúc lượt chơi
Task<ProjectVGameplayDataResult> CompleteObservedAsync(float currentHealth, int currentArmor, long rawSessionExperience, CancellationToken cancellationToken)
| Tham số | Giá trị |
|---|---|
currentHealth | Máu lúc kết thúc. Được làm tròn và giữ trong khoảng từ 0 đến giá trị tối đa lúc bắt đầu; phải là số hữu hạn |
currentArmor | Giáp lúc kết thúc; giữ trong khoảng từ 0 đến giá trị tối đa lúc bắt đầu |
rawSessionExperience | EXP kiếm được trong lượt chơi trước mọi bonus; từ 0 trở lên |
cancellationToken | Dừng chờ server |
SDK dựng kết quả từ những gì bạn đã báo rồi gửi đi:
- Khi thắng, không gửi kẻ địch có thưởng nào đã bị hạ; server trao mọi phần thưởng của các kẻ địch có thư ởng trong lượt chơi.
- Khi thua hoặc bỏ cuộc, các kẻ địch có thưởng đã bị hạ được gửi đi, nhưng
chỉ tới
Stageđầu tiên còn kẻ địch có thưởng chưa bị hạ. - Các slot dùng chung (ở trên) bị loại ra.
Lần gọi đầu tiên chốt kết quả. Thử lại sau khi thất bại sẽ gửi lại đúng kết quả đó và bỏ qua giá trị mới.
Trả về một ProjectVGameplayDataResult:
IsSuccess | Hiện gì |
|---|---|
true | Rewards (những gì server đã trao), MailedRewards (phần được gửi vào hộp thư vì không đủ chỗ) và Stars (null khi thua, bỏ cuộc hoặc nhiệm vụ không có sao) |
false | SafeMessage, kèm nút thử lại (Retry). Giữ màn kết quả mở |
Khi thất bại:
| Tình huống | SafeMessage |
|---|---|
Chưa gọi ObserveOutcome, máu không phải số hữu hạn, hoặc EXP âm | "Observe a valid outcome and gameplay sample before completion." |
| Các giá trị không tạo thành kết quả hợp lệ | "The admitted gameplay sample is invalid." |
| Presenter đã bị unbind | "The gameplay data binding no longer belongs to the active run." |
| Server từ chối, hoặc không có kết nối | Thông báo của server hoặc của mạng. Thử lại bằng cùng lần gọi đó |
Chỉ hiện phần thưởng mà kết quả trả về. Giữ dữ liệu lượt chơi local cho tới khi
IsSettlementConfirmed là true.
TryGetHealthPercent(currentHealth, out percent) cho máu dưới dạng phần trăm của
giá trị tối đa lúc bắt đầu (0 đến 100) để xem trước kết quả, tính giống hệt cách
dựng kết quả; nó trả về false khi máu không phải số hữu hạn.
Chơi lại
Task<ProjectVGameplayDataResult> CancelRunAsync(CancellationToken cancellationToken)
Dành cho Pause > Restart: kết thúc lượt chơi mà không có kết quả và không tốn stamina (thể lực). Bắt đầu lại nhiệm vụ từ Campaign.
| Tình huống | SafeMessage |
|---|---|
ObserveOutcome đã được gọi | "The observed run outcome must be settled, not cancelled." Hãy kết thúc lượt chơi bằng CompleteObservedAsync |
| Presenter đã bị unbind | "The gameplay data binding no longer belongs to the active run." |
Bỏ cuộc không phải là chơi lại: báo nó là Abandoned và kết thúc lượt chơi,
trước khi rời scene.
Quy tắc
- Stamina bị trừ khi lượt chơi kết thúc: thắng, thua hoặc bỏ cuộc. Chơi lại, lượt chơi được giải phóng từ Campaign, hoặc app bị tắt hay bị crash thì không tốn gì.
- Sao do server chấm cho lượt chơi thắng. Hiện sao từ
result.Stars. - Vật phẩm nhặt được và cấp chiến đấu tạm thời không thay đổi kết quả, trừ khi bật trust client rewards.
- Nếu kết quả thất bại hoặc không rõ, giữ màn kết quả mở với nút thử lại (Retry) và gọi lại đúng phương thức đó.
Trust client rewards
Khi bật ProjectVSettings.TrustClientRewards, game tự gửi phần thưởng mà người
chơi đã nhặt. Dựng list các vật phẩm nhặt được và vật phẩm EXP thành các phần tử
ProjectVGameplayClientRewardView(itemId, amount) (ID không rỗng, số lượng
dương) và gọi SetClientRewards(list) trước CompleteObservedAsync. Với
CompleteAsync, truyền cùng list đó vào
ProjectVGameplayCompletionView.ClientRewards. List rỗng nghĩa là "không có
phần thưởng"; bỏ trống nghĩa là "không dùng trust mode". Tự kiểm tra setting
trước khi gửi list.
Server chỉ dùng list khi trust mode cũng được bật ở phía server
(PROJECTV_MISSION_TRUST_CLIENT). Khi đó server trao đúng list đó, bỏ qua ID
không xác định, và không cộng thêm vật rơi, EXP hay phần thưởng thắng nào khác;
mở khoá nhiệm vụ và sao vẫn hoạt động như bình thường. Nếu trust mode tắt ở một
trong hai phía, server tự tính phần thưởng.
Lỗi thường gặp
- Có nhiều hơn một consumer trong scene, kể cả trên object inactive. Không có gì được bind.
- Spawn nhân vật chọn ở local hoặc dùng chỉ số local. Dùng
ActiveCharacterIdvàStats. - Bắt đầu lượt chơi khi kiểm tra kỹ năng thất bại. Kết quả sẽ không khớp với lượt chơi; hãy huỷ nó.
- Dùng lại kẻ địch trong pool mà không có
activationLeasemới. Khi đó một callback muộn từ lần kích hoạt trước bị coi là của lần mới. - Gắn một slot ID lên hai kẻ địch. Slot đó bị loại khỏi kết quả.
- Ghi lại lượt dùng kỹ năng sau
ObserveOutcome. Chúng bị từ chối; báo kết quả sau cùng. - Gửi giá trị mới khi thử lại (Retry).
CompleteObservedAsyncđầu tiên đã chốt kết quả; chỉ cần gọi lại. - Hiện vật rơi local thay vì
result.Rewards. Chỉ kết quả mới là thứ người chơi thực sự nhận được. - Dùng presenter sau
UnbindGameplayData. Mọi lần gọi đều thất bại.
Presenter
IProjectVGameplayDataPresenter
chính là lượt chơi; IProjectVGameplayDataPolicy thêm các hàm hỗ trợ ở trên
trên cùng object.
| Member | Dùng để |
|---|---|
OwnsAuthoritativeRun | True khi presenter này thuộc về một lượt chơi đã bắt đầu và chưa bị huỷ. Kiểm tra trước khi kết thúc lượt chơi |
IsSettlementConfirmed | True khi server đã xác nhận lượt chơi kết thúc |
RecordSkillActivated(skillId, slot) | Ghi lại lượt dùng kỹ năng |
CancelRunAsync(token) | Chơi lại |
CompleteAsync(completion, token) | Kết thúc lượt chơi bằng kết quả do bạn tự dựng (bên dưới) |
Kết thúc lượt chơi bằng kết quả của bạn
Task<ProjectVGameplayDataResult> CompleteAsync(ProjectVGameplayCompletionView input, CancellationToken cancellationToken)
Chỉ dùng khi bạn không dùng các hàm hỗ trợ của policy. Dựng một
ProjectVGameplayCompletionView:
| Member | Giá trị |
|---|---|
Outcome | Succeeded, Failed hoặc Abandoned |
RawSessionExperience | EXP kiếm được trước mọi bonus; từ 0 trở lên |
DefeatedDropIds | Slot ID của các kẻ địch có thưởng đã bị hạ; rỗng khi thắng |
Telemetry | Một ProjectVGameplayTelemetryView: Health, StartingMaximumHealth, Armor, StartingMaximumArmor, DamageTaken, ShieldActivations, ShieldAbsorptions, tất cả từ 0 trở lên |
EnemiesDefeated | Số kẻ địch đã hạ, từ 0 đến 1024; null khi không đếm |
ClientRewards | Phần thưởng đã nhặt cho trust client rewards; null trong trường hợp khác |
Nó ném ArgumentNullException khi input là null và ArgumentException khi có
giá trị sai, ví dụ telemetry âm hoặc outcome không xác định. Sau khi thành công,
gọi lại sẽ trả về cùng kết quả; sau khi thất bại, gọi lại với cùng input để thử
lại. Kết quả giống như với CompleteObservedAsync.