Saltar al contenido principal

Batallas PvP

MCE Online proporciona un sistema de batalla completo con autoridad del servidor que cubre emparejamiento PvP, validación de batallas salvajes/de entrenador, espectadores, incursiones cooperativas y verificación de captura. Todos los comandos de batalla pasan por RPCs de Nakama, por lo que el servidor es la fuente de la verdad.

Descripción general de la arquitectura​

ClaseArchivoResponsabilidad
NetworkedBattleManagerBattle/NetworkedBattleManager.csPuente MonoBehaviour entre el BattleManager local de MCE y la capa online
ServerBattleAuthorityBattle/BattleServices.csEnvía todas las acciones de batalla (movimiento, cambio, objeto, huida) via RPCs de Nakama
BattleMatchmakerBattle/BattleServices.csEmparejamiento PvP basado en ELO usando el matchmaker de Nakama
SpectatorServiceBattle/BattleServices.csVer batallas en vivo en modo de solo lectura
RaidBattleServiceBattle/BattleServices.csBatallas de jefe cooperativas 5v1 via partidas en tiempo real

Flujo de batalla con autoridad del servidor​

NetworkedBattleManager decide si usar validación online según NakamaManager.IsConnected. Si está offline o forceOffline está establecido, las batallas se ejecutan enteramente a través del BattleManager local.

Iniciar una batalla​

Encuentro salvaje (el servidor genera el monstruo salvaje para prevenir la manipulación de IV/shiny):

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

Esto llama al RPC battle_start_wild. El servidor devuelve un BattleStartResponse con battleId y matchId. El cliente se une a la partida de batalla para actualizaciones de estado en tiempo real.

Batalla de entrenador/NPC:

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

Llama al RPC battle_start_trainer.

Enviar acciones​

Todas las acciones pasan por ServerBattleAuthority via 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);

Cada tipo de acción envía una carga diferente al RPC battle_submit_action:

AcciónCargaEl servidor valida
MovebattleId, moveIndex, targetSlot, actionType: "move"El movimiento existe, PP restante, objetivo válido
SwitchbattleId, switchTo (ranura), actionType: "switch"Monstruo vivo, no atrapado
ItembattleId, itemId, targetSlot, actionType: "item"Objeto en inventario, uso válido
FleebattleId, actionType: "flee"Verificación de huida (no se puede huir de PvP de entrenador)

Resultados de turno​

Después de que ambos jugadores envían acciones, el servidor resuelve el turno y devuelve un 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
};

Fin de batalla​

Cuando la batalla concluye, OnBattleEnded se dispara con un BattleResult:

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

La partida se abandona automáticamente y el estado se limpia via EndBattleAsync().

Sistema de captura online​

Los intentos de captura se validan del lado del servidor para prevenir la manipulación de la tasa de captura:

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

El RPC battle_attempt_capture recibe el battleId y el ballId, y el servidor calcula la tasa de captura, el conteo de sacudidas y la probabilidad de captura crítica.

Emparejamiento basado en ELO​

BattleMatchmaker usa el matchmaker integrado de Nakama con filtrado basado en ELO.

Iniciar una búsqueda​

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

La consulta del matchmaker encuentra oponentes dentro de +/- 200 ELO:

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

Las propiedades numéricas incluyen el elo del jugador, y las propiedades de cadena incluyen el tier.

El matchmaker crea una partida de 2 jugadores (minCount: 2, maxCount: 2).

Partida encontrada​

Cuando se encuentra una partida, ReceivedMatchmakerMatched se dispara en el socket, lo que activa:

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

Partida PvP autoritativa (pvp_match_create)​

El emparejamiento encuentra un oponente; la batalla PvP autoritativa luego se ejecuta en una partida compartida propiedad del servidor. pvp_match_create (RpcIds.PvpMatchCreate, manejado por rpcPvpMatchCreate en src/battle/pvp-engine.ts) empareja a los dos jugadores:

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

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

Quien llama pasa el opponentUserId; el servidor crea la partida compartida con autoridad del servidor pvp_battle y devuelve su matchId para unirse. Al oponente se le envía una notificación pvp_match_ready (código 111) que lleva el mismo id, así ambos clientes convergen en una partida autoritativa. Ambos rosters deben sembrarse del lado del servidor o el RPC rechaza en firme — un cliente no puede falsificar un equipo en una partida clasificatoria.

Estado de búsqueda y cancelación​

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

await NakamaManager.Instance.Matchmaker.CancelSearchAsync();

OnMatchCancelled se dispara tras una cancelación exitosa. El timeout de emparejamiento se configura via OnlineConfig.MatchmakingTimeout (por defecto: 60 segundos).

Modo espectador​

SpectatorService permite a los jugadores ver batallas en vivo en modo de solo lectura.

Listar batallas en vivo​

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

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

Esto llama al RPC get_live_battles.

Ver una batalla​

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

await NakamaManager.Instance.Spectator.StopSpectatingAsync();

El modo espectador se controla mediante OnlineConfig.EnableSpectator (por defecto: true). Máximo de espectadores por batalla: MaxSpectatorsPerBattle (por defecto: 20).

Batallas de incursión (cooperativas 5v1)​

RaidBattleService proporciona batallas de jefe cooperativas donde hasta 5 jugadores pelean contra un único monstruo poderoso.

Incursiones disponibles​

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
}

Crear un lobby de incursión​

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

Esto crea una partida de Nakama y la registra en el servidor via el RPC raid_create con el matchId, bossId, difficulty y maxParticipants (de OnlineConfig.RaidMaxParticipants, por defecto: 5).

Unirse y jugar​

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

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

Las acciones de incursión usan el op code de estado de partida 200.

Eventos de incursión​

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

Las batallas de incursión se controlan mediante OnlineConfig.EnableRaidBattles (por defecto: true).

Ajustes de dificultad​

Campo de configPor defectoDescripción
MatchmakingTimeout60sTimeout de búsqueda PvP
PvPTurnTimeout45sTiempo por turno en PvP
MaxSpectatorsPerBattle20Tope de espectadores
RaidMaxParticipants5Máximo de jugadores en una incursión
EnableSpectatortrueAlternar modo espectador
EnableRaidBattlestrueAlternar batallas de incursión

Anti-trampas en el contexto de batalla​

Cuando OnlineConfig.EnableServerAuthority es true, TODOS los resultados de batalla se determinan del lado del servidor:

  • Encuentros salvajes: El servidor genera el monstruo salvaje (especie, nivel, IVs, brillo shiny) para prevenir la manipulación del lado del cliente.
  • Intentos de captura: La tasa de captura se calcula del lado del servidor.
  • EXP/dinero/objetos: Las recompensas en BattleResult provienen del servidor y se aplican solo después de la validación.
  • Cambios de ELO: Los ajustes de clasificación se calculan y aplican por el servidor.

El OnlineCommandBridge asegura que cualquier objeto o dinero ganado en batallas se valide via los RPCs mce_give_item y mce_give_money.