Перейти к основному содержимому

PvP-бои

MCE Online предоставляет полную серверно-авторитетную боевую систему, охватывающую PvP-матчмейкинг, валидацию диких/тренерских боёв, наблюдение, кооп-рейды и проверку захвата. Все боевые команды маршрутизируются через RPC Nakama, так что сервер является источником истины.

Обзор архитектуры​

КлассФайлОтветственность
NetworkedBattleManagerBattle/NetworkedBattleManager.csMonoBehaviour-мост между локальным BattleManager MCE и онлайн-слоем
ServerBattleAuthorityBattle/BattleServices.csОтправляет все боевые действия (приём, переключение, предмет, побег) через RPC Nakama
BattleMatchmakerBattle/BattleServices.csPvP-матчмейкинг на основе ELO с использованием матчмейкера Nakama
SpectatorServiceBattle/BattleServices.csНаблюдение за живыми боями в режиме только для чтения
RaidBattleServiceBattle/BattleServices.csКооп-бои 5v1 с боссами через матчи реального времени

Серверно-авторитетный поток боя​

NetworkedBattleManager решает, использовать ли онлайн-валидацию, на основе NakamaManager.IsConnected. Если офлайн или установлен forceOffline, бои выполняются полностью через локальный BattleManager.

Начало боя​

Дикая встреча (сервер генерирует дикого монстра, чтобы предотвратить манипуляцию IV/шайни):

string battleId = await networkedBattleManager.StartWildBattleAsync(
mapId, encounterTableId, playerLevel);

Это вызывает RPC battle_start_wild. Сервер возвращает BattleStartResponse с battleId и matchId. Клиент присоединяется к боевому матчу для обновлений состояния в реальном времени.

Бой с тренером/NPC:

string battleId = await networkedBattleManager.StartTrainerBattleAsync(
trainerId, mapId);

Вызывает RPC battle_start_trainer.

Отправка действий​

Все действия проходят через ServerBattleAuthority через NetworkedBattleManager.SubmitActionAsync():

// Use a move
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Move, moveIndex: 0, target: 0);

// Switch monster
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Switch, index: 2);

// Use an item (pass the inventory slot index; the server resolves the item ID)
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Item, index: itemSlot, target: targetSlot);

// Attempt to flee
await networkedBattleManager.SubmitActionAsync(
BattleActionType.Flee, index: 0);

Каждый тип действия отправляет разную полезную нагрузку в RPC battle_submit_action:

ДействиеПолезная нагрузкаСервер проверяет
MovebattleId, moveIndex, targetSlot, actionType: "move"Приём существует, PP остался, действительная цель
SwitchbattleId, switchTo (слот), actionType: "switch"Монстр жив, не в ловушке
ItembattleId, itemId, targetSlot, actionType: "item"Предмет в инвентаре, действительное использование
FleebattleId, actionType: "flee"Проверка побега (нельзя сбежать из тренерского PvP)

Результаты хода​

После того как оба игрока отправляют действия, сервер разрешает ход и возвращает TurnResult:

networkedBattleManager.OnTurnProcessed += (TurnResult result) =>
{
// result.turnNumber, result.playerMoveName, result.enemyMoveName
// result.playerDamageDealt, result.enemyDamageDealt
// result.effectiveness, result.statusApplied, result.weatherChange
// result.playerFainted, result.enemyFainted
};

Конец боя​

Когда бой завершается, OnBattleEnded срабатывает с BattleResult:

networkedBattleManager.OnBattleEnded += (BattleResult result) =>
{
// result.playerWon, result.expGained, result.moneyGained
// result.itemsGained, result.evGained, result.enemyCaught
// result.eloChange (PvP only)
};

Матч автоматически покидается, а состояние очищается через EndBattleAsync().

Онлайн-система захвата​

Попытки захвата проверяются на сервере, чтобы предотвратить манипуляцию шансом поимки:

CaptureResult result = await networkedBattleManager.AttemptCaptureAsync("pokeball");
// result.Success -- whether the capture succeeded
// result.ShakeCount -- number of shakes (0-3)
// result.IsCriticalCapture -- whether it was a critical capture

RPC battle_attempt_capture получает battleId и ballId, а сервер вычисляет шанс поимки, количество тряски и шанс критического захвата.

Матчмейкинг на основе ELO​

BattleMatchmaker использует встроенный матчмейкер Nakama с фильтрацией на основе ELO.

Начало поиска​

await NakamaManager.Instance.Matchmaker.StartSearchAsync(
eloRating: 1200, tier: "any");

Запрос матчмейкера находит противников в пределах +/- 200 ELO:

+properties.elo:>=1000 +properties.elo:<=1400

Числовые свойства включают elo игрока, а строковые свойства включают tier.

Матчмейкер создаёт матч на 2 игрока (minCount: 2, maxCount: 2).

Матч найден​

Когда матч найден, ReceivedMatchmakerMatched срабатывает на сокете, что запускает:

NakamaManager.Instance.Matchmaker.OnMatchFound += (matchId, opponent) =>
{
// opponent.UserId, opponent.DisplayName
// matchId -- the battle match to join
};

Авторитетный PvP-матч (pvp_match_create)​

Матчмейкинг находит противника; авторитетный PvP-бой затем выполняется в общем матче, принадлежащем серверу. pvp_match_create (RpcIds.PvpMatchCreate, обрабатывается rpcPvpMatchCreate в src/battle/pvp-engine.ts) объединяет двух игроков:

[Serializable]
public class PvpMatchCreateRequest { public string opponentUserId; }

[Serializable]
public class PvpMatchCreateResponse { public bool success; public string matchId; public string error; }

Вызывающий передаёт opponentUserId; сервер создаёт общий, серверно-авторитетный матч pvp_battle и возвращает его matchId для присоединения. Противнику отправляется уведомление pvp_match_ready (код 111), несущее тот же id, так что оба клиента сходятся на одном авторитетном матче. Оба отряда должны быть посеяны на стороне сервера, или RPC жёстко отклоняется — клиент не может подделать команду в рейтинговый матч.

Статус поиска и отмена​

bool searching = NakamaManager.Instance.Matchmaker.IsSearching;
float duration = NakamaManager.Instance.Matchmaker.SearchDuration;

await NakamaManager.Instance.Matchmaker.CancelSearchAsync();

OnMatchCancelled срабатывает после успешной отмены. Таймаут матчмейкинга настраивается через OnlineConfig.MatchmakingTimeout (по умолчанию: 60 секунд).

Режим наблюдателя​

SpectatorService позволяет игрокам наблюдать за живыми боями в режиме только для чтения.

Список живых боёв​

List<SpectableBattle> battles =
await NakamaManager.Instance.Spectator.GetLiveBattlesAsync();

foreach (var battle in battles)
{
// battle.BattleId, battle.Player1Name, battle.Player2Name
// battle.SpectatorCount, battle.Duration
}

Это вызывает RPC get_live_battles.

Наблюдение за боем​

bool joined = await NakamaManager.Instance.Spectator.SpectateAsync(battleMatchId);
// Receive battle state updates via OnBattleStateUpdate

await NakamaManager.Instance.Spectator.StopSpectatingAsync();

Режим наблюдателя контролируется через OnlineConfig.EnableSpectator (по умолчанию: true). Максимум наблюдателей на бой: MaxSpectatorsPerBattle (по умолчанию: 20).

Рейдовые бои (кооп 5v1)​

RaidBattleService предоставляет кооперативные бои с боссами, где до 5 игроков сражаются с одним мощным монстром.

Доступные рейды​

List<RaidBossInfo> raids =
await NakamaManager.Instance.Raids.GetAvailableRaidsAsync();

foreach (var raid in raids)
{
// raid.BossId, raid.BossName, raid.Difficulty (Normal/Hard/Legendary/Mythic)
// raid.CurrentParticipants, raid.MaxParticipants, raid.RecommendedLevel
}

Создание рейдового лобби​

string raidMatchId = await NakamaManager.Instance.Raids.CreateRaidLobbyAsync(
bossId: "raid_boss_legendary_dragon",
difficulty: RaidDifficulty.Legendary);

Это создаёт матч Nakama и регистрирует его на сервере через RPC raid_create с matchId, bossId, difficulty и maxParticipants (из OnlineConfig.RaidMaxParticipants, по умолчанию: 5).

Присоединение и игра​

bool joined = await NakamaManager.Instance.Raids.JoinRaidAsync(raidMatchId);

// Submit actions during the raid
await NakamaManager.Instance.Raids.SubmitRaidActionAsync(raidId, moveIndex);

Рейдовые действия используют код операции состояния матча 200.

Рейдовые события​

Raids.OnRaidFound += (raidId) => { };
Raids.OnPlayerJoinedRaid += (userId, count) => { };
Raids.OnRaidStarted += (raidId) => { };
Raids.OnRaidCompleted += (raidId, success) => { };

Рейдовые бои контролируются через OnlineConfig.EnableRaidBattles (по умолчанию: true).

Настройки сложности​

Поле конфигурацииПо умолчаниюОписание
MatchmakingTimeout60sТаймаут поиска PvP
PvPTurnTimeout45sВремя на ход в PvP
MaxSpectatorsPerBattle20Лимит наблюдателей
RaidMaxParticipants5Макс. игроков в рейде
EnableSpectatortrueПереключить режим наблюдателя
EnableRaidBattlestrueПереключить рейдовые бои

Античит в боевом контексте​

Когда OnlineConfig.EnableServerAuthority равно true, ВСЕ исходы боёв определяются на стороне сервера:

  • Дикие встречи: Сервер генерирует дикого монстра (вид, уровень, IV, шайни), чтобы предотвратить манипуляцию на стороне клиента.
  • Попытки захвата: Шанс поимки вычисляется на стороне сервера.
  • EXP/деньги/предметы: Награды в BattleResult приходят с сервера и применяются только после валидации.
  • Изменения ELO: Корректировки рейтинга вычисляются и применяются сервером.

OnlineCommandBridge гарантирует, что любые предметы или деньги, полученные из боёв, проверяются через RPC mce_give_item и mce_give_money.