Cursos / Jogos Digitais / Introdução a Jogos Digitais / Aula

arrow_back Aula 10 - Motores de Jogos II

1 - Liguem os Motores!

Na aula passada, vimos que a programação de jogos exige uma série de cuidados e que o motor facilita muito a nossa vida. Também vimos várias formas de como ele faz isso, abstraindo as funcionalidades mais difíceis de programar e deixando o desenvolvedor focado na lógica do jogo. Assim, os motores são ótimas invenções e resolvem todos os nossos problemas! Ou pelo menos aqueles que nos dariam dores de cabeça na hora de programar os nossos jogos!

No entanto, as coisas não são tão simples assim! Acho que será melhor se entendermos um pouquinho mais como tudo começou.

Senta que lá vem a história!

Lembra que na aula passada nós falamos que algumas empresas de jogos começaram a desenvolver ferramentas que implementavam partes do jogo que se repetiam em diferentes projetos? Antigamente, o código era misturado entre a parte da lógica do jogo e o código necessário para a aplicação rodar e usar de forma adequada e eficiente o hardware disponível. Isso dificultava muito para que esse código pudesse ser reutilizado, pois você precisava identificar dentro de um código-fonte gigante quais partes tratavam de cada coisa. Uma bagunça!

E então veio DOOM. Um jogo que não só alavancou o gênero do FPS (e ajudou a definir vários padrões para esse gênero), mas também contribuiu significativamente para o desenvolvimento de jogos.

DOOM, um marco no desenvolvimento de jogos.

A equipe que desenvolveu DOOM se preocupou em projetar o jogo com uma separação dos componentes de programação e dos recursos de arte. Dessa forma, uma equipe de artistas poderia criar novos recursos e fazer um “novo” jogo (com a parte de funcionamento igual ao DOOM) no que concerne ao visual. Esse primeiro esforço de separação entre a parte da programação e a parte artística fez com que o termo “motor” fosse utilizado pela primeira vez para se referir a essa aplicação base que poderia ser alterada para criar novos jogos.

Perceba que essa arquitetura não era tão flexível quanto a que temos hoje em dia, mas apresenta uma noção muito importante do funcionamento dos motores: a modularização. Os componentes são separados de acordo com a sua funcionalidade, assim, partes que tratam de uma mesma atividade (desenhar coisas na tela, por exemplo) estão juntas na ferramenta. Isso facilita dois aspectos do processo de desenvolvimento:

  • Facilidade para isolar erros: se o jogo começa a apresentar personagens com falhas no desenho, pode-se testar os diferentes sistemas envolvidos (desenho em tela, carga do modelo 3D) e detectar em que ponto o erro está acontecendo. Em um código onde as funcionalidades estão misturadas e entrelaçadas, é mais difícil detectar onde a falha ocorre. Com a modularização por funcionalidades, é possível testar apenas a parte do desenho e, não detectado o erro, pode-se testar a função que carrega os dados do modelo 3D na memória. Menos dores de cabeça para os programadores, com certeza!
  • Segurança para fazer alterações: se for necessário mudar um algoritmo que renderiza a iluminação pontual (simula a iluminação de uma lâmpada, por exemplo), é mais fácil trabalhar em um código no qual você sabe que todas as instruções interferem apenas no desenho da luz do que em um código no qual você pode acidentalmente alterar a lógica do jogo!
A felicidade dos programadores quando usam um motor de jogos!

Perceba que a separação proposta por DOOM ainda é bastante limitada: usando o motor do jogo, você só conseguia criar jogos parecidos com DOOM, mudando a arte. Com o tempo, novas propostas de modularização foram surgindo, gerando vários níveis de flexibilidade para o motor. Primeiramente, isso se restringiu a motores para jogos do mesmo gênero, ou seja, motores que possuíam otimizações para problemas comuns em jogos FPS ou de estratégia, por exemplo. Em Counter Strike, um jogo de FPS, o modo como a câmera e o nível de detalhe em que o personagem do jogador e os adversários são exibidos na tela é totalmente diferente de um jogo de estratégia em tempo real como Starcraft. Esses motores buscavam facilitar o desenvolvimento trazendo as melhores implementações de funcionalidades comuns (iluminação, renderização do espaço, representação dos elementos do jogo em memória, câmera do jogo, geração de rotas dos personagens no jogo, etc.). Com o tempo, motores mais generalistas passaram a ser criados, de forma que o mesmo motor pudesse ser usado para construir jogos em qualquer gênero, como é feito no Unreal 4 e no Unity. A diferença entre essas abordagens ocorre no grau de reuso que elas permitem. Vamos pensar em um exemplo para clarear essa questão!

Imagine que você deseja fazer um jogo de corrida. Uma das funcionalidades que, com certeza, precisará é desenhar o carro na tela, não é mesmo? Um motor poderia ter diferentes abordagens para isso:

  • Um motor específico para jogo de Fórmula 1 poderia ter uma função desenhaCarro, que só conseguiria desenhar carros no modelo de um carro de circuito de Fórmula 1, permitindo ao desenvolvedor apenas a escolha das cores.
  • Um motor para jogos de corrida poderia ter uma função desenhaCarro, na qual o desenvolvedor informaria o tipo do carro a ser desenhado (Fórmula 1, Ferrari, Fusca) e o motor saberia como desenhar baseado no modelo.
  • O motor poderia ter uma função desenhaModelo, capaz de desenhar qualquer objeto a partir de uma malha 3D fornecida pelo desenvolvedor. Além de desenhar os carros, ele poderia desenhar pessoas, animais, etc.

Existe uma relação de custo/benefício entre flexibilidade de uso e facilidade de desenvolvimento. É óbvio que, se o seu foco é apenas um jogo de Fórmula 1, o primeiro motor já seria o suficiente. Você não precisaria se preocupar em desenhar o modelo do carro, pois ele já estaria pronto! No entanto, você ficaria limitado a trabalhar apenas com os elementos que ele lhe fornecesse, então, se quisesse colocar aquela paisagem linda com montanhas rochosas e o motor não deixasse, poderia esquecer. No outro extremo, temos o motor que deixará você fazer o que quiser, porém precisará efetivamente fazer várias coisas, pois ele lhe dá uma funcionalidade básica de desenho em tela a partir de um modelo 3D, mas você terá de produzir todos os modelos para ele.

Quanto mais flexível o motor, mais você pode dar o seu toque pessoal para o jogo.

E já que falamos em reuso, deixe-me aproveitar o exemplo para falar de outro ponto! Imagine um motor com a função desenhaCarro que só consegue desenhar carros de Fórmula 1. De repente, ela resolve modificar essa função para que os desenvolvedores de jogos possam usar a ferramenta para desenhar não apenas um tipo de carro, mas também outros veículos. Uma grande vantagem de uma ferramenta modularizada diz respeito à definição das interfaces de como usar as funcionalidades, permitindo a transparência sobre como elas são implementadas. Agora imagine que, para usar a função desenhaCarro, o desenvolvedor do jogo só precise escrever a seguinte linha:

desenhaCarro(numero1)

Onde numero1 é o carro que ele quer desenhar. Ao alterar a função, a empresa que desenvolve o motor pode mudar a interface da função e dizer que a partir de agora o jogo precisa informar o carro e o tipo do modelo a desenhar, por exemplo:

desenhaCarro(numero1, “Ferrari”)

OK, isso já é legal! Mas ainda não é perfeito, porque o desenvolvedor do jogo precisará fazer ajustes no seu código para se adequar à nova interface ou, então, usar uma versão antiga da ferramenta. Agora imagine uma função mais genérica, como iluminaSala, que é responsável por pegar uma fonte de iluminação no mundo do jogo e simular todos os raios de luz da cena. Digamos que ela é usada assim:

iluminaSala()

Depois de muita pesquisa, a equipe desenvolvedora do motor descobre um jeito muito mais eficiente de fazer essa simulação, que dobrará a velocidade da renderização da luz e deixará os jogos mais rápidos consumindo menos memória. Mas, para isso, eles precisam implementar a função iluminaSala completamente, mudando inclusive as estruturas que são utilizadas internamente. A função que tinha 30 linhas passa a ter 500, porém a equipe não muda a interface de chamada.

Sabe o que o desenvolvedor de jogos usuário do motor precisa fazer no seu código para usar essa nova função? Nada. Continua chamando-a iluminaSala(). E essa é uma grande vantagem que se obtém com a transparência de funcionalidades do motor: você não precisa saber como a coisa funciona internamente, precisa apenas saber como utilizá-la.

Essa discussão toda foi para falar que nenhum motor é perfeito, mas o uso de um deles torna a sua tarefa muito mais fácil tanto do ponto de vista técnico como no ponto de vista de projeto. Porém, mesmo com uma gama diferente de motores, existem alguns componentes que são comuns entre eles, por serem necessários em praticamente todos os jogos. Falaremos um pouco sobre cada um deles!

Versão 5.3 - Todos os Direitos reservados