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
| Clase | Archivo | Responsabilidad |
|---|---|---|
NetworkedBattleManager | Battle/NetworkedBattleManager.cs | Puente MonoBehaviour entre el BattleManager local de MCE y la capa online |
ServerBattleAuthority | Battle/BattleServices.cs | Envía todas las acciones de batalla (movimiento, cambio, objeto, huida) via RPCs de Nakama |
BattleMatchmaker | Battle/BattleServices.cs | Emparejamiento PvP basado en ELO usando el matchmaker de Nakama |
SpectatorService | Battle/BattleServices.cs | Ver batallas en vivo en modo de solo lectura |
RaidBattleService | Battle/BattleServices.cs | Batallas 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ón | Carga | El servidor valida |
|---|---|---|
| Move | battleId, moveIndex, targetSlot, actionType: "move" | El movimiento existe, PP restante, objetivo válido |
| Switch | battleId, switchTo (ranura), actionType: "switch" | Monstruo vivo, no atrapado |
| Item | battleId, itemId, targetSlot, actionType: "item" | Objeto en inventario, uso válido |
| Flee | battleId, 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 config | Por defecto | Descripción |
|---|---|---|
MatchmakingTimeout | 60s | Timeout de búsqueda PvP |
PvPTurnTimeout | 45s | Tiempo por turno en PvP |
MaxSpectatorsPerBattle | 20 | Tope de espectadores |
RaidMaxParticipants | 5 | Máximo de jugadores en una incursión |
EnableSpectator | true | Alternar modo espectador |
EnableRaidBattles | true | Alternar 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
BattleResultprovienen 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.