Departamento de Informática
PROGRAMANDO JOGOS COM CÉU
Aluno: Leonardo Kaplan
Orientador: Roberto Ierusalimschy
Introdução
A programação de jogos eletrônicos envolve a coordenação de objetos de cena
(centenas ou milhares deles), tais como personagens, itens, projéteis e mapas. Cada ação do
usuário pode gerar uma reação em múltiplos objetos que devem ser processados
concorrentemente e em tempo real, de modo a não afetar a experiência do usuário. Além
disso, o próprio passar do tempo físico deve ser prontamente processado como fonte para o
movimento contínuo de objetos animados. Como se espera, a característica reativa,
concorrente e de tempo real impõe grandes desafios para o programadores de jogos
eletrônicos.
A linguagem de programação predominante para o desenvolvimento de jogos é C++,
que possui suporte à orientação a objetos e é bastante eficiente. No entanto, sendo uma
linguagem de propósito geral, ela não possui facilidades dedicadas a lidar com entradas do
usuário e tampouco com concorrência em tempo real.
Céu é uma linguagem reativa e concorrente desenvolvida no LabLua do Departamento
de Informática da PUC-Rio. Acreditamos que Céu é uma alternativa viável para a atual forma
de desenvolver jogos, visto as garantias de sincronismo e concorrência que a linguagem
oferece e as facilidades em fazer um software que reativo, ou seja, que garante uma saída em
um tempo limitado, no caso de um jogo, deve-se computar as mudanças entre dois quadros.
Céu garante em tempo de compilação que não haverá condições de corrida, não haverá
vazamento de memória e todos inputs serão tratados, ou seja, não haverá estados bloqueantes.
Simultaneamente, Céu compila para C, permitindo o uso de funções C diretamente, com um
peso sintático bastante leve. Desta forma podemos usar as bibliotecas C sem muito esforço
[1].
Para embasar nossa hipótese que Céu é uma linguagem adequada para o
desenvolvimento de jogos, fizemos um estudo comparativo entre implementações de jogos
eletrônicos nas linguagens de programação Javascript, C e C++ e Céu. A fim de tal
comparação ser possível, os jogos selecionados foram reimplementados na linguagem Céu
(processo conhecido como porte). Abordamos jogos de tamanho pequeno e grande, com
diferentes metodologias e resultados para cada escopo.
Metodologia
Para implementar os jogos optamos pela biblioteca SDL2, escrita em C, por ser
gratuita, portátil, amplamente utilizada e documentada e ter uma relação entre desempenho e
curva de aprendizado razoável para nossos estudos.
Durante todo o processo, documentamos o desenvolvimento e as tomadas de decisão
junto ao versionamento do código, podendo assim voltar e olhar as diferenças entre
implementações ao longo do trabalho.
Começamos o trabalho procurando jogos de código aberto e estudando os principais
padrões que Céu se propõe a resolver. Buscamos em duas categorias: Jogos pequenos, com
até 5.000 linhas de código e jogos maiores, a partir de 50.000 linhas.
Departamento de Informática
A.
Jogos de pequeno porte
Estratégia de implementação e descrições dos jogos
Para implementar os jogos pequenos, procuramos observar os atores dos jogos e
implementar os módulos Céu correspondentes, imitando seus comportamentos. Os jogos
foram implementados do zero.
Os jogos pequenos escolhidos foram da série “lessmilk”, uma coleção de jogos
paradidáticos na instrução de uso do arcabouço phaser.js, feito em JavaScript. Os jogos são
simples e seus códigos foram bem documentados.
O primeiro jogo tem como objetivo sobreviver pelo maior tempo possível. O jogo
termina quando o personagem colide com um dos inimigos, que são gerados proceduralmente
nas bordas da tela.
O jogo trabalha com 2 tipos de entidades: o jogador e os inimigos.
O segundo jogo é um scroller vertical com vida (o jogador sobrevive a vários
impactos), itens e um contador de tempo, como no anterior. O objetivo é também sobreviver
pelo maior tempo possível.
O mesmo trabalha com um jogador, variados tipos de projéteis, itens e inimigos.
As diferenças entre este jogo e o anterior foram a variedade de projéteis e inimigos, os
itens e a classe de texto, que criamos pois o número de contadores cresceu e consideramos
necessário criar esta abstração.
No terceiro jogo, o jogador é um objeto que se move automaticamente para a direita e
tem como objetivo desviar dos objetos da fase e chegar ao final da mesma, na extremidade
direita. A fase recomeça na colisão entre objeto e jogador. Existem várias fases.
A principal mudança foi a adição de um sistema de fases.
Figura 3 - Terceiro jogo
Figura 1 - Primeiro jogo
Figura 2 Segundo jogo
Resultados e Discussões
Com a característica de ser uma linguagem orientada à eventos, Céu possibilitou
realizar um mapeamento direto entre os eventos de pressionamento de teclas e movimentação
dos personagens. Porém, para se adequar ao modelo da versão de controle, reescrevemos os
códigos de modo estruturado.
O gerenciamento de memória dos inimigos se deu de forma automática, através do
mecanismo de pools de Céu. A possibilidade de cada instância de inimigo poder se atualizar e
se deletar da memória, aliada à certeza de que a linguagem iria garantir a correta iteração das
Departamento de Informática
pools foi fator de grande economia e qualidade de código visto que provê um desacoplamento
entre o módulo que gera e armazena as instâncias de inimigos e o módulo do inimigo em si.
Além disso, os trechos de códigos responsáveis pela eliminação e correção da estrutura de
armazenamento das instâncias foram eliminados, pois estavam implícitos na estrutura da
linguagem.
Como nossos estados de jogo , tela inicial e o jogo em si, sempre seguiam a mesma
ordem, pudemos utilizar o recurso sintático “do <Classe>” ”emit ok” dentro de um ciclo.
O primeiro é usado para criar uma instância da classe e o segundo é para destruir a mesma,
sendo chamado internamente. Desta forma a transição entre estados foi trivial, já que os
estados também gerenciavam sua própria memória e estados internos.
A variedade de projéteis foi bastante simples de ser implementada pois a instância de
jogador, que era o que criava os projéteis, mantinha o estado dos itens coletados, responsáveis
pela tomada de decisão do projétil a ser instanciado.
No segundo jogo, o maior obstáculo na implementação da variedade dos inimigos foi
o impacto que os projéteis faziam nos mesmos: enquanto um tipo era imune aos projéteis do
jogador, o outro era destruído no impacto. Como os inimigos mantinham o seu modelo de
colisão internamente, pudemos abstrair essa diferença no código. Quanto à vida do jogador, a
mudança foi criar um contador interno na classe.
O Sistema de fases do terceiro jogo foi criado no mesmo modelo que os estados
principais do jogo anterior. O estado de jogo instancia estados internos, cada um
representando uma fase, estes estados então instanciam os jogadores e os inimigos. A
transição de um estado interno para o próximo também se da com o uso do par “do <Classe>”
e “emit ok”.
O modelo de organismos [2] como forma de abstração nos possibilitou fazer um
desenvolvimento modular e incremental, enquanto mantínhamos as abstrações concorrentes
do jogo base.
Como não tínhamos variáveis globais, o processo de desenvolvimento foi facilitado,
visto que a quantidade de efeitos colaterais associados a um organismo é mínimo. Pudemos
assim, reaproveitar bastante código Céu.
Ao fim do processo, os jogos ficaram idênticos aos originais, visto que a execução é
determinística e usamos os mesmos sprites e constantes.
B.
Jogos de grande porte
Estratégia de implementação e descrição do jogo
O jogo grande escolhido foi Pingus, uma versão do clássico Lemmings porém com
código aberto e mantida pela comunidade. A escolha do mesmo se deu por conter diversos
padrões que gostaríamos de trabalhar, como: timers, spawners, movers, polimorfismo, GUI e
uso de uma engine modular. Além disso, estava razoavelmente bem documentado.
O jogo tem como objetivo principal garantir a segurança dos diversos pinguins que
percorrem a fase em busca da saída. Os obstáculos incluem armadilhas e abismos. Existem
diversos tipos de pinguins, cada um com uma habilidade passiva. O jogador é responsável por
alterar os tipos de pinguins durante a partida.
A estratégia de implementação do Pingus em Céu foi oposta à dos jogos pequenos: em
vez de fazer um jogo novo, definindo os módulos Céu e implementando cada um, nós
modificamos o próprio jogo, tomando cuidado para que ele sempre mantivesse as mesmas
propriedades do original. Desta forma, implementamos módulo a módulo, sempre mantendo o
jogo em desenvolvimento com comportamento igual ao do binário de controle. Ao mesmo
Departamento de Informática
tempo, tivemos que prestar atenção com o uso da memória e dos recursos compartilhados, já
que o código C++ não provê as garantias de Céu.
O código C++ que pretendíamos reescrever é composto de diversas classes, a maioria
com capacidades de leitura e escrita de seus estados internos, outras possuíam capacidade de
instanciar e destruir objetos do mundo.
Com esses dados, a forma de trabalho se deu em instanciar a memória em C++, passar
para Céu, computar as transformações e retornar a memória modificada para o lado C++,
onde seria renderizada e exibida. Este processo é realizado de forma atômica e não
bloqueante, garantida pelo modelo síncrono de Céu. Além disso, a única mudança no estado
global é feita através da interface provida pelo código C++. Através do sistema de mudança
de contexto e das próprias variáveis globais do código original.
Figura 4 - Pingus: Tela de seleção
de fase
Figura 5 - Pingus: Tela de
descrição de fase
Figura 6 - Pingus: Durante o jogo
Figura 7 - Pingus: Durante o jogo
Resultados e Discussões
À medida que transformamos os métodos de escrita em eventos Céu, o controle pode
ser passado totalmente para o lado Céu, visto que não haveria concorrência de escrita de
memória. Desta forma, fomos reduzindo as dependências de leitura/escrita em C++,
transformando os acessos em eventos Céu. Passado o momento em que o módulo fosse
modificado pelo lado C++, pudemos instanciá-lo e destruí-lo de dentro do ambiente Céu.
Implementamos parte dos módulos do jogo, escolhidos de forma que pudéssemos
estudar e implementar ao menos um exemplo de cada padrão considerado relevante.
Acreditamos que os demais módulos são formados pelos mesmos padrões de código.
Conclusões
O uso de organismos permite a adição e remoção de módulos no jogo sem efeitos
colaterais, o que nos provê uma ótima fonte de reuso e facilidade de depuração de código.
Departamento de Informática
Comparando com os códigos escritos em Javascript, o maior trabalho foi em
reescrever partes da biblioteca phaser.js, que o autor dos mesmos utilizou. Os códigos Céu
ficaram mais legíveis, visto que existe o mecanismo explícito em sintaxe para executar blocos
de código concorrentemente e desta forma, cada bloco corresponde à uma ação específica.
Comparando com o código C++, pudemos verificar que as partes mais trabalhosas do
processo foram encontrar os efeitos colaterais que os diversos módulos causavam entre si e as
condições de corrida provenientes disso. Quando a quantidade de variáveis globais começa a
ser significativa, a dificuldade de depuração aumenta, pois perdemos a parte da noção de
escopo sintático.
Para analisar o trabalho, utilizamos a mesma métrica de [1], contabilizando as
reduções de timers e variáveis globais. Devido às propriedades da linguagem, tivemos 100%
de redução: 6 timers e 15 variáveis de estado dentre os jogos.
No primeiro jogo retiramos 2 timers, 4 variáveis globais; no segundo, 4 timers e 5
variáveis globais; no terceiro, não retiramos nenhum timer mas retiramos 6 variáveis globais.
No caso do Pingus, a forma escolhida pelos desenvolvedore originais para organizar as
instâncias dos objetos do jogo foi prover vários pequenos escalonadores e estruturas de
dados. Isso gerou a necessidade de criar diversas classes auxiliares, uma para cada classe. Em
Céu, o mecanismo de pool é agnóstico quanto à classe utilizada, podendo ser sempre
utilizado. Portanto estas classes puderam ser desconsideradas.
Além disso, os inputs eram passados de objeto em objeto, de modo que os objetos que
instanciavam outros precisavam manter o controle dos instanciados. Como cada organismo
Céu pode gerenciar seus inputs, pudemos abstrair esse controle.
Desta forma o custo em termos de organização de dependências dos módulos diminui,
visto que as dependências de remoção são mínimas com o controle de pools e as de inserção
são eliminadas com a passagem de inputs por dentro da linguagem.
O polimorfismo que C++ provê foi substituído pela composição de organismos em
Céu. Chegando aos mesmos resultados, porém com uma carga sintática menor, pois não
precisávamos deixar explícito a atual forma da instância.
Não tivemos uma mudança significativa em Pingus para medir quantitativamente as
reduções, visto que não fizemos o porte completo e o código C++ precisa das variáveis
globais e timers para manter o funcionamento dos módulos não implementados.
Referências
1 - SANT’ANNA, Francisco. Ceu: A Reactive Language for Wireless Sensor Networks. Proceedings of
the ACM SenSys’ 11, 2011.
2 - SANT’ANNA, Francisco; IERUSALIMSCHY, Roberto; RODRIGUEZ, Noemi. Structured Reactive
Programming with Céu.
Download

Leonardo Kaplan - PUC-Rio