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

Character

Màn Character hiện các nhân vật và đồng minh (ally) của người chơi, đưa họ vào đội hình (loadout), cho họ lên cấp bằng vật phẩm EXP, và nâng cấp cây kỹ năng cùng các kỹ năng Weapon và Character của họ. Cấp, giá, EXP và những gì mỗi lần nâng cấp mang lại đều do server quyết định.

Bạn dùngNó cho bạn
IProjectVCharacterScreenScreen contract: theo dõi view, chọn, lên cấp, nâng cấp và reset
ProjectVCharacterViewNhững gì cần vẽ: trạng thái đang tải, dữ liệu, một lần lên cấp đang chờ, trạng thái nâng cấp và preview EXP
ProjectVCharacterDataNhân vật, đội hình, cây kỹ năng, cast skill, kho đồ và số dư
ProjectVCharacterUpgradeStateNhân vật có lên cấp được ngay không, và nếu không thì vì sao
ProjectVCharacterSkillUpgradeDetailsMọi thứ nút Upgrade của cây kỹ năng cần
ProjectVCharacterCastSkillUpgradeDetailsMọi thứ nút Upgrade của kỹ năng Weapon hoặc Character cần
ProjectVCharacterLevelPolicyBộ chọn EXP "+1" và "Max", và preview reset cấp
ProjectVResultKết quả của một command (lệnh)

Trang bị, ba lô, skin, dùng các vật phẩm khác và hộp thoại xác nhận chưa có trên screen contract; xem Presenter.

Bắt đầu nhanh​

using System;
using System.Collections.Generic;
using SilverTiger.ProjectV.Unity.Presentation.Character;
using UnityEngine;
using VContainer;

public sealed class CharacterPage : MonoBehaviour
{
private IProjectVCharacterScreen character = null!;
private IDisposable? observation;
private string selectedId = string.Empty;

[Inject]
public void Construct(IProjectVCharacterScreen screen) => character = screen;

private void OnEnable() => observation = character.Observe(Render);

private void OnDisable()
{
observation?.Dispose();
observation = null;
}

private void Render(ProjectVCharacterView view)
{
if (view.IsLoading || view.Data == null)
{
ShowLoading(view.LoadError); // LoadError is null while the first load is running
return;
}
if (view.Data.IsEmpty)
{
ShowEmpty();
return;
}
if (selectedId.Length == 0)
{
selectedId = view.Data.Characters[0].Id.Value;
}
ShowRoster(view.Data.Characters, view.Data.Loadout);
ShowUpgrade(view.UpgradeState(selectedId)!, view.PendingUpgrade != null);
}

public async void OnLevelUpClicked(IReadOnlyList<ProjectVCharacterXpItemSelection> items)
{
var result = await character.LevelUpAsync(selectedId, items);
if (this != null && !result.IsSuccess)
{
ShowMessage(result.Message);
}
}

private void ShowLoading(string? error) { }
private void ShowEmpty() { }
private void ShowRoster(IReadOnlyList<ProjectVCharacterEntry> characters, IReadOnlyList<ProjectVCharacterLoadoutSlot> loadout) { }
private void ShowUpgrade(ProjectVCharacterUpgradeState upgrade, bool retryOnly) { }
private void ShowMessage(string message) { }
}

Đó là toàn bộ khuôn mẫu: theo dõi, vẽ (render), gọi command. Sau khi thành công bạn không cần làm gì; Render được gọi lại với cấp, rank và số dư mới, và Header hiện ví mới.

Cần vẽ gì​

Render nhận một ProjectVCharacterView:

MemberTypeDùng để
IsLoadingboolTrue cho đến khi dữ liệu đến lần đầu. Hiện spinner
LoadErrorstring?Có giá trị khi lần tải đầu đó thất bại. Hiện nó kèm nút thử lại (Retry) đóng rồi mở lại trang
DataProjectVCharacterData?Mọi thứ bên dưới; null khi đang tải
PendingUpgradeProjectVCharacterProgressionPending?Một lần lên cấp hoặc reset cấp bị mất câu trả lời. Khi nó có giá trị, hiện nút "Retry"; xem Câu trả lời bị mất
UpgradeState(characterId)ProjectVCharacterUpgradeState?Panel lên cấp; null khi đang tải
PreviewXp(characterId, items)ProjectVCharacterXpUpgradePreview?Lên cấp bằng các vật phẩm EXP này sẽ ra sao; null khi đang tải

Từ ProjectVCharacterData:

Để hiệnĐọc
Danh sách nhân vậtCharacters: Id, Kind (ProjectVCharacterKinds.Character hoặc .Ally), Level, Experience, Stats
Danh sách rỗngIsEmpty
Ai đang trong đội hìnhLoadout: SlotId và CharacterId cho mỗi ô
Cây kỹ năng của một nhân vậtGetSkills(characterId), hoặc một node bằng GetSkill(characterId, skillId)
Điểm kỹ năng theo cấp chưa dùngLevelSkillPoints trong mục của nhân vật đó ở SkillTrees
Kỹ năng Weapon và CharacterGetCastSkills(characterId)
Bộ chọn vật phẩm EXPXpItems (ItemId, Experience, CharacterKind, SortOrder) với số lượng sở hữu lấy từ Inventory
Một số dư hoặc số lượng vật phẩmGetResourceAmount(resourceId), hoặc Balances và Inventory

Stats có MaxHealth, AttackDamage, Armor, Energy, RegenHealthDelay, RegenArmorDelay, RegenArmorInterval, RegenArmorAmount, MoveSpeed, AttackSpeed và RangeTarget.

Ghép nhân vật với art và text bằng Id.Value trong một bảng của riêng bạn (ProjectV dùng SOCharacter.V3CharacterId), không bao giờ theo tên hiển thị. Dùng skill ID nguyên vẹn; chúng gồm 32 ký tự hexa viết thường, ví dụ ad4b85fb7aa64ad196e1f7e0f330b0aa.

Chọn nhân vật​

Task<ProjectVResult> SelectCharacterAsync(string slotId, string characterId)
Tham sốTypeGiá trị
slotIdstringProjectVLoadoutSlots.Main.Value hoặc ProjectVLoadoutSlots.ActiveAlly.Value, hoặc SlotId.Value từ Loadout
characterIdstringId.Value của một nhân vật đang sở hữu

Trả về một ProjectVResult:

IsSuccessMessageĐiều gì đã xảy ra
truerỗngServer đã xác nhận thay đổi. Trang tự vẽ lại
falselý do, sẵn sàng để hiệnÔ không đổi, hoặc không xác nhận được thay đổi

Khi thất bại — hiện Message nguyên văn:

Tình huốngNgười chơi thấy gì
Server từ chối thay đổiThông báo của server
slotId hoặc characterId rỗng"The request was not confirmed. Please try again."
Mất kết nối hoặc timeoutMột thông báo lỗi mạng. Trang vẽ lại theo dữ liệu server đang có

Lên cấp bằng vật phẩm EXP​

Task<ProjectVResult> LevelUpAsync(string characterId, IReadOnlyList<ProjectVCharacterXpItemSelection> items)
Tham sốTypeGiá trị
characterIdstringId.Value của một nhân vật hoặc đồng minh đang sở hữu
itemsIReadOnlyList<ProjectVCharacterXpItemSelection>Các dòng new ProjectVCharacterXpItemSelection(itemId, quantity): tối đa 16 dòng, mỗi vật phẩm một lần, mỗi số lượng từ 1 đến 9999. Có thể rỗng để lên cấp bằng EXP nhân vật đã có

Một lần bấm, một lần lên cấp. Server tiêu các vật phẩm, cộng EXP của chúng và lấy mọi cấp mà EXP và ví đủ chi trả. Nó dừng ở cấp tối đa, khi hết EXP, hoặc khi cấp tiếp theo đắt hơn số người chơi có; EXP còn dư được giữ lại cho sau. Tất cả xảy ra cùng lúc hoặc không có gì xảy ra. EXP chưa đủ lên cấp tiếp theo vẫn được nhận và giữ lại.

Trả về một ProjectVResult:

IsSuccessMessageĐiều gì đã xảy ra
truerỗngServer đã xác nhận. Trang vẽ lại với cấp và EXP mới; so sánh Level trước và sau để hiện "+3 levels"
falselý do, sẵn sàng để hiệnKhông có gì bị tiêu, hoặc câu trả lời bị mất (xem bên dưới)

Throw ngay lập tức, trước khi gửi bất cứ gì, khi characterId rỗng hoặc toàn khoảng trắng (ArgumentException), items là null (ArgumentNullException), hoặc items có một dòng null, nhiều hơn 16 dòng hoặc cùng một vật phẩm hai lần (ArgumentException).

Khi thất bại — hiện Message nguyên văn:

Tình huốngNgười chơi thấy gì
Không có vật phẩm và không đủ EXP"Select enough EXP for the next level."
Không có vật phẩm và không đủ tiền tệ"Not enough currency for the next level."
Nhân vật đã ở cấp tối đa"Maximum level reached."
Một vật phẩm không sở hữu, không đủ số lượng, hoặc dành cho loại kia (nhân vật hoặc đồng minh)"The selected EXP items are unavailable for this character kind."
Server từ chốiThông báo của server
Mất kết nối hoặc timeoutMột thông báo lỗi mạng. PendingUpgrade có giá trị; xem Câu trả lời bị mất

Khi các vật phẩm đã chọn cộng thêm EXP nhưng không đạt cấp tiếp theo, hoặc ví không đủ trả cho nó, lệnh gọi vẫn thành công: EXP được cộng và giữ lại.

Trước khi bấm​

var preview = view.PreviewXp(characterId, items);
levelUpButton.interactable = preview is { CanSubmit: true };
previewText.text = preview?.Message ?? string.Empty;

ProjectVCharacterXpUpgradePreview cho GrantedExperience, ProjectedExperience, CurrentLevel, TargetLevel, AvailableLevelCount (số cấp tăng thêm), các giá trị thanh EXP và CurrentStats / TargetStats. TargetLevel là cấp mà EXP đạt tới; ví có thể khiến lần lên cấp dừng sớm hơn.

Với nút "+1" và "Max", để SDK chọn vật phẩm:

var nextLevel = ProjectVCharacterLevelPolicy.SelectForNextLevel(view.Data!, entry.Id);
var maximum = ProjectVCharacterLevelPolicy.SelectMaximum(view.Data!, entry.Id);

Cả hai trả về các vật phẩm đang sở hữu theo thứ tự vật phẩm của server, và một danh sách rỗng khi không có gì để chọn. SelectForNextLevel có thể thiếu khi người chơi sở hữu quá ít.

Panel nâng cấp​

var upgrade = view.UpgradeState(characterId)!;
upgradeButton.interactable = upgrade.CanUpgrade;
blockReasonText.text = upgrade.CanUpgrade ? string.Empty : upgrade.Message;

if (upgrade.Requirements is { } next)
{
xpText.text = $"{upgrade.Experience}/{next.RequiredExperience}";
RenderCosts(upgrade.Balances); // each cost line next to the wallet balance
}
Để hiệnĐọc
Có thể Level Up khôngCanUpgrade, và Message khi không thể (ví dụ "Not enough Character XP: 40/100.")
Lý do, dưới dạng giá trịBlockReason: None, NotOwned, Unavailable, Busy, SyncRequired, MaxLevel, InsufficientExperience, InsufficientFunds
Thanh EXPExperience / Requirements.RequiredExperience (cả hai là tổng EXP tính từ cấp 1)
GiáBalances, hoặc Requirements.Costs (có thể rỗng)
Trước và sauRequirements.NextLevel và Requirements.NextStats

UpgradeState mô tả việc lên cấp tiếp theo bằng EXP nhân vật đã có. Khi đã chọn vật phẩm EXP, hãy dùng PreviewXp.

Câu trả lời bị mất​

Nếu mất kết nối giữa lúc lên cấp hoặc reset cấp, PendingUpgrade giữ nhân vật và các vật phẩm. Lần LevelUpAsync tiếp theo sẽ hoàn tất hành động đó trước và bỏ qua characterId và items bạn truyền vào; vật phẩm không bao giờ bị tiêu hai lần. Việc này vẫn đúng sau khi app khởi động lại.

Khi PendingUpgrade có giá trị, PreviewXp trả về CanSubmit = false với "Resume the pending Character progression first.", và reset cấp bị từ chối với cùng thông báo đó. Hiện một nút "Retry" duy nhất gọi LevelUpAsync với CharacterId đang chờ và một danh sách rỗng.

Nâng cấp kỹ năng​

Task<ProjectVResult> UpgradeSkillAsync(string characterId, string skillId)
Tham sốTypeGiá trị
characterIdstringId.Value của nhân vật sở hữu cây kỹ năng
skillIdstringId.Value của một kỹ năng trong cây của nhân vật đó

Mỗi nhân vật có cây riêng. Cùng một skill ID có thể có rank và cấp mở khoá khác nhau trong cây của các nhân vật khác nhau.

Trả về một ProjectVResult:

IsSuccessMessageĐiều gì đã xảy ra
truerỗngServer đã xác nhận rank. Cây và số dư tự vẽ lại
falselý do, sẵn sàng để hiệnKhông mua được rank nào

Khi thất bại — hiện Message nguyên văn:

Tình huốngNgười chơi thấy gì
Chưa đạt cấp, điều kiện tiên quyết hoặc giáThông báo của server
characterId hoặc skillId rỗng hoặc sai định dạng"The request was not confirmed. Please try again."
Mất kết nối hoặc timeoutMột thông báo lỗi mạng. Trang tự đồng bộ lại

Nút Upgrade kỹ năng​

Dùng view.Data.GetSkillUpgradeDetails(characterId, skillId) (ProjectVCharacterSkillUpgradeDetails):

Để hiệnĐọc
Bật UpgradeCanUpgrade
Vì sao khôngBlockedMessage, mọi điều kiện còn thiếu trên một dòng, ví dụ Require Lv.20 · Core Dust 28/45, hoặc "Maximum rank reached."
RankSkill.Rank / Skill.MaxRank, IsMaxed
Cấp cần cóRequiredCharacterLevel so với CharacterLevel
GiáCosts: mỗi tài nguyên một dòng với DisplayName, Required, Owned, Missing, IsSatisfied; danh sách rỗng với IsFree = true nghĩa là rank miễn phí
Kỹ năng yêu cầuPrerequisites: SkillId, RequiredRank, CurrentRank, IsSatisfied
Lý do khoáLockReasons: Prerequisite, CharacterLevel, Resources, Unknown

Điểm kỹ năng đến từ hai nguồn: LevelSkillPoints riêng của nhân vật (mỗi cấp trên 1 được một điểm, chỉ dùng trong cây của nhân vật đó) và skill_point trong ví. Costs[i].Owned đã tính cả hai. ProjectVCharacterResourceNames.GetDisplayName cho tên hiển thị với người chơi của một tài nguyên.

Reset cây kỹ năng​

Task<ProjectVResult> ResetSkillsAsync(string characterId)
Tham sốTypeGiá trị
characterIdstringId.Value của nhân vật có cây cần reset

Chỉ xoá mọi rank trong cây của nhân vật đó. Những gì được hoàn lại do server quyết định.

Trả về một ProjectVResult:

IsSuccessMessageĐiều gì đã xảy ra
truerỗngServer đã xác nhận reset. Cây và số dư tự vẽ lại
falselý do, sẵn sàng để hiệnKhông có gì được reset

Throw ArgumentException ngay lập tức khi characterId rỗng hoặc toàn khoảng trắng.

Khi thất bại — hiện Message nguyên văn:

Tình huốngNgười chơi thấy gì
Không kỹ năng nào trong cây có rank"There are no skill ranks to reset." (không gửi gì)
Server từ chốiThông báo của server
Mất kết nối hoặc timeoutMột thông báo lỗi mạng. Trang tự đồng bộ lại

Tắt nút Reset khi không kỹ năng nào trong GetSkills(characterId) có Rank lớn hơn 0.

Nâng cấp kỹ năng Weapon hoặc Character​

Task<ProjectVResult> UpgradeCastSkillAsync(string characterId, ProjectVCharacterCastSkillSlot slot)
Tham sốTypeGiá trị
characterIdstringId.Value của nhân vật
slotProjectVCharacterCastSkillSlotWeapon hoặc Character, không bao giờ Unknown

Các kỹ năng này tách biệt với cây kỹ năng và mỗi lần gọi tăng một cấp.

Trả về một ProjectVResult:

IsSuccessMessageĐiều gì đã xảy ra
truerỗngServer đã xác nhận nâng cấp. Trang tự vẽ lại
falselý do, sẵn sàng để hiệnKhông có gì được nâng cấp

Khi thất bại — hiện Message nguyên văn:

Tình huốngNgười chơi thấy gì
Bị chặn: thiếu tài nguyên, đã cấp tối đa, hoặc bị khoáCùng nội dung với BlockedMessage, ví dụ "Maximum level reached."
Không tìm thấy kỹ năng hoặc dữ liệu của nó không đầy đủ"Skill is unavailable. Sync to continue."
Dữ liệu đã cũ và lần đồng bộ trước đó thất bạiThông báo của lần đồng bộ, ví dụ lỗi mạng
characterId rỗng"The request was not confirmed. Please try again."
Server từ chốiThông báo của server

Nút Upgrade cast skill​

Dùng view.Data.GetCastSkillUpgradeDetails(characterId, slot) (ProjectVCharacterCastSkillUpgradeDetails):

Để hiệnĐọc
Bật UpgradeCanUpgrade
Vì sao khôngBlockedMessage
CấpLevel / MaxLevel (cả hai bắt đầu từ 1), IsMaxed
Asset gameplayCurrentSkillId hiện tại, NextSkillId sau khi nâng cấp (null ở cấp tối đa)
GiáCosts: Kind (vật phẩm hoặc tiền tệ), DisplayName, Required, Owned, Missing, IsSatisfied

Một nhánh đã max không có ID tiếp theo và không có chi phí. Khi có gì đó chưa rõ, nâng cấp vẫn bị chặn; không bao giờ coi chi phí bị thiếu là nâng cấp miễn phí.

Reset cấp​

Task<ProjectVResult> ResetLevelAsync(string characterId)
Tham sốTypeGiá trị
characterIdstringId.Value của một nhân vật hoặc đồng minh đang sở hữu

Đưa một nhân vật hoặc đồng minh về cấp 1 và hoàn lại các vật phẩm EXP đã dùng, khi server cho phép. Kiểm tra trước:

var reset = ProjectVCharacterLevelPolicy.ResetPreview(view.Data!, entry.Id);
resetButton.interactable = reset.CanReset;
resetText.text = reset.Message; // "Ready to reset the level." or why not
ShowRefunds(reset.Refunds); // ItemId and Quantity per line

Trả về một ProjectVResult:

IsSuccessMessageĐiều gì đã xảy ra
truerỗngServer đã xác nhận reset. Cấp và kho đồ tự vẽ lại
falselý do, sẵn sàng để hiệnKhông có gì được reset, hoặc câu trả lời bị mất

Khi thất bại — hiện Message nguyên văn:

Tình huốngNgười chơi thấy gì
Server không cho phépLý do của server, giống ResetPreview(...).Message
Nhân vật không được sở hữu"This character is not owned."
Server không có thông tin reset"Server reset requirements are unavailable."
Một câu trả lời lên cấp hoặc reset vẫn đang chờ"Resume the pending Character progression first."
Dữ liệu đã cũ và lần đồng bộ trước đó thất bạiThông báo của lần đồng bộ, ví dụ lỗi mạng
Mất kết nối hoặc timeoutMột thông báo lỗi mạng. PendingUpgrade có giá trị; xem Câu trả lời bị mất

Lượt bấm và đồng bộ lại​

  • Các command chạy lần lượt từng cái một. Bấm lần thứ hai vào một command đang chạy sẽ trả về cùng kết quả thay vì chạy hai lần. Mọi lần lên cấp dùng chung một slot: một LevelUpAsync thứ hai trong lúc một lần đang chạy, kể cả cho nhân vật khác, sẽ trả về kết quả của lần đầu.
  • Sau một kết quả thất bại hoặc chưa được xác nhận, màn tự đồng bộ lại và vẽ lại. Bạn không cần nút Sync.
  • Các command khác ngoài LevelUpAsync và ResetSkillsAsync không bao giờ throw khi đầu vào sai; chúng trả về "The request was not confirmed. Please try again."

Lỗi thường gặp​

  • Hiện result.Message sau khi lên cấp thành công. Nó rỗng khi thành công. Đọc Level mới từ lần Render tiếp theo.
  • Tự tính EXP cần có, chi phí hay cấp tối đa từ asset cục bộ. Dùng UpgradeState, PreviewXp và các upgrade details; khi thiếu điều kiện, giữ nút ở trạng thái tắt.
  • Dùng Data.CanAfford cho rank kỹ năng. Nó bỏ qua LevelSkillPoints riêng của nhân vật. Dùng GetSkillUpgradeDetails(...).CanUpgrade hoặc Costs của nó.
  • Bắt đầu một lần lên cấp mới khi PendingUpgrade có giá trị. Lệnh gọi sẽ hoàn tất lần đang chờ; lựa chọn mới của bạn không được dùng.
  • Mong EXP trong nhiệm vụ làm nhân vật lên cấp. Cấp và EXP kiếm được trong nhiệm vụ chỉ dành cho lượt chơi đó. EXP của nhân vật đến từ vật phẩm EXP.
  • Mong thay đổi ảnh hưởng đến một nhiệm vụ đang chạy. Gameplay nhận rank kỹ năng và kỹ năng Weapon, Character của nhân vật đã xuất phát trong ProjectVGameplayStartingDataView; thay đổi chúng ở đây sau khi lượt chơi bắt đầu không ảnh hưởng đến nó.
  • Dùng kết quả sau khi trang đã đóng. Kiểm tra this != null sau await.
  • Không dispose observation. Giữ IDisposable từ Observe và dispose (giải phóng) nó trong OnDisable hoặc OnDestroy.

Presenter​

IProjectVCharacterPresenter là interface tầng thấp hơn nằm sau screen contract. Chỉ dùng nó cho những gì màn không có:

Nhu cầuMember của presenter
Trang bị hoặc nâng cấp trang bịEquipEquipmentAsync, UnequipPrimaryWeaponAsync, UpgradeEquipmentAsync
Ba lôSetBackpackAsync(slotId, itemId, token); null làm trống ô
SkinEquipSkinAsync(skinId, token)
Vật phẩm khác trong khoUseInventoryItemAsync(itemId, quantity, token) (vật phẩm EXP dùng LevelUpAsync)
Cộng EXP mà không lên cấpUseCharacterXpItemsAsync(characterId, items, token)
Hộp thoại xác nhận giáPrepareSkillUpgrade, PrepareEquipmentUpgrade, ConfirmPreparedUpgradeAsync, Transactions
Mã lỗi và request IDCurrent.Error: Category, Code, RequestId, SafeMessage

Command của presenter trả về một ProjectVCharacterCommandResult (IsSuccess, Snapshot). Listener truyền vào 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. Observe trên màn luôn vẽ trên main thread.

Hộp thoại xác nhận​

Xác nhận là tuỳ chọn. Một lần nâng cấp kỹ năng đã chuẩn bị nhớ nhân vật và kỹ năng của nó, nên việc đổi nhân vật không thể thay đổi thứ nó nâng cấp. PrepareSkillUpgrade throw InvalidOperationException kèm BlockedMessage khi rank không thể nâng, nên hãy kiểm tra CanUpgrade trước.

presenter.PrepareSkillUpgrade(characterId, skillId); // fills presenter.Transactions.Current
ShowConfirm(presenter.Transactions.Current!); // title, price, balance preview

// on Confirm:
if (presenter.Transactions.CanConfirm)
{
var result = await presenter.ConfirmPreparedUpgradeAsync(token);
if (this != null && !result.IsSuccess)
{
ShowMessage(result.Snapshot.Error?.SafeMessage ?? string.Empty);
}
}

// on Cancel:
if (presenter.Transactions.CanCancel)
{
presenter.Transactions.Cancel();
}

presenter là IProjectVCharacterPresenter được inject; xem Xác nhận giao dịch để biết trạng thái của hộp thoại.