
Nossa empresa possui seu próprio mecanismo de jogo, usado para todos os jogos desenvolvidos. Ele fornece todas as funcionalidades básicas importantes:
- renderização
- trabalhar com SDK;
- trabalhar com o sistema operacional;
- com rede e recursos.
No entanto, faltava o valor para o Unity - um sistema conveniente para organizar cenas e objetos de jogos, além de editores para eles.
Aqui, quero contar como introduzimos todas essas comodidades e o que viemos.
O que é agora
Agora temos uma aparência de um sistema de componentes no Unity com todos os subsistemas e editores importantes. No entanto, como partimos das necessidades de nossos projetos específicos, há diferenças bastante significativas.
Temos objetos visuais que são armazenados em cenas. Esses objetos consistem em nós organizados em uma hierarquia e cada nó pode ter um número de entidades, como:
- Transform - transformação do nó;
- Componente - está envolvida na renderização e pode haver apenas uma ou nenhuma. Os componentes são sprite, malha, partícula e outras entidades que podem ser exibidas. O equivalente mais próximo ao Unity é Renderer;
- Comportamento - responsável pelo comportamento, e pode haver vários. Este é um análogo direto do MonoBehaviour in Unity, eles escrevem qualquer lógica;
- A classificação é uma entidade responsável pela ordem na qual os nós em uma cena são exibidos. Como nosso sistema deveria ter sido fácil de integrar em jogos já em execução, com a lógica existente e diversificada para exibir objetos, era necessário poder integrar novas entidades nas antigas. Portanto, a classificação permite transferir o controle sobre a ordem de exibição para o código externo.
Como no Unity, os programadores criam seus componentes, comportamento ou classificação. Para fazer isso, basta escrever uma classe, redefinir os eventos necessários (Update, OnStart, etc.) e marcar os campos necessários de uma maneira especial. No UnrealEngine, isso é feito com macros, e decidimos usar tags nos comentários.
Além disso, levando em consideração as tags, todo o código será gerado, o que é necessário para salvar e carregar dados, para o trabalho dos editores, para oferecer suporte à clonagem e outras pequenas funções.
A serialização e geração automáticas de editores são suportadas não apenas para entidades armazenadas em um objeto visual, mas também para qualquer classe. Para fazer isso, basta herdá-lo da classe Serializable especial e marcar as propriedades necessárias com tags. E se você deseja que instâncias da classe sejam ativos completos (um análogo do ScriptableObject do Unity), a classe deve ser herdada da classe Asset.
Como resultado, a biblioteca oferece uma oportunidade para desenvolver rapidamente novas funcionalidades. E agora parte do trabalho de desenvolvimento do jogo, por exemplo, criação de efeitos, layout da interface do usuário, design de cenas do jogo, pode ser transferida para especialistas que podem lidar melhor com isso do que os programadores.
Blocos principais

Geração de código
Para muitos sistemas funcionarem, você precisa escrever bastante código de rotina, o que é necessário devido à falta de reflexão em C ++ (
reflexão - a capacidade de acessar informações sobre tipos no código do programa). Portanto, geramos a maior parte desse código técnico.
Um gerador é um conjunto de scripts python que analisam os arquivos de cabeçalho e geram o código necessário em sua base. Para configurações de geração flexíveis, tags especiais são usadas nos comentários.
Podemos gerar código para os seguintes subsistemas:
- Serialização - usada para salvar / carregar dados do disco ou ao transmitir pela rede. Será considerado mais detalhadamente mais tarde.
- Ligações para a biblioteca de reflexão - usada para exibir automaticamente o editor para os dados. Será discutido no capítulo sobre o editor.
- Código para entidades de clonagem - usado para clonar entidades no editor e no jogo.
- Código para nossa reflexão em tempo de execução leve.
→ Um exemplo do código gerado para uma classe pode ser
encontrado aqui.Analisando c ++
Quase todas as opções para resolver o problema de analisar arquivos de cabeçalho levaram à análise de código com clang. Mas, após os experimentos, ficou claro que a velocidade de uma solução desse tipo não nos convinha. Além disso, o poder fornecido pelo clang não era necessário para nós.
Portanto, outra solução foi encontrada:
CppHeaderParser . Esta é uma biblioteca de arquivo único python que pode ler arquivos de cabeçalho. É muito primitivo, não segue #include, ignora macros, não analisa caracteres e funciona muito rapidamente.
Ainda o usamos até hoje, no entanto, tivemos que fazer uma quantidade razoável de edições para corrigir bugs e expandir nossos recursos, em particular, foi adicionado suporte a inovações do C ++ 17.
Queríamos evitar mal-entendidos relacionados à incerteza do status de geração de código. Portanto, foi decidido que a geração deveria ocorrer completamente automaticamente. Usamos o CMake, no qual a geração começa em cada compilação (não conseguimos configurar a geração para iniciar apenas quando as dependências mudam). Para que isso não demore muito e não incomode, armazenamos um cache com o resultado da análise de todos os arquivos e conteúdos do diretório. Como resultado, o início ocioso da geração de código leva apenas alguns segundos.
Gerador de código
Com a geração, tudo é mais simples. Existem muitas bibliotecas para gerar qualquer coisa a partir de um modelo. Escolhemos o
Templite + , por ser muito pequeno, ter a funcionalidade necessária e funcionar corretamente.
Havia duas abordagens para geração. A primeira versão continha muitas condições, verificações e outros códigos; portanto, os modelos eram mínimos e a maior parte da lógica e do texto produzido estava em código python. Era conveniente, porque no código python é mais conveniente escrever do que nos modelos, e era fácil ferrar lógica arbitrariamente complicada. No entanto, isso também foi terrível, porque o código python, misturado com um grande número de linhas de código C ++, era inconveniente para ler ou escrever. Os geradores python usados simplificaram a situação, mas não eliminaram o problema como um todo.
Portanto, a versão atual da geração é baseada em modelos, e o código python simplesmente prepara os dados necessários e agora parece muito melhor.
Serialização
Para serialização, várias bibliotecas foram consideradas: protobuf, FlexBuffers, cereais, etc.
Bibliotecas com geração de código (Protobuf, FlatBuffers e outras) não se encaixam, porque temos estruturas manuscritas e não há como integrar as estruturas geradas no código do usuário. E dobrar o número de classes apenas para serialização é um desperdício demais.
A biblioteca de
cereais parecia ser o melhor candidato - boa sintaxe, implementação clara, é conveniente gerar código de serialização. No entanto, seu formato binário não nos convinha, assim como o formato da maioria das outras bibliotecas. Os requisitos de formato importantes foram a independência do hardware (os dados devem ser lidos independentemente da ordem dos bytes e da profundidade dos bits) e o formato binário deve ser conveniente para a gravação em python.
Escrever um arquivo binário a partir de python era importante, pois queríamos ter um script universal independente de plataforma e independente de projeto que convertesse dados de uma exibição de texto para uma binária. Portanto, escrevemos um script que acabou sendo uma ferramenta de serialização muito conveniente.
A idéia principal foi extraída do cereal, é baseada em arquivos básicos para leitura e gravação de dados. Diferentes herdeiros são criados a partir deles que implementam o registro em diferentes formatos: xml, json, binário. E o código de serialização é gerado pelas classes e usa esses arquivos para gravar dados.

O editor
Utilizamos a biblioteca ImGui para editores, na qual escrevemos todas as principais janelas do editor: conteúdo da cena, visualizador de arquivos e ativos, inspetor de ativos, editor de animação etc.
O código principal do editor é escrito à mão, mas para exibir e editar as propriedades de classes específicas, usamos a biblioteca rttr, o binning gerado para ela e o código geral do inspetor que pode trabalhar com o rttr.
Biblioteca de Reflexão - rttr
Para organizar a reflexão em C ++, a biblioteca rttr foi escolhida. Ele não requer intervenção nas próprias classes, possui uma API conveniente e compreensível, oferece suporte a coleções e wrappers sobre tipos (como ponteiros inteligentes) com a capacidade de registrar seus wrappers e permite que você faça o que for necessário (criar tipos, interagir com membros da classe, alterar propriedades, métodos de chamada etc.).
Ele também permite que você trabalhe com ponteiros, como nos campos regulares, e usa o padrão de objeto nulo, o que simplifica bastante o trabalho com ele.
O ponto negativo da biblioteca é que ela é volumosa e não muito rápida, então a usamos apenas para editores. No código do jogo para trabalhar com os parâmetros dos objetos, por exemplo, para um sistema de animação, usamos a biblioteca de reflexão mais simples de nossa própria produção.
A biblioteca rttr requer a criação de uma ligação com a declaração de todos os métodos e propriedades da classe. Essa ligação é gerada a partir do código python para todas as classes que precisam de suporte de edição. E devido ao fato de que os metadados podem ser adicionados ao rttr para qualquer entidade, o gerador de código pode definir configurações diferentes para os membros da classe: dicas de ferramentas, parâmetros de limites de valor aceitáveis para campos numéricos, um inspetor especial para o campo etc. Esses metadados são usados no inspetor para exibir a interface de edição. .
→ Um exemplo de código para declarar uma classe em rttr pode ser
encontrado aquiInspetor
O código dos próprios editores raramente trabalha diretamente com o rttr. A camada mais usada é que o objeto é capaz de desenhar um inspetor do ImGui para ele. Este é um código manuscrito que trabalha com dados do rttr e desenha controles do ImGui para ele.
Para personalizar a exibição da interface de edição de dados, os metadados especificados durante o registro no rttr são usados. Apoiamos todos os tipos primitivos, coleções, é possível criar objetos armazenados por valor e por ponteiro. Se o membro da classe for um ponteiro para a classe base, você poderá selecionar um herdeiro específico durante a criação.
Além disso, o código do inspetor aceita o suporte de operações de cancelamento - ao alterar valores, um comando é criado para alterar os dados, que podem ser revertidos.
Até o momento, não temos um sistema para determinar alterações atômicas com a capacidade de visualizá-las e salvá-las. Isso significa que não temos suporte para salvar as propriedades alteradas do objeto na cena e aplicá-las após carregar a pré-fabricada. E também não há criação automática de faixas animadas ao alterar as propriedades de um objeto.
Windows e editores
No momento, muitos subsistemas e editores diferentes foram criados com base em nossos editores, geração de código e sistemas de criação de ativos:
- O sistema de interface do jogo fornece um layout flexível e conveniente e inclui todos os elementos de interface necessários. Um sistema de script visual do comportamento da janela foi feito para ela.
- O sistema para alternar o estado das animações é semelhante ao editor de estado nas animações no Unity, mas difere um pouco pelo princípio de operação e tem uma aplicação mais ampla.
- O designer de missões e eventos permite que você personalize de forma flexível eventos, missões e tutoriais de jogos, quase sem a participação de programadores.
Ao desenvolver todos esses subsistemas e editores, analisamos atentamente o
Unity , o
Unreal Engine e tentamos tirar o melhor deles. E alguns desses subsistemas são feitos ao lado de projetos de jogos.
Resumir
Concluindo, eu gostaria de descrever como o desenvolvimento foi realizado. A primeira versão de trabalho foi criada e integrada em alguns projetos de jogos por algumas pessoas em apenas dois meses. Ainda não teve geração de código e a abundância de editores que existe agora. Ao mesmo tempo, era uma versão funcional, com a qual o movimento para a frente começou. Isso não quer dizer que naquela época correspondesse ao vetor principal do desenvolvimento do mecanismo, tudo dependia do entusiasmo de várias pessoas e de uma compreensão clara da necessidade e correção do que fizemos.
Todo o desenvolvimento subsequente foi realizado de maneira muito ativa e evolutiva, passo a passo, mas sempre levando em consideração os interesses dos projetos de jogos. No momento, mais de dez pessoas estão trabalhando no desenvolvimento de "nossa pequena unidade" e o desenvolvimento de uma nova versão não é mais tão rápido e rápido como era no começo.
No entanto, alcançamos ótimos resultados em apenas alguns anos e não vamos parar. Desejo que você avance para o que acha certo e importante para si e para a empresa como um todo.