|
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
im
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:
- A situação atual é pintada na tela;
- A lógica do jogo é calculada;
- 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 5 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.
-
26/01/2010 14:53:46 |200.168.57.xxx| Vitor Almeida da Silva - Parabéns

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.
-
26/01/2010 20:14:37 | Vinícius Godoy de Mendonça - Obrigado.

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.
-
27/01/2010 18:12:10 |201.1.105.xxx| David Buzatto - Muito bom!

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
-
27/01/2010 19:34:04 | Vinícius Godoy de Mendonça - GameSteps

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).
-
28/01/2010 02:43:16 |189.75.1.xxx| Eduardo Skrepnek Tosin - Muito bom!!

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).
-
28/01/2010 14:16:40 |201.87.183.xxx| Dyorgio - Multi Thread

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
-
28/01/2010 14:33:50 | Vinícius Godoy de Mendonça

É 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.
-
28/01/2010 15:03:48 |201.87.183.xxx| Dyorgio - Pausas

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.
-
28/01/2010 15:13:45 | Vinícius Godoy de Mendonça

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.
-
28/01/2010 16:12:35 |200.168.57.xxx| Vitor Almeida da Silva - re: Isso mesmo

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.
-
28/01/2010 16:35:58 | Vinícius Godoy de Mendonça

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.
-
28/01/2010 16:36:25 | Vinícius Godoy de Mendonça

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.
-
28/01/2010 16:37:53 |201.87.183.xxx| Dyorgio - como?

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.
-
28/01/2010 17:03:51 | Vinícius Godoy de Mendonça

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?
-
28/01/2010 17:08:44 | Bruno Crivelari Sanches

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.)...
-
28/01/2010 17:04:52 | Vinícius Godoy de Mendonça

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.
-
03/02/2010 17:02:07 |189.75.33.xxx| Eduardo Skrepnek Tosin - Utilização do MainLoop

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á.
-
03/02/2010 17:20:45 | Vinícius Godoy de Mendonça - Applets

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
-
18/02/2011 09:02:14 |189.41.54.xxx| Bruno - game.processLogics();

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?
-
19/07/2011 15:20:31 |189.40.216.xxx| Flávio Henrique Alves - e se separassse lógica e repaint em threads separa

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?
-
19/07/2011 15:44:41 | Vinícius Godoy de Mendonça - Isso não é necessário

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.
-
05/09/2011 18:03:27 |189.71.40.xxx| Allan

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
-
05/09/2011 19:39:34 | Vinícius Godoy de Mendonça

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;











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?