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.