Ponto V!

Home Java Java 2D O loop de animação
Vinícius Godoy de Mendonça
O loop de animaçãoImprimir
Escrito por Vinícius Godoy de Mendonça

Desenhar na tela é muito interessante, mas, a menos que você queira fazer o jogo dos sete erros, não é suficiente. Videogames são, por natureza, animados.

Neste artigo, iremos explicar como controlar um dos mais importantes aspectos do jogo, o MainLoop. Uma boa introdução a ele, é ler o artigo do Paulo V. Radtke chamado “Animação baseada em tempo”. Por incrível que pareça, muitas animações em java aparentam ter flickering, não pela ausência de double buffering, que ocorre transparentemente e por padrão em swing, mas pela ausência de um bom game loop.

O artigo implementa o algoritmo proposto pelo dr. Andrew Davidson, no livro Killer Game Programming in Java. Apenas adicionei uma explicação própria, em português, e o refatorei para ser reutilizável. O algoritmo proposto é muito bom, não vi sentido em altera-lo.

A idéia inicial

Vamos pensar num caso simples. Você tem um mapa e um inimigo. Esse inimigo se move da direita para a esquerda em toda a extensão desse mesmo mapa. Esse efeito pode ser obtido através do seguinte loop:

enquanto (oJogoEstaRodando)
inicio
   calcule a proxima posicao do inimigo com base na atual
   se (o inimigo atingiu a parede)
      inverta a direcao
   senao
      assume a posicao calculada
   fim se
   pinte a situacao de jogo atual
  espere 40 milisegundos
fim

Como um segundo é formado por mil milisegundos, espera-se que esse código irá rodar 25 vezes. O usuário terá a ilusão de ver o inimigo movendo, já que a cada vez que todo o cenário estiver pintado, o inimigo estará um pouco mais deslocado para o lado. Se rodarmos o código, entretanto, perceberemos que a taxa de 25 quadros por segundo nunca é obtida. A taxa final é mais baixa e não é constante. Vejamos o porque:

1. Em primeiro lugar, pintar o fundo e mover inimigo tomam tempo. Esse tempo pode variar de máquina para máquina. Você ainda poderia subtrair o tempo do processamento da quantidade a ser esperada, mas o que acontece se isso for mais do que 40ms?

2. Dificilmente o computador esperará 40 milisegundos, por mais que você ordene. Existe um pequeno erro nos timers que varia de sistema operacional para sistema operacional (por exemplo, o Windows 98 erra em 50ms, o XP em 10ms e o Unix em apenas 1ms).

Como resolver esses problemas?

Separando conceitos

Se analisarmos o pseudo-código da sessão anterior, vamos perceber que o loop é dividido em 3 etapas:

  1. A situação atual é pintada na tela;
  2. A lógica do jogo é calculada;
  3. O sistema espera algum tempo para pintar o próximo quadro;

Chamaremos a pintura e exibição da tela de quadro. A quantidade de vezes que ela ocorre em um segundo é chamada “quadros por segundo” e abreviada como FPS (da sigla em inglês, Frames Per Second). O processo de pintura é um dos mais custosos em termos de processamento. Imagens tem de ser carregadas, redimensionadas, trabalhadas e desenhadas na tela. Essa taxa está diretamente relacionada a percepção do jogador sobre a qualidade gráfica do jogo.

Chamaremos também o calculo da lógica do jogo de “atualização”. Essa fase atualiza apenas os estados do jogo. Ela verificará onde nosso herói está, para onde ele irá se mover, se aquele tiro o atingiu ou não ou se ele matou o dragão que protegia a princesa. Nenhuma pintura é feita aqui, mas a situação seguinte é preparada para ser pintada. A taxa de UPS dá ao jogador a percepção sobre a velocidade do jogo.

Reescrevendo o algoritmo anterior

Para simplificar os próximos conceitos, vamos reescrever o algoritmo anterior, agora em java, da seguinte forma:

while (isRunning) {
   game.processLogics();
   game.renderGraphics();
   sleep(40);
}

A linha game.renderGraphics() é responsável pelo passo “pinte a situação atual do jogo”. É importante que essa etapa faça apenas isso, pintar. Caso esse método seja chamado N vezes, sem uma chamada a update, exatamente a mesma imagem deveria ser pintada N vezes. Isso é um detalhe de implementação importante, e fará diferença caso queiramos aplicar efeitos na tela no futuro.

A linha game.processLogics() fica responsável pelas demais linhas dentro do loop (calcular a próxima posição do inimigo ou decidir se é hora de virar). Já a linha sleep(40) é equivalente “espere 40 milisegundos”.

O método sleep é bastante simples:

public void sleep(int millis) {
    try {
       Thread.sleep(millis);
   } catch (InterruptedException e) {}
}

Uma possibilidade mais elegante é fazer com que a InterruptedException efetivamente aborte o game loop. Isso porque essa exception é disparada pela virtual machine em todas as threads, caso uma chamada a System.exit(0) seja feita.

Velocidade constante

O primeiro passo na busca de uma velocidade de jogo constante é calcular o tempo levado nas fases de desenho e atualização e reduzir isso na fase de espera. Modificando-se isso o novo algoritmo fica:

while (isRunning) {
   long beforeTime = System.currentTimeMillis();
   game.processLogics();
   game.renderGraphics();
   long afterTime = System.currentTimeMillis();
   long sleepTime = afterTime - beforeTime;

   if (sleepTime < 40)
      sleep(40 - sleepTime);

Problema resolvido, certo? Errado. O que acontece se o totalTime for 45 milisegundos? A espera será de 0, mas em 8 quadros teremos demorado 40 milisegundos a mais do que gostaríamos!!! Isso representa um jogo 10% mais lento.

Como contornar esse problema? Basta lembrarmos que quem é lenta no processo é a fase de pintura, game.renderGraphics(), e não a fase de atualização da lógica. Podemos, então, “pular” um quadro quando a demora assim justificar. Para pular um quadro, basta atualizarmos a lógica sem fazermos a pintura. Voltemos ao algoritmo demonstrando como se faz isso:

long excess = 0;
inal long DESIRED_UPDATE_TIME = 40;while (isRunning) {
   long beforeTime = System.currentTimeMillis();
   //Pula os quadros enquanto o tempo for em excesso.
   while (excess > DESIRED_UPDATE_TIME) {
       game.processLogics();
       excess -= DESIRED_UPDATE_TIME;
   }
   game.processLogics();
   game.renderGraphics();
   long afterTime = System.currentTimeMillis();
   long totalTime = afterTime - beforeTime;

   if (totalTime < DESIRED_UPDATE_TIME)
      sleep(DESIRED_UPDATE_TIME - totalTime);
   else
     excess += totalTime - DESIRED_UPDATE_TIME;
}

Note que o pulo dos quadros é uma série de fases de atualização especiais. Portanto, seu tempo também deve ser considerado. Essa é a razão pela qual o while está no início (e após a obtenção de beforeTime) e não no final, como poderia parecer mais lógico.

O que esse algoritmo garante? Que a velocidade do jogo (UPS), vai ser manter relativamente constante, mesmo que a qualidade gráfica do jogo (FPS) varie.

Isso contorna o nosso primeiro problema. Entretanto, se você executar o jogo numa máquina lenta, notará que agora o java travou!!! Por que? Quando o tempo de desenho é excessivamente alto, não deixarmos a thread dormir nenhuma vez. Assim, nenhuma outra thread do java tem a chance de processar. A AWT não poderá reportar os eventos de teclado ou mouse. O ideal é forçarmos a aplicação a abrir mão da thread do jogo caso um número de saltos de quadro específico tenha sido atingido.

long excess = 0;
long noDelays = 0;

final long DESIRED_UPDATE_TIME = 40;
final long NO_DELAYS_PER_YIELD = 16;while (isRunning) {
   long beforeTime = System.currentTimeMillis();

   //Pula os quadros enquanto o tempo for em excesso.

   while (excess > DESIRED_UPDATE_TIME) {
       game.processLogics();
       excess -= DESIRED_UPDATE_TIME;
   }

   game.processLogics();
   game.renderGraphics();

   long afterTime = System.currentTimeMillis();
   long totalTime = afterTime - beforeTime;

   if (totalTime < DESIRED_UPDATE_TIME) {
        sleep(DESIRED_UPDATE_TIME - totalTime);
        noDelays = 0;
   }
   else {
      excess += totalTime - DESIRED_UPDATE_TIME;

      if (++noDelays == NO_DELAYS_PER_YIELD)
            Thread.yield();
    }
}

Resolvendo imprecisões do timer

Se você rodar esse algoritmo notará resultados bem melhores: a taxa gráfica varia, mas os UPSs sao quase constantes. Entretanto, ainda existe uma variação indesejada na atualização. Ela ocorre pois, conforme já explicado, o sleep() contém imprecisões. Podemos reduzir consideravelmente essa imprecisão calculando o tempo total de sleep, mais ou menos como fizemos para a fase de paint e update. Abaixo segue o código fazendo isso:

long excess = 0;
long noDelays = 0;
final long DESIRED_UPDATE_TIME = 40;
final long NO_DELAYS_PER_YIELD = 16;

long overSleepTime = 0;while (isRunning) {
   long beforeTime = System.currentTimeMillis();

   //Pula os quadros enquanto o tempo for em excesso.

   while (excess > DESIRED_UPDATE_TIME) {
        game.processLogics();
        excess -= DESIRED_UPDATE_TIME;
    }

   game.processLogics();
   game.renderGraphics();

   long afterTime = System.currentTimeMillis();
   long totalTime = afterTime - beforeTime;

   if (totalTime < DESIRED_UPDATE_TIME) {
        sleep(DESIRED_UPDATE_TIME - totalTime - overSleepTime);
        long afterSleepTime = System.currentTimeMillis();
        overSleepTime = afterSleepTime - afterTime;
        noDelays = 0;
    }
    else {
        overSleepTime = 0;
        excess += totalTime - DESIRED_UPDATE_TIME;
        if (++noDelays == NO_DELAYS_PER_YIELD)
            Thread.yield();
    }
}

Agora sim! Nosso algoritmo está pronto.

Retoques finais

Se você estiver usando o Java 5, pode utilizar o timer em nano segundos. Também é uma boa idéia separar o algoritmo numa outra classe, tornando tanto DESIRED_UPDATE_TIME, quanto NO_DELAYS_PER_YIELD configuráveis. É recomendável controlar com um contador o número máximo de quadros que podem ser pulados, isso evita que máquinas muito lentas percam atualizações importantes de tela. Finalmente, o código pode ser refatorado para ficar mais claro. Abaixo, segue a classe pronta, com todas essas modificações. A classe também implementa Runnable. Assim, fica fácil disparar o game numa Thread separada.

public class MainLoop implements Runnable
{
    public static final int DEFAULT_UPS = 80;
    public static final int DEFAULT_NO_DELAYS_PER_YIELD = 16;
    public static final int DEFAULT_MAX_FRAME_SKIPS = 5;

    private LoopSteps game;
    private long desiredUpdateTime;
    private boolean running;

    private long afterTime;
    private long beforeTime = System.currentTimeMillis();

    private long overSleepTime = 0;
    private long excessTime = 0;

    private int noDelaysPerYield = DEFAULT_NO_DELAYS_PER_YIELD;
    private int maxFrameSkips = DEFAULT_MAX_FRAME_SKIPS;

    int noDelays = 0;
    public MainLoop(LoopSteps loopSteps,
            int ups, int maxFrameSkips, int noDelaysPerYield) {
        super();
        if (ups < 1)
            throw new IllegalArgumentException("You must display at least one frame per second!");

        if (ups > 1000)
            ups = 1000;

        this.game = loopSteps;
        this.desiredUpdateTime = 1000000000L / ups;
        this.running = true;
        this.maxFrameSkips = maxFrameSkips;
        this.noDelaysPerYield = noDelaysPerYield;
    }

    public MainLoop(LoopSteps loopSteps, int ups) {
        this(loopSteps, ups, DEFAULT_MAX_FRAME_SKIPS, DEFAULT_NO_DELAYS_PER_YIELD);
    }

    public MainLoop(LoopSteps loopSteps) {
        this(loopSteps, DEFAULT_UPS);
    }

    private void sleep(long nanos) {
        try {
            noDelays = 0;
            long beforeSleep = System.nanoTime();
            Thread.sleep(nanos / 1000000L, (int) (nanos % 1000000L));
            overSleepTime = System.nanoTime() - beforeSleep - nanos;
        } catch (Exception e) {}
    }

    private void yieldIfNeed() {
        if (++noDelays == noDelaysPerYield) {
            Thread.yield();
            noDelays = 0;
        }
    }

    private long calculateSleepTime() {
        return desiredUpdateTime - (afterTime - beforeTime) - overSleepTime;
    }

    public void run() {
        running = true;

        try {
            game.setup();
            while (running) {
                beforeTime = System.nanoTime();
                skipFramesInExcessTime();

                // Updates, renders and paint the screen
                game.processLogics();
                game.renderGraphics();
                game.paintScreen();

                afterTime = System.nanoTime();
                long sleepTime = calculateSleepTime();
                if (sleepTime >= 0)
                   sleep(sleepTime);
               else {
                   excessTime -= sleepTime; // Sleep time is negative
                   overSleepTime = 0L;
                   yieldIfNeed();
               }
           }
       } catch (Exception e) {
            e.printStackTrace();
       } finally {
           running = false;
           game.tearDown();
           System.exit(0);
       }
    }

    private void skipFramesInExcessTime() {
        int skips = 0;
        while ((excessTime > desiredUpdateTime) && (skips < maxFrameSkips)) {
            excessTime -= desiredUpdateTime;
            game.processLogics();
            skips++;
        }
    }

    public void stop() {
        running = false;
    }
}

Através dessa interface, qualquer futura aplicação pode se beneficiar do código acima. Por questão de conveniência o método renderGraphics() foi separado em dois métodos separados, renderGraphics() e paintScreen(). Isso ajuda a tirar profiling da aplicação. Também foi incluído um método, que será chamado quando o loop iniciar e terminar. Isso garante que a terminação do jogo seja executada na mesma Thread do GameLoop.

public interface LoopSteps {
    void setup();
    void processLogics();
    void renderGraphics();
    void paintScreen();
    void tearDown();
}

Concluindo

Bastante coisa, não? Nos próximo artigos, vamos ver como utilizar essa arquitetura na prática. Você pode baixar o código fonte de ambas as classes clicando aqui.


Comentários (39)
  • Marco Biscaro  - Belo texto!
    avatar

    Bem legal Vinícius.

    Aliás, todos do site estão de parabéns: todos com belos artigos. Continuem assim!

    Apesar de eu já ter lido o texto original, sempre é bom uma "revisão". E em português acaba sendo mais simples. Ajudou no meu game loop!

    PS: Em "Separando conceitos", faltou dizer o que é a taxa de UPS. Outra coisa: nos seus primeiros trechos de exemplo o "paintGraphics()" apareceu antes do "updateLogics()" no loop. Não faria mais sentido fazer o contrário?

  • Vinícius Godoy de Mendonça
    avatar

    Sim, bem observado. Vou corrigir ali em cima.

  • Vitor Almeida da Silva  - Parabéns
    avatar

    Excelente texto Vinícius.

    O capítulo original do livro já era bom, mas com a explicação em português fica ainda mais acessível.

    O bacana deste game loop é que com poucas alterações pode ser utilizado facilmente em jogos para celulares (J2ME).

    Aliás, a equipe do pontoV está de parabéns com ótimos artigos.

  • Vinícius Godoy de Mendonça  - Obrigado.
    avatar

    Obrigado. Eu deixei o código final beem mais organizado que o do livro. Achei o do autor muito disperso. Corrigi alguns bugs e usei os timers com precisão de nanossegundos também.

  • David Buzatto  - Muito bom!
    avatar

    Muito legal Vinícius! Parabéns pelo artigo! Na época do meu TCC dei uma olhada no livro do Andrew, mas acabei ficando com o do Brackeen. Realmente o main loop no Andrew é bem mais completo. Agora uma pergunta: um GameStep seria uma fase do jogo?

    []´s

  • Vinícius Godoy de Mendonça  - GameSteps
    avatar

    Na verdade, vai o jogo inteiro lá dentro.

    Mas é claro, você pode fazer uma fase ser um GameStep também (e eu geralmente faço isso).

  • Eduardo Skrepnek Tosin  - Muito bom!!
    avatar

    Achei muito bom essa aula sobre game loop. Me ajudou muito com duvidas quanto ao FPS e UPS. Muito obrigado Vinicius.

    P.S.: No ultimo código, o nome da interface é LoopSteps e não GameSteps (no texto, pois nos arquivos está certo).

  • Vinícius Godoy de Mendonça
    avatar

    Obrigado pela correção, já arrumei no artigo. :)

  • Dyorgio  - Multi Thread
    avatar

    Muito bom esse "game core"
    você poderia melhora-lo mais com a adição de game pauses (quando se deseja liberar a maquina do usuario por um tempo, minimizar a janela por exemplo).
    E sei que é muito dificil, mais terias algo de multi thread que rode rapido? hoje faço esses games em java e noto apenas 1 cpu em 100% e a outra parada :(

  • Vinícius Godoy de Mendonça
    avatar

    É muito difícil você ter o "core" do game dividido em duas threads/processadores, e garantir justiça.

    Seria possível dividir tarefas secundárias para um outro núcleo de processamento, como a carga do cenário, por exemplo.

    É bastante fácil modificar esse algoritmo de game loop para suportar pausas. Estou pensando em fazer uma versão mais avançada dele para a próxima versão da JGF que inclui poder programar múltiplos pontos de update. ao invés de um só, cada um com sua por segundo (por exemplo, programar para chamar processamento de IA a cada 1 miinuto, da lógica do jogo a cada update, e da física a cada 10 segundos)...

    Vou aproveitar e seguir a sua sugestão, inserindo também a possibilidade de pausa.

  • Dyorgio  - Pausas
    avatar

    Pois é, mencionei as pausas pois quando fui implementar acabei me enrolando todo,
    o ponto ai não é simplesmente pausar e depois de 1 minuto de atrasdo ficar 1 minuto soh fazendo logica, essa é a parte facil.
    O dificil é quando você faz algumas logicas baseadas na passagem do tempo e outras não.
    Não consegui trabalhar com esses 2 modos ao mesmo tempo ( baseado em tempo, baseado em interações) com o pause.

  • Vinícius Godoy de Mendonça
    avatar

    hummmm... em artigos futuros volto a trazer esse assunto, então. O ideal é que o próprio GameLoop calcule o intervalo entre loops, e despreze o tempo de pausa ao restaurar o jogo. Assim, sua lógica pode se basear nessa diferença e nunca sequer saberá que o jogo foi mesmo pausado.

  • Vitor Almeida da Silva  - re: Isso mesmo
    avatar

    Exato Vinícius.

    Programação em várias threads é complicado.
    Em um dos jogos que eu desenvolvi para a MyPlay (www.myplaymobile.com/games/ijig) eu fiz isso que você sugeriu: dividi o carregamento do cenário em outra thread secundária (a divisão das threads em si é fácil utilizando-se apis como posix threads, mas os "pequenos" problemas que aparecem são horríveis: compartilhamento de contexto opengl, falta de sincronização entre variáveis, etc).

    Resumindo: eu não recomendo a não ser que seja realmente necessário :)

    A sugestão da pausa é uma boa ideia.

    Vinícius Godoy de Mendonça Escreveu:
    É muito difícil você ter o "core" do game dividido em duas threads/processadores, e garantir justiça.

    Seria possível dividir tarefas secundárias para um outro núcleo de processamento, como a carga do cenário, por exemplo.

    É bastante fácil modificar esse algoritmo de game loop para suportar pausas. Estou pensando em fazer uma versão mais avançada dele para a próxima versão da JGF que inclui poder programar múltiplos pontos de update. ao invés de um só, cada um com sua por segundo (por exemplo, programar para chamar processamento de IA a cada 1 miinuto, da lógica do jogo a cada update, e da física a cada 10 segundos)...

    Vou aproveitar e seguir a sua sugestão, inserindo também a possibilidade de pausa.
  • Vinícius Godoy de Mendonça
    avatar

    O que eu quis dizer com carregamento do cenário, seria ler do disco para a memória. Isso evitaria aqueles tempos de loading, que jogos um pouco mais antigos abusavam.

    A tarefa de carregar da memória para o vídeo ainda ficaria com a thread principal, na etapa de render. Como elas nunca vão chegar a compartilhar esse dado (já que, a principio, o cenário já deve estar completamente carregado quando o jogador chegar lá), não há bloqueios por sincronização.

    Uma thread secundária pode ser usada para processar IA estratégica também, já que ela envolve decisões de mais longo prazo, geralmente sobre informações consolidadas que não vão mudar só pq um ou outro agente moveu-se poucos centímetros. Isso é prático, pois essa thread pode trabalhar com a cópia de um dos estados do jogo, e deixar que o estado do jogo seja alterado enquanto processa.

  • Vinícius Godoy de Mendonça
    avatar

    Um dos grandes problemas envolvendo multi-thread é justamente o stall que dados compartilhados podem causar, no momento em que há uma região crítica (no caso do java, criada a partir dos blocos synchronized). A cópia dos dados, nesse caso, minimiza esse problema.

    Ter duas threads processando sobre o loop principal porque você acabará tendo que esperar as duas threads se alcançarem para poder fazer o render. Por isso a recomendação delegar a cada thread tarefas que sejam menos interdependentes e possam maximizar o paralelismo. Minimizar o compartilhamento de dados é uma das melhores formas de atingir esse objetivo.

  • Dyorgio  - como?
    avatar

    Pensei por um momento nessa ideia de clonar todo o estado do jogo, e mandar renderizar em uma thread especifica pra isso, criando uma especie de "pilha de estados", para o processo do render se virar....mas sera que vale apena? esse clone sera mais rapido do que apenas 1 thread fazer todo o trabalho?

    A pergunta que não quer calar...
    Como aproveitar 9 nucleos do PS3?
    lembrando que eu nao estou mais no campo Java, e sim na area da multi-task em jogos.

  • Vinícius Godoy de Mendonça
    avatar

    Essa, na verdade, tem sido a grande reclamação do PS3. Do que adianta zilhões de núcleos, se gerar um comportamento único e síncrono (como cedo ou tarde vai ter que ser), é um inferno?

  • Bruno Crivelari Sanches
    avatar

    A questão de duplicar o estado é bem usado, se não me engano no idtech5 funciona assim para objetos do jogo, eles possuem dados básico como posição, orientação, etc duplicados. Dai todo frame um conjunto é usado para updates e o outro para consulta.

    O PS3 tem sido um grande desafio, pois inicialmente nos jogos é muito mais simples se dividir o código em grandes blocos que rodam em paralelo (por causa do código / engines existentes). Isso funciona bem no pc e no 360, já o ps3 a abordagem tem que ser outra...

    E código paralelo é atualmente o grande desafio dos jogos (e da ciência da comp.)...

  • Vinícius Godoy de Mendonça
    avatar

    No caso do processamento estratégico, é um processamento intenso. Só vale a pena pq esse processamento ocupa vários loops do jogo, não um só. O mesmo vale para a carga de cenário do disco.

  • Eduardo Skrepnek Tosin  - Utilização do MainLoop
    avatar

    Vinícius, consegui utilizar a classe MainLoop e uma implementação de LoopSteps com tranquilidade em um aplicativo normal utilizando como base um JFrame (créditos já estão nos comentários dos arquivos MainLoop e LoopSteps :) )
    mas não consegui implementá-los em um JApplet.
    O que eu consegui descobrir até agora é que a Thread do MainLoop roda normalmente, mas quando eu chamando o método repaint() do JApplet ele parece ser ignorado. Não é chamado o update(Graphics g) nem o paint(Graphics g) do JApplet.
    Não sei se isto é suficiente para que possas me ajudar. Se for preciso eu lhe envio o código-fonte.

    Obrigado desde já.

  • Vinícius Godoy de Mendonça  - Applets
    avatar

    O problema é que o Swing é autorizado a ignorar comandos de repaint() seguidos. Isso se chama "event coalescing". Veja bem, para uma aplicação de botões, normal, não faz sentido ter uma taxa de frames por segundo tão alta quanto à dos jogos.

    Para contornar o problema, você deve criar uma classe filha de Canvas e incluí-la no Applet. Nela, você obtém um BufferStrategy e então utiliza ele para pinta-lo na tela. Vou explicar mais sobre esse técnica no meu post, esse domingo. Até lá, você pode conferir esse tutorial do Coke and Code:

    http://www.cokeandcode.com/node/6

  • Bruno  - game.processLogics();
    avatar

    No começo do artigo, no código você escreve game.processLogics(); e na descrição logo abaixo você cita game.updateLogics();
    Não são a mesma?

  • Vinícius Godoy de Mendonça
    avatar

    É a mesma coisa sim, bem observado.

  • Flávio Henrique Alves  - e se separassse lógica e repaint em threads separa
    avatar

    bom... a começar fico bastante grato por disponibilizar assim o tutorial e dou os parabens pela qualidade do mesmo assim como os anteriores, nunca desenvovli games, não tive quase que contato nenhum com java 2D no passado (só usei para imprimir texto no passado, coisa bem básica e a bastante tempo, não lembrava nada), e mesmo assim intendi o que foi dito... mas ainda me resta uma dúvida. Não se poderia ter dois main loops, um para processar a lógica do jogo e outro para atualizar a tela? Eu li os comentários, inclusive o que você falou sobre "garantir justiça entre as threads", mas de repente poderiamos assim aproveitar um pouco melhor multiplos nucleos, controlar separadamente tempo de espera entre atualização da tela e processamento por exemplo, se uma thread estivesse executando muito mais que a outra ela poderia aumentar o tempo da outra, ou diminuir se fosse esse o caso...

    quais seriam as desvantagens dessa abordagem?

  • Vinícius Godoy de Mendonça  - Isso não é necessário
    avatar

    Na verdade, isso dificilmente é necessário. A maior parte das bibliotecas de pintura (inclusive o Java 2D) já faz isso implicitamente. A razão é que a pintura roda num hardware separado, a placa de vídeo.

    Você vai notar que na maior parte das bibliotecas, inclusive a Java2D, as funções de pintura retornam imediatamente. Isso porque elas apenas dão um comando ao hardware e, só durante a troca dos buffers (que também retorna imediatamente) a placa de vídeo começará seu trabalho. Durante esse trabalho, sua thread do game loop roda atualizando o estado do jogo e empilhando novos comandos.

    Os vários buffers facilitam esse processo. Enquanto a placa está trabalhando no desenho do front buffer, sua aplicação está processamento o próximo quadro no back buffer.

    Se você quer aproveitar os múltiplos núcleos, você pode passar para threads secundárias outros tipos de processamento:
    - Processamento de arquivos (cargas de terrenos, salvamentos de jogo, etc);
    - IA (códigos da IA mais pesados, que não exigem resposta instantânea podem ser movidos para outras threads);
    - Geração de terrenos ou texturas, otimização de meshes, etc;
    Enfim, qualquer tipo de processamento secundário.

    A justiça só é realmente importante quando você está colocando lado-a-lado inimigos e jogadores. Se threads diferentes cuidam deles, você pode acabar favorecendo um acidentalmente.

  • Allan
    avatar

    olá,
    Estou tendo problemas para montar meu loop principal.

    Quero fazer um simples jogo em da galinha que atravessa a rua com carros passando.

    a galinha deve se mover apenas no eixo y, e os carros apenas no eixo x.

    a galinha terá uma velocidades constante, enquanto os carros terão diferentes velocidades de acordo com o modelo.

    atualmente estou incrementando 1px a cada keyPress mas fica muito lento e se eu aumentar a quantidade de px nota-se claramente o salto que a figura dá de uma posição para a outra.

    uma Thread fica responsável por fazer o repaint do frame.

    alguma dica de como eu poderia fazer esse loop

  • Vinícius Godoy de Mendonça
    avatar

    Lembre-se que a fórmula física de deslocamento é:
    deslocamento = velocidade * tempo;

    É necessário calcular quantos ms transcorreram entre duas chamadas do update. Depois disso, use esse tempo, passando para segundos, e transforme sua velocidade em pixels * segundos.

    Basta fazer:
    galinha.y += velocidade * tempoEmSegundos;

  • Fernando França  - Dúvida
    avatar

    Olá boa tarde!
    como disse para Marcos Vasconcelos no seu post sobre o jogo de ping pong, graças a vcs que eu venho aprendendo muito nessa jornada. Parabéns pelo post!!!
    Eu sou iniciante e estou tentando fazer um aplicativo no android que marca batidas por minuto. Seria uma coisa simples porém que exige muita precisão no tempo. Pergunta:
    Eu importo essa classe e essa interface no meu projeto, mais eu não consigo ultilizar... (típico de quem naum manja mesmo);
    Eu fiz o seguinte:
    public class meuApp extends Activity implements LoopSteps {
    /** Called when the activity is first created. */

    private TextView cpteste;
    private int contador=0;

    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    cpteste = (TextView) findViewById(R.id.campoText);

    }

    //ai vem os métodos da interface que você fez:
    public void setup() {
    //Este evento ocorre antes da primeira iteração do ciclo, e apenas uma vez.
    // TODO Auto-generated method stub
    cpteste.setText("teste";);

    }

    public void processLogics() {
    /**
    * O método processLogics atualiza todos os estados de objeto do jogo. O jogo será
    * Processá-lo do modelo de dados e definir a situação do próximo jogo que será
    * Pintado sobre a tela.
    */
    // TODO Auto-generated method stub
    contador++;



    }

    public void renderGraphics() {
    /**
    * Processa o jogo seguinte situação real em um buffer, para a pintura futuro.
    */
    // TODO Auto-generated method stub

    }

    public void paintScreen() {
    /**
    * Pinta os gráficos prestados na tela.
    */
    // TODO Auto-generated method stub
    cpteste.setText("teste "+contador);

    }

    public void tearDown() {
    /**
    * Chamado quando o loop do jogo terminado.
    */
    // TODO Auto-generated method stub

    }

    }


    Você pode me dizer onde estou errando?

    se vc puder me ajudar mais uma vez te agradeço!!!


    abraço;

  • Ricardo Lima
    avatar

    Olá novamente, muito bem explicado esse artigo, mas pra mim ainda é um pouco complicado entende-lo 100% de primeira leitura, principalmente por que gosto de analisar os códigos e entender como está funcionando, mas nada que não se ajeite ao se re-ler algumas vezes.

    Aqui nessa parte quanto ao método renderGraphics():
    "Caso esse método seja chamado N vezes, sem uma chamada a update, exatamente a mesma imagem deveria ser pintada 5 vezes."

    Creio que aquele 5 citado ali também deveria ser um N, como o método renderGraphics() realiza somente a pintura do jogo, se não houver atualizações por parte do processLogics() entao a mesma imagem deve ser pintada N vezes.

    Seria isso? =)

  • Vinícius Godoy de Mendonça
    avatar

    Exatamente, obrigado pela correção!

  • Diego  - Duvida
    avatar

    Muito bom e interessante o artigo :)

    Uma duvida, na linha onde esta:
    overSleepTime = afterSleepTime - afterTime;

    Não deveria subtrair os 40 mili segundos do sleep normal nesta formula ?
    ficando algo como:

    overSleepTime = afterSleepTime - afterTime - defaultSleep;

    Pois pelo que entendi, esta subtraindo o momento após o sleep de um momento anterior ao sleep, que será sempre de no mínimo 40 mili, então o excesso seria o que passasse de 40 mili segundos. Ou me enganei e estou deixando algo de lado?

  • ViniGodoy
    avatar

    Você tem razão. Observe que no código final estava certo:
    overSleepTime = System.nanoTime() - beforeSleep - nanos;

    Onde nanos era o defaultSleep do código anterior.

  • Matheus Augusto  - Muito bom!
    avatar

    :woohoo: Obrigado cara, ajudou bastante nas minhas necessidades, você é o cara!

  • lucas
    avatar

    como escrevo o algoritimo no netbens estou aprendendo .
    manda a resposta pro meu email mllucasboo@gmail.com
    ou o codigo pronto é mais facil ai estudo ele

  • Anônimo
    avatar

    Como assim cara? :pinch:

  • Antônio Diego  - Idioma
    avatar

    Não seria melhor escrever o código em Português ?

Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2  

Busca

Linguagens

Twitter