Sleeps acumulando

Segue o método que pausa o jogo por 1 segundo:

public void pausa(){
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException ex) {
        Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
    }
}

Uso ele em dois momentos durante o combate:

public void atacar(){
 // operacoes
        pausa();
// operacoes
        apanhar();
}

public void apanhar(){
 // operacoes
        pausa();
// operacoes
}

O que está acontecendo é que o programa aguarda 2 segundos e executa as operações de ambos os métodos. Como é possível aguardar 1 segundo, realizar algumas operações, aguardar outro 1 segundo e finalizar as operações?

Teria que ver a implementação de todas as classes envolvidas.

Você está usando várias Threads para seu jogo?

Tentei usando apenas o código que citei acima e também criando uma classe que herda Thread, e a cada “pausa” eu criava uma nova Thread e utilizava o join(), mas ainda assim o tempo era acumulado.

Sobre as classes, tem alguma informação específica? Porque já tem umas 15 classes com bastante código.

Inicialmente, usar threads para jogos vai te dar muito mais problemas do que vantagens, principalmente por causa da sincronização.

Uma boa abordagem para jogos é usar um loop de jogo (game loop). Este artigo explica a ideia. Neste aqui, há um outro exemplo, em que o tempo entre cada loop é usado na lógica do jogo. Basicamente, a cada volta do laço, você tem uma determinada quantidade de tempo passado (por exemplo, 17 milissegundos). Com isso, você pode realizar ações baseadas no tempo que passou.

Com isso, é possível ter assim:

class Inimigo{
  private float tempoDePausa = 0;
  private float tempoAtacando = 1;
  private boolean atacando = false;

  public void update(float deltaTime){
    if (tempoAtacando > 0.0f) {
      // bla bla bla: aqui, faz o que o ataque precisa

      tempoAtacando -= deltaTime;

      if (tempoAtacando <= 0.0f){
        tempoDePausa = 1; // a partir daqui, o inimigo irá ficar pausado
      }
    }else{
      // aqui ele fica esperando

      tempoDePausa -= deltaTime;

      if (tempoDePausa <= 0.0f){
        tempoAtacando = 1; // a partir daqui, o inimigo irá atacar
      }
    }
  }
}

Daí, na classe que contém o game loop, você faz algo como:

Inimigo orc = new Inimigo();

private void gameLoop(){
   
float delta = 0;

while (gameRunning){
    // resto do código
    delta = tempopassado;// delta recebe o tempo passado entre as voltas do laço
    orc.update(delta);
    // resto do código
}
}

Procure dar uma lida sobre game loop pra entender esse conceito, que é extremamente útil. Muitas bibliotecas e engines inclusive já implementam algo parecido (para o programador não ter que se preocupar com isso).

Abraço.

Estou terminando de ler os artigos, são excelentes e estou aprendendo bastante. Conforme leio, tento novas coisas, mas ainda tenho o mesmo resultado. Talvez faltou esclarecer um pouco do problema:

O jogo é de turnos, onde você seleciona uma ação, executa ela, e o inimigo executa outra (estilo Pokémon de GameBoy).

Tendo essa imagem como referência, eu gostaria de executar a ação de dizer que houve o ataque (1), aguardar 3 segundos para futuramente colocar alguma animação (2), e só depois exibir o dano feito e as demais coisas (3).

O que acontece é que são aguardados os 3 segundos e todas as mensagens chegam de uma vez. Quando coloco o método pausa() entre os métodos que exibem mensagens, todos eles são acumulados (se coloco 4 pausa(), ele aguarda 12 segundos), e só depois as mensagens são exibidas, todas ao mesmo tempo.

Segue o método pausa:

public void pausa(){
    long tempo1 = System.currentTimeMillis();
    long tempo2;
    boolean waiting = true;
    while (waiting){
        tempo2 = System.currentTimeMillis();
        if ( (tempo2 - tempo1)/1000 == 3 ){
            waiting = false;
        }
    }
    diz("Aguardou 3 segundos");
}

Uma novidade:

Agora estou escrevendo as frases do jogo tanto com append na interface gráfica quanto com system out. No segundo caso, está funcionando normalmente, mas no append da JLabel acontece o problema.

Eu usaria uma fila para controlar as ações do jogo:

Exemplo:

List<Acao> acoes;

acoes.add(new VezDoJogador());

interface Acao {
    // retorna se terminou a execucao
    boolean executar(long delta);
}

// ao precionar o botao de ataque
acoes.add(new Ataque());
acoes.add(new AnimacaoDeAtaque());
acoes.add(new VezDoOponente());

// no run
Acao acao = acoes.get(0);
while(acao.executar(delta)) {
    acoes.remove(0);
    acao = acoes.get(0);
}

// AnimacaoDeAtaque

class AnimacaoDeAtaque implements Acao {
    long total = 0;
    boolean executar(long delta) {
        total += delta;
        return total > 3000; // 3 segundos
    }
}

Lembrando que você só deve mexer na interface do swing através da AWT Event Dispatching Thread. Talvez isso esteja causando o atraso na UI, só um palpite.

SwingUtilities.invokeAndWait e SwingUtilities.invokeLater podem ser úteis.

1 curtida

O que o lvbarbosa disse faz sentido. Swing não é thread safe, então alguns eventos acabam tendo uma execução “bagunçada” quando envolvem timers, sleeps e threads. Às vezes é necessário inclusive tentar forçar a atualização de componentes, com coisas como revalidate(), invalidate(), repaint(), …

Uma abordagem que você pode tentar também é o conceito de estados. A ideia é que cada estado represente um momento definido dentro da sua lógica (exemplo: atacando, defendendo, tomando ataque) e haja transições de estado de acordo com cada situação, (exemplo: clique, passados x segundos, etc). Adicione máquinas de estado finitas (finite state machines - FSM) a seus estudos.

Por exemplo:

abstract class State{
public abstract void execute();
}

class AtackState extends State{
  public void execute(){
    System.out.println("Atacando");
  }
}

class WaitState extends State{
public void execute(){
    System.out.println("Aguardando");
    Thread.sleep(5000); // aqui precisa do exception de InterruptedStation, etc
  }
}

É meio complicado explicar em poucas linhas, mas dê uma lida que é um conceito interessante. Inclusive, você pode fazer seu jogo todo baseado em estados (não só as batalhas). Cada estado pode processar o input (teclado, mouse) e exibir uma tela diferente, sem precisar de um monte de ifs. . Exemplo:

  • estados do jogo: andando, batalhando, comprando;
  • estados de batalha: atacando, aguardando, tomando ataque;

Abraço.

Ótima sugestão. Dedicarei umas horas para estudar e inserir estados no jogo. Trago um feedback em breve.