GameLoop com taxa constante de pulsos – parte 2 de 2

placeholderantigo

GameLoopNo artigo anterior vimos o game loop em sua forma mais simples, que tem o problema de ter a taxa de pulsos e frames presas uma a outra. Isso faz o jogo correr mais lentamente em CPUs lentas e acelerar em CPU mais poderosas, impedindo uma velocidade estável em todas plataformas. Neste artigo veremos como criar um game loop que garanta uma velocidade fixa para o jogo.

Aprendemos, no post anterior, a diferença entre o pulso e o frame, sendo o primeiro relacionado à lógica do jogo e o segundo à atualização da tela. Tinhamos uma classe Main que criava um objeto do tipo JogoLoopSimples. Vamos modificar essa classe para usar agora um objeto do tipo JogoPulsoFixo.

Arquivo Main.java

001package abrindoojogo.exemplos.gameloop;
002
003public class Main
004{
005    public static void main(String[] args)
006    {
007        JogoPulsoFixo jogo = new JogoPulsoFixo();
008        jogo.gameloop();
009    }
010}

O game loop da classe JogoLoopSimples era assim (para relembrar):

001public void gameloop()
002    {
003        initialize();
004        while (true)
005        {
006            Thread.yield();
007            update();
008            render();
009        }
010    }

Uma chamada a update() e uma a render(), acopladas. Se render demora, update também demora e o jogo fica lento. Bem, vamos partir do seguinte pressuposto: é possível “aturar” uma taxa de frames baixa, mas uma taxa de pulsos baixa modifica a jogabilidade.

Imagine um jogo onde uma nave se move um pixel por pulso. Se temos 60 pulsos por segundo, ao final de dois segundos a nave terá se movido 120 pixels. Mas se a taxa cair para 10 pulsos por segundo, em dois segundos a nave vai se mover apenas 20 pixels. E mais: se o computador for muito rápido, a taxa pode subir a 100 pulsos por segundo, e a nave vai andar 200 pixels em dois segundos. A jogabilidade é totalmente afetada.

A solução que apresento a seguir tenta manter a taxa de pulsos fixa, em detrimento da taxa de frames. Ou seja, se a máquina é lenta, priorizamos os pulsos e geramos os frames conforme der. Sempre teremos 60 pulsos por segundo, embora a taxa de frames possa cair para 10.

Isso causa o seguinte efeito: a nave vai se mover sempre 120 pixels em dois segundos, independente da velocidade da máquina. Mas se a máquina for lenta, a tela estará sendo atualizada apenas a 10 frames por segundo – o jogador vai ver a imagem quadro a quadro, mas a jogabilidade não fica tão comprometida, porque não há mudança na velocidade da ação.

E se a máquina for muito rápida, ainda assim ficaremos apenas com 60 pulsos por segundo, embora possamos estar obtendo 100 frames. A atualização da tela será bem rápida, o que é bom, e a nave continua se movendo na velocidade esperada.

Eu mostro abaixo a nova classe, chamada JogoPulsoFixo. Ela é derivada da classe do artigo anterior, de forma que tem tudo igual a ela. A lógica do jogo é exatamente a mesma. Só sobreescrevi o método gameloop, para modificá-lo.

Arquivo JogoPulsoFixo.java

001package abrindoojogo.exemplos.gameloop;
002
003public class JogoPulsoFixo extends JogoLoopSimples
004{
005    long PULSOS_DESEJADOS_POR_SEGUNDO = 60;
006    double NANOS_ESPERADOS_POR_PULSO = Contador.NANOS_EM_UM_SEGUNDO / PULSOS_DESEJADOS_POR_SEGUNDO;
007
008    @Override
009    public void gameloop()
010    {
011        initialize();
012        long nanoTimeDoProximoPulso = System.nanoTime();
013        while (true)
014        {
015            Thread.yield();
016            while (System.nanoTime() > nanoTimeDoProximoPulso)
017            {
018                update();
019                nanoTimeDoProximoPulso += NANOS_ESPERADOS_POR_PULSO;
020            }
021            render();
022        }
023    }
024}

A modificação (marcada em vermelho) é simples em termos de código, mas pode ser difícil de entender como funciona. Por isso vou explicar bem detalhadamente.

Nas linhas 005, 006 são declaradas constantes. PULSOS_DESEJADOS_POR_SEGUNDO armazena a quantidade de pulsos que desejamos que o game rode. Nesse caso é 60. Não é necessário mais do que isso para a lógica de qualquer jogo. Alguns rodam a 30 ou 25.

NANOS_ESPERADOS_POR_PULSO armazena a quantidade de nanosegundos que cada pulso deve durar. Calcular isso é fácil: se queremos 60 pulsos por segundo, basta pegar a quantidade total de nanosegundos que cabem em um segundo (1e9 ou 1.000.000.000) e dividir por sessenta pulsos. Cada pulso vai durar, nesse caso, 16.666.666 nanosegundos. Em geral vai durar menos, mas queremos ter um pulso a cada 16.666.666 nanosegundos, de forma a constituir 60 por segundo.

Agora veja o game loop. A grande mudança é que temos um outro laço dentro dele, para chamar repetidamente update(). Funciona assim: inicializamos uma variável chamada nanoTimeDoProximoPulso com o valor no nanosegundo atual. Em seguida entramos no game loop e já caimos no loop do update. É feita a verificação para ver se o nanosegundo atual é maior do que o nanoTimeDoProximoPulso, o que certamente não será, já que acabamos de atribuir o tempo atual a esta variável.

Pois bem. Entramos no loop do update(), chamamos esta rotina e depois adicionamos ao nanoTimeDoProximoPulso a quantidade de nanos esperados em um pulso. Isso nos dá o momento no tempo quando deverá ser executado o próximo pulso. Não executaremos ele antes desse tempo. Por outro lado, passar muito desse tempo, executaremos ele mais de uma vez até recuperar o atraso.

Vamos analisar um exemplo volta a volta do game loop. Respire fundo antes de continuar lendo.

  1. Vamos assumir que tudo iniciou com o nanoTime = 0 (zero). Executamos update(), que demora 5.000.000 nanosegundos e somamos 16.666.666 na variável nanoTimeDoProximoPulso. Isso quer dizer que o próximo pulso é esperado para ocorrer nesse tempo.
  2. Como update() demorou apenas 5.000.000 nanosegundos, ainda não chegamos ao 16.666.666 e saimos do loop do update. É executado em seguida o render(), que demora mais 5.000.000 nanosegundos e voltamos ao início do game loop.
  3. Nova verificação do loop do update. O tempo atual é maior do que 16.666.666? Já que update() levou 5.000.000 e render() mais 5.000.000, estamos em 10.000.000 nanosegundos, que é menor. Não entramos no loop do update, ou seja, desta vez o update() não será executado, porque ainda não está na hora esperada. Executamos o render(), que leva mais 5.000.000 nanosegundos e voltamos ao início do game loop.
  4. Agora já estamos em 15.000.000, mas ainda é menor do que 16.666.666, então pulamos novamente o loop do update e vamos direto para o render. Mais 5.000.000 nanosegundos e voltamos ao início.
  5. Agora sim, estamos em 20.000.000 nanosegundos e isso é maior do que o esperado para o próximo pulso, que deveria ter ocorrido em 16.666.666. Estamos atrasados, mas tudo bem – agora entramos no loop do update, executamos ele (mais 5.000.000) e somamos mais 16.666.666 na variável nanoTimeDoProximoPulso. Isso quer dizer que agora vamos esperar até o tempo chegar em 33.333.332 para executar update() novamente.

Já deu para notar que update está sendo limitado. Praticamente temos três render() para cada update(). Essa é uma máquina rápida. Vamos refazer a análise em uma máquina lenta.

  1. Tudo inicia no nanoTime = 0 (zero). Executamos update(), que agora demora 10.000.000 nanosegundos e somamos 16.666.666 na variável nanoTimeDoProximoPulso.
  2. Como update() demorou apenas 9.000.000 nanosegundos, ainda não chegamos ao 16.666.666 e saimos do loop do update. É executado em seguida o render(), que demora 15.000.000 nanosegundos (placa de vídeo lenta) e voltamos ao início do game loop.
  3. Nova verificação do loop do update e o tempo atual é 25.000.000, ou seja, maior que o 16.666.666 esperado para o próximo pulso. Assim, entramos no loop do update e executamos ele. Mais 10.000.000 se passam e estamos então há 35.000.000 nanosegundos de jogo. Acrescentamos novamente 16.666.666 à variável, ficando com 33.333.332, que seria a hora do próximo pulso.
  4. Mas veja: já estamos em 35.000.000, ou seja, já passou a hora do segundo pulso. Então ficamos dentro do loop do update (porque 35.000.000 é maior que 33.333.332) e vamos executar update mais uma vez. Se passam mais 10.000.000 e estamos em 45.000.000. Somamos novamente 16.666.666 na variável e o próximo pulso fica então esperado para 49.999.998.
  5. Ok, desta vez o tempo atual (45.000.000) é menor do que o esperado para o próximo pulso (49.999.998). Então pulamos o loop do update e vamos para o render(). Executamos ele e, devido aos seus 15.000.000 ficamos em 60.000.000 e voltamos ao início.

Já deu para ver que vamos entrar novamente no loop do update porque estamos bem atrasados. Vamos acabar executando o update duas vezes novamente, ou seja, nessa máquina o update executa o dobro de vezes que o render.

Caramba, que explicação cheia de volta… Entendeu? Espero que sim. Caso contrário, leia novamente o exemplo. Experimente colocar o comando contador.sleep(X), onde X é um número de milisegundos, dentro das rotinas update() e render() no arquivo JogoLoopSimples. Isso vai simular rotinas demoradas. Use valores como 50 ou 100 milisegundos.

Esse game loop mantém a taxa de pulsos fixa (na verdade pode oscilar entre 60 e 62) e pode ser utilizado para a criação de games reais. Mas ele apresenta um problema com máquina muitissimo lentas: pode ser que o update() precise rodar 500 vezes para cada render(). Ou mais! Ou ainda, ficar rodando sem nunca chegar na taxa desejada, e assim nunca executar o update().

Alguma dica de como resolver isso? Se você achar uma solução envie para mim (nornberg no gmail). Depois publico um post citando as soluções que aparecerem.

Baixe o código fonte deste artigo.

<pre><span style=”font-family:Monospaced,monospace;color:#000000″><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>001</span><span style=”color:#0000e6;”>package</span> abrindoojogo.exemplos.gameloop;<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>002</span><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>003</span></span><span style=”color:#0000e6;”>public</span> <span style=”color:#0000e6;”>class</span> JogoPulsoFixo <span style=”color:#0000e6;”>extends</span> JogoLoopSimples<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>004</span></span>{<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>005</span>&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>long</span> PULSOS_DESEJADOS_POR_SEGUNDO = <span style=”color:#000000;”>60</span>;<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>006</span>&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>double</span> NANOS_ESPERADOS_POR_PULSO = Contador.NANOS_EM_UM_SEGUNDO / PULSOS_DESEJADOS_POR_SEGUNDO;<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>007</span>&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>long</span> MAX_FRAMES_PARA_PULAR = <span style=”color:#000000;”>2</span>;<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>008</span><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>009</span>&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>public</span> <span style=”color:#0000e6;”>void</span> gameloop()<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>010</span>&nbsp;&nbsp;&nbsp;&nbsp;</span>{<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>011</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>initialize();<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>012</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>long</span> nanoTimeDoProximoPulso = System.nanoTime();<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>013</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>while</span> (<span style=”color:#0000e6;”>true</span>)<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>014</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>{<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>015</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>Thread.yield();<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>016</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>long</span> pulsos = <span style=”color:#000000;”>0</span>;<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>017</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style=”color:#0000e6;”>while</span> (System.nanoTime() &gt; nanoTimeDoProximoPulso &amp;&amp; pulsos &lt; MAX_FRAMES_PARA_PULAR)<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>018</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>{<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>019</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>update();<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>020</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>nanoTimeDoProximoPulso += NANOS_ESPERADOS_POR_PULSO;<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>021</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>pulsos++;<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>022</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>}<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>023</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>render();<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>024</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>}<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>025</span>&nbsp;&nbsp;&nbsp;&nbsp;</span>}<span style=”color:#000000;”><br/><span style=’color:#666;background-color:#DDD;border-right:1px #999 solid;margin-right:5px;padding:2px’>026</span></span>}</span></pre></div>
Autor: Luiz Nörnberg Ver todos os posts de
Sou Bacharel em Ciência da Computação pela Universidade Católica de Pelotas (UCPel), onde também atuei como professor. Desde a época da faculdade (mais de quinze anos atrás) a paixão por jogos tem sido importante no meu direcionamento profissional. Sou sócio-fundador do Izyplay Game Studio, onde exerço o cargo de Diretor de Tecnologia. Sempre tive grande foco em desenvolvimento em Java, embora tenha migrando para a tecnologia Adobe AIR em função de sua portabilidade. Ah, e é claro, dou meus palpites no game design.

11 Comentários em "GameLoop com taxa constante de pulsos – parte 2 de 2"

  1. Alex Cançado 22/10/2009 at 18:58 - Reply

    Nornberg, muito bons seus tutoriais, estou aprendendo muito, dá uma base muito boa, pois não é como os que encontramos por aí, “faça assim” e pronto.

    Método muito didático, valeu.

    PS, estou te mandando uma tentativa de responder o desafio.

    • Luiz Alessandro Nörnberg 23/10/2009 at 00:28 - Reply

      Olá Alex. Recebi seu e-mail. Obrigado pelo interesse! Vou olhar seu código e lhe responder amanhã a noite.

    • Luiz Alessandro Nörnberg 24/10/2009 at 12:40 - Reply

      Alex, boa a sua idéia e ela realmente funciona para o propósito que tu descreves – vou adicionar ela como exemplo no post. Mas… não resolve ainda o problema do desafio. Experimente colocar um valor muito alto para o tempo do update (não do render) e veja que o jogo fica trancado nele, sem chance de atualização da tela.

  2. Rafael Sol Maker 02/11/2009 at 17:19 - Reply

    Hmmm, até que enfim o fatídico loop sem fim dos jogos, e aqui com direito a temporizador! Gostei muito! Será que tem como implementar algum VSync aqui com o Java? Se possível mostre em alguma próxima postagem, quando tiver oportunidade.

    • Luiz Alessandro Nörnberg 03/11/2009 at 16:20 - Reply

      O VSync é implementado no Java pela classe BufferStrategy quando em tela-cheia (e já notei que não é em todas máquinas). Fora isso, ainda não há suporte para sincronização com o refresh do monitor. Mas, em geral, utilizando double buffer (createBufferStrategy(2)) a coisa fica bem fluida, sem piscar, não sendo necessário o vsync. Lembro também que o vsync faz seu game rodar de acordo com a taxa do monitor, que pode variar muito (60, 75, 80, 100Hz), o que pode dificultar o controle da velocidade.

  3. Cristhian 07/03/2013 at 17:59 - Reply

    Muito bom o post, mas agora me surgiu uma dúvida.
    Em um jogo simples, qual a vantagem de se atualizar a tela um número de vezes maiores que o update do jogo, se a tela desenhada será a mesma? Digo, se eu atualizar uma vez o jogo e duas vezes a tela, a tela será atualizada em vão, visto que todos os objetos estarão no mesmo lugar.
    Imagino que haja uma vantagem, porém ainda não vejo qual.

  4. Luiz Nörnberg 07/03/2013 at 19:50 - Reply

    Cristhian, da forma que você falou, realmente não há vantagem. Sua percepção é correta. Para ter vantagem em renderizar mais vezes do que atualizar a lógica, é quando a própria renderização gera mudanças visuais.

    Isso ocorre quando temos uma separação grande entre o que chamamos de view (vista) e o que chamamos de model (modelo). A view pode ser responsável pela animação de um personagem, por exemplo, ou pela sua movimentação de um ponto ao outro da tela.

    Exemplo: no update a posição do personagem muda de 10 para 20. Esta alterações fica no model – para efeitos de física, colisão, etc, ele está na posição 20. Na tela, ele vai “pular” de 10 para 20.

    Mas podemos fazer algo melhor: dentro do render, escrevemos código para efetivamente mover o personagem da posição 10 para 20, pixel a pixel. Veja que não interfere na lógica do jogo (que é mais pesada). Esse é um calculo leve apenas para melhorar a aparência da animação, para deixá-la mais suave. E quanto mais vezes o render for executado, mais suave ela fica.

    Veja mais explicação sobre isso nesse post, no final, onde fala em interpolação: http://abrindoojogo.com.br/tutorial-html-5-parte-33

    Isso costuma ser feito em jogos 3D onde as animações são calculadas no render, de forma que quanto mais quadros por segundo tivermos, mais fluídas elas ficam. Em um jogo de luta, a lógica do jogo controla apenas quadros chave (uns 5 em um movimento de chute), momentos em que é verificada a colisão. Entre estes quadros, é o render que faz a interpolação do modelo 3D para a animação ficar fluída.

  5. Thiago CMV 31/10/2013 at 14:30 - Reply

    Uma possivel solução para o desafio é substituir o while por um if.
    Para render com taxa > 60 o update fica limitado a 60 e para render com taxa < 60 ou updates lentos um acompanha o outro (como na parte 1 do tutorial). O jogo fica lento, mas roda. Se descobriem uma forma melhor de fazer isso agradeço.

    • Thiago CMV 31/10/2013 at 14:41 - Reply

      Consegui! :D usei Threads, uma para o render e uma para o update.
      Criei uma classe ‘UpdateRender’

      iniciei 2 Threads a partir delas: render.start();
      update.start();

      e no loop chamo um metodo de cada Thread tipo update.rodar(…) e render.rodar(….)
      Segue o link do código:
      https://drive.google.com/folderview?id=0B7VmBXCaJbWPYnhTZmJjdGtKNHM&usp=sharing

      • Thiago CMV 31/10/2013 at 14:58 - Reply

        peço desculpas pelo comentário anterior, o código com Threads tb não está funcionando (está até pior que o anterior). Mas creio a solução seria usar threads. Uma thread para update com seu próprio laço e uma para render com seu próprio laço. No meu código estou usando apenas 1 laço. Deve se tomar cuidado tb com o compartilhamento de variáveis entre as threads.

        Gostei muito do seu post Luiz e se a solução que pensei não resolver ou se tiver solução melhor que o if favor responder. Valeu :D

        • Luiz Nörnberg 01/11/2013 at 09:50 - Reply

          Olá Thiago. O uso de threads é um caminho possível, cada uma com seu loop independente (se cada uma não tiver seu loop, você não está tirando vantagem das threads, porque elas não estão executando em paralelo).
          Mas como você notou, é preciso MUITO cuidado com as variáveis que ambas threads acessam. Isso pode ser realmente muito complicado de controlar e na maioria das vezes, não vale a pena.
          Eu preferiria ficar com a versão do post (e seu problema) ou usar a sua versão com IF. Afinal, elas só são problema em máquinas muito lentas, enquanto as threads complicam você durante o desenvolvimento, e assim podem gerar problemas em todas as máquinas…

Deixar um Comentário