Alternar tema

Design de máquina de estados para minijogos: fluxo completo da tela inicial à batalha e ao resultado

Easton editorial illustration: central gameplay state rotor linking home, battle, pause, and result states with a nested combat ring

"Em jogos de cartas e tabuleiro, 80% dos bugs vêm de um design inadequado de estados; a máquina de estados ajuda a resolver o problema de verificações espalhadas pelo código."

- Artigo da Sohu sobre arquitetura de jogos de cartas e tabuleiro

"Máquinas de estados aninhadas já se tornaram um padrão comum em jogos complexos; dentro de Battle State, é possível incluir quatro subestados: BeginBattle, HeroTurn, EnemyTurn e EndBattle."

Seu minijogo acabou de ir ao ar, e os jogadores começam a reclamar que a tela de resultado trava com frequência. A partida já acabou, mas a interface ainda mostra o estado de batalha. Você abre o código e encontra uma tela cheia de if (isPlaying && !isPaused && !isGameOver && hasPlayerLeft). Naquele momento, só dá vontade de perguntar: quem escreveu esse código? Ah, fui eu.

Quando trabalhei em um minijogo de cartas, caí nessa mesma armadilha. A lógica de estado estava espalhada por variáveis booleanas; para mudar uma funcionalidade, eu precisava mexer em mais de uma dezena de arquivos. O pior era no multiplayer: o servidor já mudava para o estado de resultado, enquanto o cliente ainda tocava a animação de batalha. Os dois lados entravam em desvio de estado. Depois refatorei para uma máquina de estados, e o código ficou bem mais limpo.

Este texto fala sobre como desenhar uma máquina de estados para minijogos. Da tela inicial à batalha e ao resultado, vou separar tudo em uma arquitetura de três camadas: camada de fluxo do jogo, camada de fluxo da batalha e camada de detalhes da batalha. Também entram as armadilhas de produção: bloqueio de estado, validação no servidor e timeout. É isso que costuma salvar o projeto no mundo real.

Por que minijogos precisam de máquina de estados?

Resumindo, um jogo é um conjunto de estados alternando o tempo todo.

Tela inicial, batalha, resultado, pausa, modo espectador: por trás de cada tela existe um estado. O jogador clica em “Iniciar jogo”, a tela inicial desaparece e a tela de batalha aparece. A batalha termina, e o resultado surge. Parece simples, mas o código começa a se embolar rápido.

Os três pecados do fluxo em cascata de if-else

Veja primeiro este trecho. Você provavelmente já viu algo parecido:

// Verificações de estado espalhadas por todos os lados
if (isPlaying && !isPaused && !isGameOver) {
  // O jogador pode operar
}

if (isGameOver && !hasShownResult) {
  // Exibe a tela de resultado
}

if (isPlaying && currentPlayer === 'player1') {
  // Turno do jogador 1
}

Esse código tem três pecados.

Difícil de ler: para saber o estado atual, você precisa procurar variáveis booleanas pelo arquivo inteiro. isPlaying, isPaused, isGameOver: tudo fica espalhado, como peças soltas de um quebra-cabeça. Você precisa montar a imagem sozinho.

Difícil de mudar: quer adicionar um estado de “pausa”? Vai precisar alterar ifs em vários lugares. Se esquecer um, o bug aparece. Uma vez adicionei um estado de “espectador”, mexi no código por dois dias e ainda subi com problema: uma animação específica não pausava.

Difícil de depurar: quando a ordem das transições bagunça, você nem sabe onde procurar. isGameOver virou true, mas hasShownResult ainda está false. Qual condição ficou faltando?

Aquele artigo da Sohu mencionava que, em jogos de cartas e tabuleiro, 80% dos bugs vêm de um design inadequado de estados. O número assusta, mas quem já passou por isso sabe que faz sentido.

Como a máquina de estados resolve isso?

A máquina de estados puxa essas verificações espalhadas para um único lugar.

Cada estado tem seu próprio comportamento: o que fazer ao entrar, o que fazer enquanto roda e o que fazer ao sair. As regras de transição ficam explícitas: da tela inicial você só pode ir para a batalha, não pular direto para o resultado.

Compare:

// Abordagem com máquina de estados
class HomeState {
  enter() { showHomeUI(); }
  exit() { hideHomeUI(); }
  handleEvent(event) {
    if (event === 'START_BATTLE') {
      manager.changeState(new BattleState());
    }
  }
}

class BattleState {
  enter() { showBattleUI(); startBattle(); }
  exit() { hideBattleUI(); cleanupBattle(); }
}

O estado fica encapsulado. Mudar a lógica de HomeState não afeta BattleState. Para adicionar um estado, você cria uma nova classe State e adiciona uma regra de transição.

O artigo da Better Programming diz que máquinas de estados aninhadas já viraram um padrão comum em jogos complexos. Minijogos não fogem disso: quando o estado fica claro, o código também fica claro.

Arquitetura de máquina de estados em três camadas

A máquina de estados de um minijogo precisa ser desenhada em camadas. Sem essa separação, a lógica de batalha acaba enfiada na camada de fluxo do jogo, e o código vai ficando cada vez mais inchado.

Eu separo a máquina de estados em três camadas: fluxo do jogo, fluxo da batalha e detalhes da batalha.

Primeira camada: fluxo do jogo

Esta é a espinha dorsal do jogo, o fluxo maior do boot ao resultado:

BootState: quando o jogo inicia, carrega recursos e inicializa o motor. A cena Boot do Cocos Creator serve para isso: roda Boot primeiro, depois troca para Home.

HomeState: menu inicial. O jogador escolhe fase, personagem e vê o ranking. A lógica aqui é relativamente simples: mostrar UI e tratar cliques de botão.

BattleState: cena de batalha. Esta camada não é o fim; ela é um contêiner. Dentro da batalha ainda existe uma máquina de subestados.

SettlementState: tela de resultado. Mostra vitória ou derrota, estatísticas e limpa os recursos da batalha.

O fluxo fica assim: Boot -> Home -> Battle -> Settlement -> volta para Home ou sai.

Um rascunho em código:

Início do jogo -> BootState
              -> (carregamento concluído)
             HomeState
              -> (clicar em iniciar)
             BattleState <- máquina de subestados da batalha aqui
              -> (batalha encerrada)
          SettlementState
              -> (clicar em voltar)
             HomeState

Segunda camada: fluxo da batalha

Dentro de BattleState, ainda existe outra máquina de subestados. O artigo da Better Programming menciona que Battle State pode conter quatro subestados: BeginBattle, HeroTurn, EnemyTurn e EndBattle.

BeginBattleState: inicializa a batalha. Define parâmetros da fase, carrega dados dos personagens e sincroniza o estado de todos os jogadores. Em multiplayer, esta etapa precisa de bloqueio para impedir que jogadores entrem ou saiam no meio do processo.

PlayerTurnState: turno do jogador. Jogar carta, atacar, defender: todas essas ações são tratadas nesse estado. Quando o turno termina, a máquina troca para o próximo turno ou para o resultado.

EnemyTurnState (opcional): ação da IA. Em um minijogo single-player, pode ser pulado; em multiplayer, é o turno do oponente.

EndBattleState: lógica de resultado. Decide vitória ou derrota, calcula recompensas e dispara a animação de resultado.

Fluxo:

Entrar em BattleState -> BeginBattleState (bloqueio, sincronização)
                      -> (preparação concluída)
                  PlayerTurnState
                      -> (turno encerrado)
                  EnemyTurnState (opcional)
                      -> (batalha encerrada)
                  EndBattleState
                      -> (resultado concluído)
             Sair de BattleState -> SettlementState

Terceira camada: detalhes da batalha

Alguns estados ainda precisam ser divididos. Por exemplo, dentro de PlayerTurnState pode existir:

Máquina de estados de animação: toca animação de ataque quando o personagem ataca e animação de dano quando recebe dano. Unity e Cocos Creator já têm máquinas de estados de animação internas, então não vou aprofundar aqui.

Máquina de estados do turno: jogar carta -> atacar -> defender -> encerrar turno. Cada ação é um subestado.

O jogo Beast Card Clash encapsula setup, scoring e results screen em classes de estado independentes. A vantagem desse desenho é simples: mudar a lógica de uma etapa não afeta as outras.

A principal vantagem da arquitetura em três camadas é a separação de responsabilidades. Mudar a lógica da tela inicial não quebra a lógica da batalha. Mudar detalhes do turno não interfere no fluxo de resultado. A colaboração em equipe também melhora: uma pessoa cuida da camada de fluxo da batalha, outra cuida da camada de detalhes.

Design da interface central da máquina de estados

Com a arquitetura em três camadas definida, vamos ver como escrever o código.

Aquele artigo sobre Unity no Zhihu mencionava que a interface unificada de um nó de máquina de estados é OnEnter, OnUpdate, OnExit e OnHandleEvent. Usei essa ideia como referência e escrevi uma versão em TypeScript.

Interface IGameState

Cada estado precisa implementar esta interface:

interface IGameState {
  name: string;                    // Nome do estado, útil para identificar durante a depuração
  
  enter(params?: any): void;       // Inicializa ao entrar no estado
  update(dt: number): void;        // Atualiza a cada frame (opcional)
  exit(): void;                    // Limpa ao sair do estado
  handleEvent(event: GameEvent): void;  // Trata eventos e dispara transições de estado
}

O papel dos cinco métodos:

name: usado para depuração. Ao imprimir logs, dá para ver se o estado atual é “HomeState” ou “BattleState”, o que é mais direto do que olhar variáveis booleanas.

enter: chamado quando entra no estado. O enter de HomeState mostra a UI da tela inicial e carrega dados do jogador. O enter de BattleState inicializa parâmetros da batalha e entra na máquina de subestados da batalha.

update: atualização a cada frame. É muito usado na cena de batalha para atualizar contagem regressiva e detectar operações do jogador. Na tela inicial, quase nunca é necessário.

exit: limpeza ao sair do estado. Libera recursos, oculta UI e remove listeners.

handleEvent: troca orientada por eventos. Clicar em “Iniciar jogo” dispara o evento START_BATTLE; HomeState trata esse evento e troca para BattleState.

GameStateManager: o gerenciador de estados

A máquina de estados precisa de um gerenciador para trocar estados. Aquele artigo do Stack Exchange citava um desenho de classe CGameEngine com Init, Cleanup, ChangeState, PushState e PopState.

Eu simplifiquei:

class GameStateManager {
  private currentState: IGameState | null = null;
  private stateStack: IGameState[] = [];  // Suporta estados sobrepostos (pausa cobrindo batalha)
  
  // Inicialização
  init(firstState: IGameState) {
    this.currentState = firstState;
    this.currentState.enter();
  }
  
  // Troca de estado
  changeState(newState: IGameState, params?: any) {
    if (this.currentState) {
      this.currentState.exit();
    }
    this.currentState = newState;
    this.currentState.enter(params);
  }
  
  // Empilha um estado sobreposto (para pausa e popups)
  pushState(state: IGameState) {
    if (this.currentState) {
      // O estado atual não sai; apenas fica pausado
      this.stateStack.push(this.currentState);
    }
    this.currentState = state;
    state.enter();
  }
  
  // Desempilha o estado sobreposto
  popState() {
    if (this.currentState) {
      this.currentState.exit();
    }
    this.currentState = this.stateStack.pop();
    // Não precisa chamar enter de novo, porque antes ele só estava pausado
  }
  
  // Atualização a cada frame
  update(dt: number) {
    if (this.currentState) {
      this.currentState.update(dt);
    }
  }
  
  // Tratamento de eventos
  handleEvent(event: GameEvent) {
    if (this.currentState) {
      this.currentState.handleEvent(event);
    }
  }
}

changeState: troca de estado. O estado antigo sai; o novo entra. É isso que você usa ao ir da tela inicial para a batalha.

pushState / popState: estados sobrepostos. Durante a batalha, ao apertar pausa, PauseState entra no topo da pilha cobrindo BattleState. Ao cancelar a pausa, PauseState sai da pilha e BattleState é retomado.

Estados sobrepostos são importantes. Pausa, popup e caixa de confirmação precisam cobrir o estado atual sem destruí-lo.

Armadilhas práticas e soluções

Com a teoria no lugar, vale falar das armadilhas reais. O artigo da Sohu resumia experiências de jogos de cartas e tabuleiro; aqui vou abrir alguns problemas clássicos.

Desvio de estado: servidor e cliente ficam inconsistentes

Esta é uma das armadilhas mais comuns em partidas multiplayer.

O cenário é assim: o servidor já mudou para o estado de resultado, mas o cliente de um jogador ainda está na animação de batalha. Por causa de latência de rede, ele não recebeu o broadcast de troca de estado. A tela mostra “em batalha”, mas a partida já terminou.

Causa: a mensagem de troca de estado se perdeu ou atrasou demais.

Solução:

  1. Mecanismo de sincronização de estado: quando o servidor troca de estado, faz broadcast para todos os clientes. Ao receber a mensagem, o cliente sincroniza imediatamente.

  2. Heartbeat: a cada poucos segundos, o cliente envia um heartbeat ao servidor com o estado atual. Se o servidor detectar inconsistência, força o envio de uma mensagem de sincronização.

// Heartbeat do cliente
class BattleState {
  enter() {
    this.startHeartbeat();
  }
  
  startHeartbeat() {
    setInterval(() => {
      socket.send({
        type: 'HEARTBEAT',
        state: this.manager.currentState.name,
        roomId: this.roomId
      });
    }, 3000);  // Envia uma vez a cada 3 segundos
  }
}

Conflito de concorrência: vários jogadores operam ao mesmo tempo

Na mesa, dois jogadores jogam carta ao mesmo tempo, e a ordem das transições de estado bagunça.

Causa: não há bloqueio de estado, e operações concorrentes não entram em fila.

Solução: bloqueio de estado + mecanismo de “ID do operador atual”.

class DealingState implements IGameState {
  private lock: boolean = true;
  private currentOperator: string = '';
  
  enter() {
    this.lock = true;  // Bloqueia a etapa de distribuição de cartas
    // Impede jogadores de entrar ou sair
    // Sincroniza os dados iniciais de todos
    
    setTimeout(() => this.unlock(), 3000);  // Desbloqueia após 3 segundos
  }
  
  unlock() {
    this.lock = false;
    this.currentOperator = this.getFirstPlayerId();
    this.manager.changeState(new PlayerTurnState());
  }
  
  handleEvent(event: GameEvent) {
    if (this.lock) {
      // Em estado bloqueado, recusa operações
      return;
    }
    
    // Apenas o operador atual pode disparar eventos
    if (event.playerId === this.currentOperator) {
      // Trata a operação
    }
  }
}

A vantagem do bloqueio é que, em momentos críticos como distribuição de cartas e resultado, só existe um fluxo rodando. Ele não é interrompido por ações do jogador.

Segurança do resultado: pacote adulterado envia pontuação falsa

O cliente envia dados de resultado: pontuação, vitória ou derrota. Um pacote adulterado altera a pontuação. Como o servidor sabe o que é verdadeiro?

Causa: o servidor confia nos dados do cliente.

Solução: o servidor calcula de forma independente e não confia no resultado do cliente.

// Lógica de resultado no servidor
class EndBattleState {
  enter() {
    // Não aceita a pontuação enviada pelo cliente
    // O servidor calcula de forma independente com base no registro da batalha
    const result = this.calculateResultFromLog(battleLog);
    
    // Envia o resultado para todos os clientes
    this.broadcastResult(result);
  }
  
  calculateResultFromLog(log: BattleLog) {
    // Calcula de forma independente com base no registro da batalha
    // (ordem das cartas, dados de ataque)
    // O log do cliente pode ser falsificado; dados críticos precisam ser validados no servidor
  }
}

O princípio central é: o servidor não confia no cliente. O cliente só exibe; quem decide é o servidor.

Travamento por timeout: estado sem condição de saída

Um estado fica preso e nunca troca para o próximo.

Causa: o estado não tem mecanismo de timeout, e o evento aguardado nunca dispara.

Solução: definir um timer de timeout para cada estado.

class PlayerTurnState implements IGameState {
  private timeoutTimer: number;
  
  enter() {
    this.timeoutTimer = setTimeout(() => {
      // Pula automaticamente ao estourar o tempo
      this.manager.handleEvent({
        type: 'TIMEOUT_SKIP',
        playerId: this.currentPlayer
      });
    }, 30000);  // Timeout de 30 segundos
  }
  
  exit() {
    clearTimeout(this.timeoutTimer);  // Limpa o timer ao sair
  }
}

O mecanismo de timeout é essencial. Jogador cai, rede trava: o estado não pode esperar para sempre. Precisa existir uma saída de segurança.

Essas quatro armadilhas são mais importantes do que a teoria. Eu já caí no desvio de estado e levei dois dias para encontrar a causa. Depois de adicionar heartbeat, o problema sumiu.

Caso prático com Cocos Creator

Com teoria, interface e armadilhas em mãos, vamos ver como aplicar isso no Cocos Creator.

No texto anterior, “Estrutura de projeto para minijogos com Cocos Creator”, falamos sobre como separar Boot, cena e página de resultado. Este texto segue a mesma linha: como combinar máquina de estados com Layer.

Máquina de estados em uma arquitetura de cena única

Para minijogos no Cocos Creator, a recomendação é cena única + múltiplos Layers. Uma cena, vários Layers exibidos e ocultados. A máquina de estados encaixa exatamente nessa arquitetura.

BootLayer -> HomeLayer -> BattleLayer -> SettlementLayer

Cada Layer corresponde a um estado:

import { director, Node } from 'cc';

// HomeState
class HomeState implements IGameState {
  name = 'Home';
  private homeLayer: Node | null = null;
  
  enter() {
    // Exibe HomeLayer
    const scene = director.getScene();
    this.homeLayer = scene?.getChildByName('HomeLayer') ?? null;
    if (this.homeLayer) {
      this.homeLayer.active = true;
      this.initHomeUI();
    }
  }
  
  exit() {
    // Oculta HomeLayer
    if (this.homeLayer) {
      this.homeLayer.active = false;
      this.cleanupHome();
    }
  }
  
  handleEvent(event: GameEvent) {
    if (event.type === 'START_BATTLE') {
      // Troca para BattleState
      this.manager.changeState(new BattleState(), event.params);
    }
  }
  
  initHomeUI() {
    // Carrega dados do jogador e configura eventos de clique dos botões
  }
  
  cleanupHome() {
    // Libera recursos da tela inicial
  }
}

// BattleState
class BattleState implements IGameState {
  name = 'Battle';
  private battleLayer: Node | null = null;
  
  enter(params?: any) {
    const scene = director.getScene();
    this.battleLayer = scene?.getChildByName('BattleLayer') ?? null;
    if (this.battleLayer) {
      this.battleLayer.active = true;
      this.startBattle(params);
    }
  }
  
  exit() {
    if (this.battleLayer) {
      this.battleLayer.active = false;
      this.cleanupBattle();
    }
  }
  
  startBattle(params: any) {
    // Entra na máquina de subestados da batalha
    this.battleStateManager.init(new BeginBattleState(params));
  }
  
  cleanupBattle() {
    // Limpa recursos da batalha
  }
}

Mostrar ou ocultar Layer é a representação visual da troca de estado. enter mostra; exit oculta.

Máquina de estados e passagem de dados

Ao trocar de estado, passe parâmetros. Por exemplo, quando a tela inicial escolhe uma fase, BattleState precisa saber qual é:

// HomeState inicia a transição
handleEvent(event: GameEvent) {
  if (event.type === 'START_BATTLE') {
    // Passa parâmetros da fase
    this.manager.changeState(new BattleState(), {
      levelId: event.levelId,
      difficulty: event.difficulty
    });
  }
}

// BattleState recebe os parâmetros
enter(params?: any) {
  const levelId = params?.levelId ?? 'default';
  const difficulty = params?.difficulty ?? 'normal';
  this.loadLevel(levelId, difficulty);
}

Para persistir estado, use localStorage. Se o jogador sair no meio da batalha, dá para recuperar o progresso na próxima entrada:

// Salva o progresso
exit() {
  localStorage.setItem('battle_progress', JSON.stringify({
    levelId: this.levelId,
    round: this.currentRound,
    score: this.score
  }));
}

// Recupera o progresso
enter() {
  const saved = localStorage.getItem('battle_progress');
  if (saved) {
    const progress = JSON.parse(saved);
    this.resumeFromProgress(progress);
  }
}

Pontos de atenção em minijogos do WeChat

O artigo da CSDN menciona que, em minijogos do WeChat, deixar a máquina de estados em um arquivo global facilita modificações.

Limite de pacote: o pacote principal de um minijogo do WeChat tem 4 MB. O código da máquina de estados precisa ser enxuto; não dá para criar classes demais sem critério.

Carregamento por subpacotes: coloque a máquina de estados central no pacote principal (Boot, Home, Battle) e estados de expansão em subpacotes, como fases especiais e telas de evento.

Arquivo global de estado: inicialize a máquina de estados no arquivo de entrada game.js do minijogo WeChat; outros módulos acessam por variável global.

// game.js
import { GameStateManager } from './states/GameStateManager';
import { BootState } from './states/BootState';

// Gerenciador global de estados
window.gameStateManager = new GameStateManager();
window.gameStateManager.init(new BootState());

Ao chamar APIs do WeChat, use a variável global para obter o estado:

// Callback de login do WeChat
wx.login({
  success: (res) => {
    const state = window.gameStateManager.currentState;
    if (state.name === 'HomeState') {
      state.handleEvent({ type: 'LOGIN_SUCCESS', code: res.code });
    }
  }
});

A vantagem dessa arquitetura é que a API do WeChat não precisa importar a máquina de estados. Ela acessa tudo pela variável global.


Este texto fica mais completo quando lido junto com o anterior, “Estrutura de projeto para minijogos com Cocos Creator”. A estrutura do projeto é o contêiner da máquina de estados; a máquina de estados é a lógica de comportamento dessa estrutura.

Resumo

Depois de tudo isso, máquina de estados é basicamente dividir o fluxo do jogo em nós claros.

A arquitetura em três camadas, com fluxo do jogo, fluxo da batalha e detalhes da batalha, funciona tanto em minijogos quanto em projetos maiores. A vantagem é que alterar uma camada não afeta as outras, e a colaboração da equipe fica mais simples.

Experiência de armadilha vale mais do que teoria. Desvio de estado, conflito de concorrência, segurança no resultado e travamento por timeout: já passei pelos quatro, e depurar isso é doloroso. Depois que adicionei bloqueio de estado, heartbeat, validação no servidor e timeout, os problemas desapareceram. Quando o código acerta a estrutura, aparecem bem menos bugs.

Se você quiser testar na prática, pode baixar o exemplo completo de código (link do GitHub). Leia primeiro o texto anterior, “Estrutura de projeto para minijogos com Cocos Creator”, para entender como separar Layers; depois volte a este texto para entender como a máquina de estados roda. Com os dois juntos, a arquitetura fica clara.

O próximo passo será falar sobre teste de máquina de estados com ajuda de IA: deixar a IA gerar casos de teste cobrindo diferentes caminhos de transição. Com testes automatizados, a chance de cair nessas armadilhas fica menor.

Projetar uma arquitetura de máquina de estados em três camadas para minijogos

Fluxo completo de gerenciamento de estados da tela inicial à batalha e ao resultado

⏱️ Estimated time: 60 min

  1. 1

    Step 1: Definir os estados da camada de fluxo do jogo

    Liste os estados principais do jogo: BootState, HomeState, BattleState e SettlementState. Cada estado corresponde a um Layer ou a uma cena, e as regras de transição entre estados devem ficar claras.
  2. 2

    Step 2: Separar os subestados da camada de fluxo da batalha

    Dentro de BattleState, crie uma máquina de subestados: BeginBattleState (inicialização com bloqueio), PlayerTurnState (turno do jogador), EnemyTurnState (turno opcional da IA) e EndBattleState (decisão de resultado).
  3. 3

    Step 3: Implementar a interface de estado e o gerenciador

    Crie a interface IGameState (name, enter, exit, update, handleEvent) e implemente GameStateManager para controlar transições de estado (changeState) e operações de pilha (pushState, popState).
  4. 4

    Step 4: Adicionar mecanismos de proteção contra armadilhas

    Bloqueie estados críticos, como distribuição de cartas e resultado, para evitar conflitos de operação concorrente; use timeouts para evitar travamentos; implemente heartbeat para resolver desvio de estado; e calcule o resultado no servidor para evitar trapaça.
  5. 5

    Step 5: Integrar com Layers do Cocos Creator

    No enter, exiba o Layer correspondente e inicialize os dados; no exit, oculte o Layer e limpe recursos. A máquina de estados dirige a troca de UI e separa a lógica do código da representação visual.

FAQ

Todo minijogo precisa usar máquina de estados?
Não necessariamente. Mas, quando há muitos estados, como tela inicial, batalha, pausa e resultado, o if-else começa a virar bagunça. A máquina de estados ajuda a concentrar a lógica e deixa as mudanças mais seguras.
Uma máquina de estados em três camadas não é overengineering?
Um minijogo simples talvez não precise de três camadas, mas entender essa arquitetura ajuda em cenários mais complexos. Jogos simples podem usar duas camadas; jogos mais complexos podem evoluir para três.
Qual é a diferença entre máquina de estados e troca de cena?
Troca de cena é um conceito de gerenciamento de recursos do Cocos; máquina de estados é um conceito de lógica. Uma cena pode corresponder a um estado, e um estado também pode apenas controlar a exibição ou ocultação da UI.
Como resolver desvio de estado em partidas multiplayer?
Com broadcast de troca de estado pelo servidor + heartbeat no cliente. A cada 3 segundos, o cliente envia um heartbeat; se o servidor detectar inconsistência, força a sincronização.
Como impedir trapaça nos dados de resultado?
O servidor calcula tudo de forma independente e não confia no resultado enviado pelo cliente. O cliente apenas exibe; toda a lógica de decisão fica no servidor.

16 min de leitura · Publicado em: 19 mai 2026 · Atualizado em: 14 jul 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog