Minha Jornada Pessoal no Desenvolvimento de Game Engines

Recentemente lancei um projeto que vinha trabalhando desde o ano passado, a selene, mas parando pra fazer uma análise do que desenvolvi até agora e das ideias que ainda tenho para a mesma, vi que muitos dos conceitos apareciam nos meus primeiros projetos de engine, nem que seja em forma de protótipo ou ideia. Por mais que um projeto seu nunca veja a luz do dia, ou não venha a ser finalizado, só de colocar um esforço de aprendizado ali já serve de muita coisa. E fazendo essa análise dos projetos em que já trabalhei, acabei achando uns paralelos bem interessantes de como meu passo seguinte acaba meio que sendo influenciado pela minha dificuldade no momento. Aqui vou tentar filtrar mais sobre game engines, frameworks e projetos do tipo, mas até mesmo meu tempo usando diversas engines como GameMaker, Construct 2, Godot, etc, foram valiosos e moldaram de certa forma meus interesses.

Pulando nas frameworks

Uma das minhas primeiras frustrações lidando com game engines foi exatamente a de não ter acesso ao loop principal do jogo, isso me irritava por N motivos. Selene ironicamente foi o nome que dei pro meu primeiro projeto de quando comecei a estudar maneiras de desenvolver jogos sem engines, utilizando somente frameworks, ainda mantenho guardado nesse repositório selene-old. Nesse ponto eu já vinha mexendo com Lua através da LÖVE e também vinha estudando sobre OpenGL e SDL2. Era um projeto mais despreocupado, como falei estava começando a estudar computação gráfica, renderização e coisas do gênero. Só isso já estou falando de 7/8 anos atrás, meados de 2016/2017. Na época eu estava na faculdade e era bolsista, além de estar tocando um projeto pessoal com uns amigos, o Railgunners, então facilmente não voltei a mexer na biblioteca.

Game Making Tools

Depois disso fiz diversos projetos relacionados, geralmente projetos gráficos utilizando SDL2 e/ou OpenGL para os gráficos, aí entram jogos, emuladores, cheguei a começar a desenvolver um fantasy console também. Todos não finalizados, foram mais protótipos que seguiam essas áreas de interesse. Durante 2018 e 2019 continuei em projetos do tipo e mexendo muito com a framework LÖVE, nesse meio tempo, buscando sobre como construir interfaces gráficas também conheci a imgui através de um binding para a LÖVE. Nisso eu comecei a adentrar no mundo de criar minhas próprias ferramentas para desenvolvimento jogos, tentando fazer um editor para LÖVE (pixel coffee). Nessa época eu tinha zero ou muito pouca noção de desenvolvimento multiplataforma, então uma das barreiras que encontrei na época foi exatamente só conseguir compilar um .so do ImGui para usar no Linux, não fazia ideia de como compilar um .dll.

tiny coffee

Chegando em 2020, um dos projetos que me fez ter essa noção melhor de desenvolvimento multiplataforma foi exatamente a tiny coffee (perceba a criatividade para nomes), pelo menos no que tange compilação para Linux e Windows. É até hoje o meu projeto com mais estrelas no github. A ideia era ser uma framework escrita somente em C, porém com possibilidade programar tanto em Lua quanto em Wren através de bindings. Esse projeto também foi extremamente importante para eu entender como que funcionam linguagens de script e como que integro elas no meu código, já tinha feito algo utilizando a API C da Lua mas eu tinha pouquíssima noção. Nesse ponto eu já entendia muito melhor de computação gráfica então não tive muitos problemas pra construir o renderizador, só que aí eu tive que aprender que game engines não servem só para renderizar gráficos. Foi minha primeira vez organizando módulos de input, filesystem, etc, e talvez lidar com um dos meus piores pesadelos na época, programação de áudio, já que eu queria porque queria fazer um mixer na mão (insisto nisso até hoje..). E como eu não tinha nenhuma noção de estrutura, segui a que eu estava mais acostumado e que me agradava na época, a da LÖVE. Nos códigos de exemplo no README.md dá pra ver isso claramente.

A tiny coffee (tico) é um projeto que eu gosto bastante, inclusive gostaria de melhorar algumas coisas nela com o conhecimento que possuo hoje, mas vamos de um projeto de cada vez pra não pirar a cabeça. Infelizmente esse projeto já começou errado em eu querer suportar duas linguagens ao mesmo tempo mesmo e tendo zero noção sobre como implementar uma delas. Como eu queria que a engine fosse primariamente em C e eu tava com essa ideia de desenvolver meus próprios editores (usando imgui inclusive), eu não queria criar dependência de cara com nenhuma dessas linguagens, então eu precisaria de um formato neutro de serialização (optei por JSON) e precisaria desenvolver todas as features (editores, resource manager, …) em C de forma que pudessem ser manipulados tanto em Lua quanto em Wren, isso faria parte do sistema de plugins, inclusive essas próprias linguagens se tornariam plugins. Apesar de ainda ter chegado a fazer uns testes com a Wren, admito que acabei mexendo praticamente só com Lua, a dificuldade maior aqui nem iriam ser os bindings em si, mas sim a integração da linguagem com o editor que eu estava desenvolvendo. Por achar o JSON limitado em certos aspectos, pois queria formas de guardar certos dados do editor como referências, tipos personalizados, e que por mais que eu pudesse fazer no JSON criando metadados, talvez seria uma possibilidade fazer uma espécie de JSON estendido. E aqui acabei entrando numa nova fase de estudos que foi a de escrever meu próprio parser, e foi exatamente a partir desses estudos que eu acabei desembocando no desenvolvimento de linguagens de script.

Ainda cheguei a começar a escrever um parser de um formato que como falei seria algo como um JSON estendido, cafe-machine. Porém percebi que em certos momentos estava pensando em uma estrutura não só mais de linguagem de formatação, já tava puxando mais pra programação, com operações aritméticas, referências, etc. Então pensei que de fato uma linguagem de script simples seria uma solução interessante. Nesse caso, o sistema de plugins agora seria gerido por essa linguagem intermediária, e as interações com o editor (que continuaria sendo desenvolvido em C) seriam também feitas por intermédio dessa linguagem. A partir disso até mesmo o binding para outras linguagens poderia ser facilitado, uma vez que dando para manipular plugins através de uma linguagem intermediária, bastaria criar uma camada de comunicação entre a minha linguagem A e a linguagem destino B. Mas até então isso ficou só no campo das ideias. Nisso comecei a estudar sobre desenvolvimento de linguagens de script, a princípio pensei em uma baseada em Lisp pois ela é fácil de se lidar nesse sentido de fazer um parser e sua estrutura já ser algo muito próximo da própria AST (Abstract Syntax Tree).

cafe

A partir disso comecei outro projeto, a cafe engine, que no caso desse repositório já vai ser a versão que fiz em Rust depois, a versão em C ainda pode ser encontrada em cafe-project. É uma ideia similar à tiny coffee, mas agora deixando de lado qualquer outra linguagem além de C e esse Lisp customizado. Esse projeto é divido em subprojetos que operam coisas diferentes:

  • Tea: OpenGL loader and renderer
  • Coffee: Lisp scripting language
  • Mocha: Audio loader and system
  • Latte: Filesystem and packaging
  • Cafe: Game engine, uses all anterior libraries

Como falei anterioremente, esse projeto acabou sendo convertido em parte para Rust posteriormente. Estava aprendendo a linguagem e achei muito que a estrutura que havia pensado pro projeto, com isso de subprojetos que funcionam como bibliotecas independentes, funcionaria bem em Rust. Cada biblioteca seria um pacote e eu consigo usar o Cargo para facilmente gerenciar essas dependências, e não ficar me preocupando com fazer um build system para cada. É um projeto que não tenho intenções de voltar a mexer tão cedo. Ainda quero desenvolver essa linguagem baseada em Lisp como um projeto separado, e apesar de ter gostado muito de Rust, não tenho intenção de voltar a mexer nela por agora, em 2025 na real to querendo dar uma olhada mais a fundo na Zig. No fim acabou virando um projeto bem mais de estudo, foi o primeiro projeto que cheguei a compilar pra web usando emscripten.

poti e selene

Continuei mexendo na cafe até o ano passado, porém em meados de 2021/2022 entra em paralelo o ultimo projeto que acaba virando o que é a selene hoje, e o que eu quero fazer com ele que é uma parada bem mais pé no chão, que é a poti. Ainda muito inspirado pela LÖVE (tanto que a estrutura da API é praticamente a mesma), a poti nasceu com a ideia de ser uma game framework em Lua com um core simples escrito em C. Então nada de linguagens externas ou até mesmo editores dentro da própria engine, no máximo alguns scripts escritos em Lua que seriam convertidos em arrays de bytes e compilados junto do executável, como um shader factory para lidar com diferentes versões do GLSL ou um audio bank pra gerenciar sons e evitar carregar dados duplicados.

Aqui já tinham as raízes do que eu tava querendo desenvolver, um executável que funciona em cima de poucos callbacks. Eu tenho loop principal que fica rodando enquanto uma variável de running for true, nesse loop principal o programa busca uma função que fica armazenada na tabela de REGISTRY_INDEX da Lua, com isso basta alterar esse campo com outra função que altero facilmente o comportamento do loop principal. Um dos principais motivos para mudar o nome do projeto foi simplesmente por combinar mais, selene tem muito mais a ver com um projeto que tem como foco principal a linguagem Lua. Porém a mudança estrutural veio (creio eu) principalmente de dois fatores, me distanciar da LÖVE e ter mais liberdade de simplesmente reescrever todo o meu renderizador do zero sem necessariamente precisar recompilar todo o código. Então a ideia dessa vez era expor as bibliotecas como OpenGL e SDL2 para Lua através de bindings e escrever a framework e a engine diretamente em Lua. Então renderizador, sistema de áudio, filesystem, tudo seria escrito em Lua, e nessa seria bem mais fácil de customizar para cada projeto. Nesse meio tempo também comecei a usar o premake5 que me ajudou a consolidar mais essa ideia de um executável simples com módulos embutidos e extensões para os existentes (string, os, table, …).

Por fim trago de volta a ideia dos plugins para fazer essa divisória entre o que já está no executável final e o que pode ser alterado por fora. Aqui posso me aproveitar plenamente de todas as versatilidades que a própria linguagem Lua já trás para carregar scripts externos das mais diferentes localidades pelas estruturas de require, package.path e package.searchers, permitindo inclusive carregar bibliotecas compartilhadas (.so e .dll).

A poti, por outro lado, quero transformar em uma game framework apenas em C, sem me preocupar com linguagens externas e focando somente em funcionar no multiplataforma. Aqui seria mais pra uso pessoal mesmo, não tenho planos de suportar nada de avançado por padrão. Por ironia ou não da vida, estou há uns 3 anos trabalhando com engenharia de porting, então de quebra também tive uma oportunidade de conhecer mais a fundo as estruturas dos consoles, ou pelo menos dos principais atualmente (Nintendo Switch, Xbox One e PS4 e PS5). E isso também com certeza também vai influenciar na forma como desenvolvo meus projetos futuros.

Pra que continuar desenvolvendo?

Pra finalizar o texto, você talvez se pergunte qual a utilidade disso tudo. Durante esses anos eu também me perguntei a mesma coisa, se eu não estava só perdendo tempo estudando isso tudo e focando nesses projetos. Inclusive até hoje em dia me pergunto isso, com a diferença que hoje meu trabalho como engenheiro de porting acaba de alguma maneira se relacionando com isso. Mas por mais que possa ser bem contra intuitivo, há diversas vantagens de se desenvolver uma engine própria pro seu jogo, mas pra mim o primeiro de tudo é que é divertido estudar e criar esses códigos, eu gosto. Apesar de não viver pra mexer nesses projetos, gosto de aproveitar quando estou me sentindo produtivo e quero colocar algo pra frente. O segundo ponto é sobre a influência das engines que cito no início desse texto. Por elas terem estruturas fixas e se não forem open source, mais difícil ainda ter acesso ao loop principal, então fazer coisas tipo um sistema de pause simples, onde preciso congelar o update dos meus objetos, requerem um certo nível de criatividade para contornar essas limitações. Esse foi o principal motivo para eu começar a explorar frameworks, ter acesso ao loop principal e controle das bases do meu jogo, em resumo, construir minha própria engine em cima de uma framework. E a selene eu to desenvolvendo bem com esse pensamento em mente, de ter a liberdade de criar estruturas customizadas praticamente do zero pra qualquer projeto, sem a necessidade de recompilar o binário para isso.