Parece até engraçado eu ter falado de controle de tempo e de uma classe que rode animações no tempo certo independente de FPS sem ter construído um game loop. Pois é, eu ainda não havia.
O fato é que sem o "coração do jogo" seria impossível continuar o desenvolvimento da engine e a partir dai me foquei em resolver esse problema em questão.
Confesso que não foi muito fácil a criação de um game loop para a engine. Me atrasei em praticamente todas as matérias da faculdade enquanto tentava encontrar a abordagem ideal para o projeto da engine.
Pesquisei cerca de dez abordagens de game loop (inclusive o game loop usando na franquia Prince of Persia, onde é possível se voltar no tempo), e percebi que não existe um game loop ideal, mas sim game loops para casos diferentes. Esse estudo melhorou e muito a minha noção de tempo em jogos.
Quando tomei consciência desse fator, me decidi pela abordagem de um game loop semelhante ao usado na Chien 2D (lib desenvolvida pelo professor Radke e pelo professor Binder). Este game loop basicamente recebe a quantidade de FPS do jogo e sincroniza. Contextualizarei ele abaixo:
1- A quantia de FPS é inicializada a partir do construtor da paSDL (ou seja, permanece estática durante todo o jogo).
2- Para atualização do tempo, obrigatóriamente a função membro updateTicks() deve ser chamada no início do game loop.
3- Toda a burocracia de controle de sincronização é feita na função membro sync(), que deve obrigatóriamente ser chamada em algum lugar no game loop.
Um código base para o game loop descrito acima pode ser visto abaixo:
//Inicializa o jogo (nome da janela +
FPS)
paSDL *jogo = new
paSDL("Exemplo GameLoop basico usando SDL",60);
//Inicia GameLoop
do{
//Atualiza tempo
jogo->updateTicks();
//Limpa tela
jogo->clean();
//Funções para
processar o jogo
process();
draw();
//Sincroniza de acordo com a taxa de FPS ajustada
jogo->sync();
}while(1);
delete jogo;
Mas não é isso que um game loop tem que fazer? Por que fiquei tanto tempo procurando abordagens diferenciadas?
O grande problema desse game loop é que ele só cobre o caso de máquinas rápidas, dando ao processador uma "folga" para que ele não rode 100% do tempo no jogo. Em máquinas lentas, o jogo vai simplesmente rodar mais lento, e dependendo do fator de atraso a cada game loop, este pode acabar ficando injogável.
A solução para este problema existe em vários tipos de game loop. A paSDL oferece, portanto, maneiras do usuário desativar o game loop padrão e implementar sua própria abordagem. Basta para isso chamar a função membro setDefaultSync, que recebe um boleano indicando se o game loop padrão deve estar ativo ou não. Caso o mesmo seja desativado, a função membro sync apenas vai atualizar a tela, cabendo ao usuário o controle da sincronização do jogo como um todo.
Complicado? Para a satisfação dos usuários, a engine também oferece um game loop mais complexo, o qual controla toda a burocracia do game loop na engine para o usuário. Este é implementado na classe paGameLoop. Para utilizá-lo, basta criar uma classe filha de paGameLoop, a qual deverá implementar as funções membro draw() e processLogics().
Este game loop leva em conta que o gargalo de todo o jogo está no desenho, e portanto pula até 6 quadros de animação para dar a ideia de que o jogo continua rodando suavemente. Também trata da captura das exceções disparadas pela engine para o usuário, atualização do tempo e sleep do jogo, tendo o usuário apenas que informar na classe filha como se portará o desenho e a lógica do jogo.
Para contextualizar, imagine uma classe derivada de paGameLoop denominada myGameLoop. Esta classe deverá ter um construtor que inicialize paGameLoop. A única exigência da classe pai é que no construtor da classe filha se inicialize a META da classe pai. Esta funcionalidade evita problemas de inicialização na ordem das classes, uma vez que o gerenciador do tempo está em paSDL, e a classe paGameLoop fará uso desta funcionalidade, mas é criada em memória em primeiro lugar. Dar a alternativa de inicializar a META depois oferece ao usuário a flexibilidade de iniciar o tempo do game loop quando todos os requisitos iniciais estiver corretamente ajustados.
Em seguida, é necessário implementar as funções membro draw e processLogics. É importante separar esses dois blocos de código, pois a paGameLoop irá gerenciar os pulos de quadro por meio de processLogics, ou seja, todos os updates de quadro devem ser dados nessa área. Por fim, se os quadros estiverem corretamente ajustados e o game loop não estiver "atrasado", os desenhos definidos em draw são exibidos.
No próximo post falarei sobre as funcionalidades inseridas na classe de tratamento das funcionalidades OpenGL inicialmente implementadas.
Nenhum comentário:
Postar um comentário