Portando Surreal Engine C++ para WebAssembly

Neste post vou descrever como portei o motor de jogo Surreal Engine para WebAssembly.

Motor Surreal – um motor de jogo que implementa a maior parte das funcionalidades do Unreal Engine 1, jogos famosos neste motor – Torneio Unreal 99, Unreal, Deus Ex, Imortal. Refere-se a mecanismos clássicos que funcionavam principalmente em um ambiente de execução de thread único.

Inicialmente tive a ideia de assumir um projeto que não conseguiria concluir em um prazo razoável, mostrando assim aos meus seguidores do Twitch que existem projetos que nem eu consigo realizar. Durante minha primeira transmissão, de repente percebi que a tarefa de portar o Surreal Engine C++ para WebAssembly usando Emscripten é viável.

Surreal Engine Emscripten Demo

Depois de um mês posso demonstrar minha montagem de garfo e motor no WebAssembly:
https://demensdeum.com/demos/SurrealEngine/

O controle, como no original, é feito através das setas do teclado. Em seguida, pretendo adaptá-lo para controle móvel (tachi), adicionando iluminação correta e outros recursos gráficos da renderização do Unreal Tournament 99.

Por onde começar?

A primeira coisa que quero dizer é que qualquer projeto pode ser portado de C++ para WebAssembly usando Emscripten, a única dúvida é quão completa será a funcionalidade. Escolha um projeto cujas portas de biblioteca já estejam disponíveis para Emscripten, no caso do Surreal Engine, você tem muita sorte, pois o mecanismo usa as bibliotecas SDL 2, OpenAL – ambos foram portados para o Emscripten. No entanto, Vulkan é usado como uma API gráfica, que atualmente não está disponível para HTML5, o trabalho está em andamento para implementar WebGPU, mas também está em fase de rascunho, e também não se sabe quão simples será a porta adicional de Vulkan para WebGPU , depois de totalmente padronizado. Portanto, tive que escrever minha própria renderização básica OpenGL-ES/WebGL para Surreal Engine.

Construindo o projeto

Construir sistema no Surreal Engine – CMake, que também simplifica a portabilidade, porque Emscripten fornece aos seus construtores nativos – emcmake, emmake.
O porte do Surreal Engine foi baseado no código do meu último jogo em WebGL/OpenGL ES e C++ chamado Death-Mask, por isso o desenvolvimento foi muito mais simples, eu tinha todos os build flags necessários comigo e exemplos de código.

Um dos pontos mais importantes em CMakeLists.txt são os sinalizadores de construção do Emscripten. Abaixo está um exemplo do arquivo do projeto:


-s MAX_WEBGL_VERSION=2 \

-s EXCEPTION_DEBUG \

-fexceptions \

--preload-file UnrealTournament/ \

--preload-file SurrealEngine.pk3 \

--bind \

--use-preload-plugins \

-Wall \

-Wextra \

-Werror=return-type \

-s USE_SDL=2 \

-s ASSERTIONS=1 \

-w \

-g4 \

-s DISABLE_EXCEPTION_CATCHING=0 \

-O3 \

--no-heap-copy \

-s ALLOW_MEMORY_GROWTH=1 \

-s EXIT_RUNTIME=1")

O próprio script de construção:


emmake make -j 16

cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data

cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js

cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm

cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

cp ../buildScripts/Emscripten/background.png /srv/http/SurrealEngine/background.png

Em seguida, prepararemos o índice .html , que inclui o pré-carregador do sistema de arquivos do projeto. Para fazer upload para a web, usei o Unreal Tournament Demo versão 338. Como você pode ver no arquivo CMake, a pasta do jogo descompactada foi adicionada ao diretório de construção e vinculada como um arquivo de pré-carregamento para Emscripten.

Alterações no código principal

Então foi necessário alterar o loop do jogo, você não pode executar um loop infinito, isso faz com que o navegador congele, em vez disso você precisa usar emscripten_set_main_loop, escrevi sobre esse recurso em minha nota de 2017 “< a href="https://demensdeum.com /blog/ru/2017/03/29/porting-sdl-c-game-to-html5-emscripten/" rel="noopener" target="_blank">Portar jogo SDL C++ para HTML5 (Emscripten)”
Alteramos o código para sair do loop while para if, então exibimos a classe principal do mecanismo de jogo, que contém o loop do jogo, no escopo global, e escrevemos uma função global que chamará a etapa do loop do jogo do objeto global :


#include <emscripten.h>

Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;

void emscripten_game_loop_step() {

	EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();

}

#endif

Depois disso, você precisa ter certeza de que não há threads em segundo plano no aplicativo, se houver, então prepare-se para reescrevê-los para execução de thread único ou use a biblioteca phtread no Emscripten.
O thread de segundo plano no Surreal Engine é usado para reproduzir música, os dados vêm do thread do mecanismo principal sobre a faixa atual, a necessidade de tocar música ou sua ausência, então o thread de segundo plano recebe um novo estado por meio de um mutex e começa a tocar nova música ou pausa-o. O fluxo de fundo também é usado para armazenar música em buffer durante a reprodução.
Minhas tentativas de construir o Surreal Engine para Emscripten com pthread não tiveram sucesso, porque as portas SDL2 e OpenAL foram construídas sem suporte a pthread e eu não queria reconstruí-las por causa da música. Portanto, transferi a funcionalidade do fluxo de música de fundo para execução de thread único usando um loop. Ao remover as chamadas pthread do código C++, movi o buffer e a reprodução da música para o thread principal, para que não houvesse atrasos, aumentei o buffer em alguns segundos.

A seguir, descreverei implementações específicas de gráficos e som.

Vulkan não é compatível!

Sim, Vulkan não é compatível com HTML5, embora todos os folhetos de marketing apresentem suporte multiplataforma e ampla plataforma como a principal vantagem do Vulkan. Por esse motivo, tive que escrever meu próprio renderizador gráfico básico para um tipo OpenGL simplificado – – ES, é usado em dispositivos móveis, às vezes não contém os recursos modernos do OpenGL moderno, mas porta muito bem para WebGL, que é exatamente o que o Emscripten implementa. A escrita da renderização básica de blocos, renderização bsp, para a exibição da GUI mais simples e renderização de modelos + mapas foi concluída em duas semanas. Esta foi talvez a parte mais difícil do projeto. Ainda há muito trabalho pela frente para implementar todas as funcionalidades da renderização do Surreal Engine, portanto, qualquer ajuda dos leitores é bem-vinda na forma de código e solicitações pull.

OpenAL compatível!

Grande sorte é que o Surreal Engine usa OpenAL para saída de áudio. Depois de escrever um hello world simples em OpenAL e montá-lo em WebAssembly usando Emscripten, ficou claro para mim como tudo era simples e comecei a portar o som.
Após várias horas de depuração, ficou óbvio que a implementação OpenAL do Emscripten possui vários bugs, por exemplo, ao inicializar a leitura do número de canais mono, o método retornou um número infinito, e após tentar inicializar um vetor de tamanho infinito, C++ trava com a exceção vector::length_error.

Conseguimos contornar isso codificando o número de canais mono para 2048:


		alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);



#if __EMSCRIPTEN__

		monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?

#endif



Existe uma rede?

O Surreal Engine atualmente não suporta jogos online, jogar com bots é compatível, mas precisamos de alguém para escrever IA para esses bots. Teoricamente, você pode implementar um jogo em rede no WebAssembly/Emscripten usando Websockets.

Conclusão

Concluindo, gostaria de dizer que a portabilidade do Surreal Engine acabou sendo bastante tranquila devido ao uso de bibliotecas para as quais existem portas Emscripten, bem como à minha experiência anterior na implementação de um jogo em C++ para WebAssembly em Emscripten. Abaixo estão links para fontes de conhecimento e repositórios sobre o tema.
M-M-M-MATANÇA DE MONSTRO!

Além disso, se você quiser ajudar o projeto, de preferência com código de renderização WebGL/OpenGL ES, escreva para mim no Telegram:
https://t.me/demenscave

Links

https://demensdeum.com/demos/SurrealEngine/
https://github.com/demensdeum/SurrealEngine-Emscripten

https://github.com/dpjudas/SurrealEngine

Flash Forever – Interceptor 2021

Recently, it turned out that Adobe Flash works quite stably under Wine. During a 4-hour stream, I made the game Interceptor 2021, which is a sequel to the game Interceptor 2020, written for the ZX Spectrum.

For those who are not in the know – the Flash technology provided interactivity on the web from 2000 to around 2015. Its shutdown was prompted by an open letter from Steve Jobs, in which he wrote that Flash should be consigned to history because it lagged on the iPhone. Since then, JS has become even more sluggish than Flash, and Flash itself has been wrapped in JS, making it possible to run it on anything thanks to the Ruffle player.

You can play it here:
https://demensdeum.com/demos/Interceptor2021

Video:
https://www.youtube.com/watch?v=-3b5PkBvHQk

Source code:
https://github.com/demensdeum/Interceptor-2021

Jogos de demonstração Masons-DR

Masonry-AR é um jogo de realidade aumentada onde você precisa navegar pela cidade no mundo real e coletar conhecimento maçônico de livros, obtendo moeda e capturando território para sua ordem maçônica. O jogo não tem relação com nenhuma organização real, todas as partidas são aleatórias.

Demonstração do jogo:
https://demensdeum.com/demos/masonry-ar/client

Vicky:
https://demensdeum.com/masonry-ar-wiki-ru/

Código fonte:
https://github.com/demensdeum/Masonry-AR

Corredor de motor de aço flamejante

Apresento a sua atenção Flame Steel Engine Runner – plataforma para lançamento de aplicativos multimídia baseados no kit de ferramentas Flame Steel Engine. As plataformas suportadas são Windows, MacOS, Linux, Android, iOS, HTML 5. A ênfase do desenvolvimento de código de aplicativo mudou para scripts – scripts. No momento, foi adicionado suporte para JavaScript usando TinyJS, o próprio kit de ferramentas e o motor continuarão a ser desenvolvidos em linguagens próximas ao hardware (C, C++, Rust, etc.)
Flame Steel Engine Runner Demo
Na página abaixo você pode girar o cubo, escrever código em JavaScript, fazer upload de modelos, sons, músicas, código usando o botão Carregar arquivos e iniciar a partir do arquivo main.js usando o botão Executar.
https://demensdeum.com/demos/FlameSteelEngineRunner/

RPG de ação Space Jaguar 0.0.4

O primeiro protótipo do jogo Space Jaguar Action RPG para Webassembly:

Demorará muito para carregar (53 MB) sem indicação de carregamento.

RPG de ação Space Jaguar – simulador de vida de um pirata espacial. No momento, as mecânicas de jogo mais simples estão disponíveis:

  • voar no espaço
  • morrer
  • comer
  • dormir
  • contrate uma equipe
  • veja o fluxo inquieto e veloz do tempo

Devido à má otimização da renderização de cenas 3D na versão web, a capacidade de caminhar em um ambiente 3D não está disponível. A otimização será adicionada em versões futuras.

O código-fonte do motor, do jogo e dos scripts está disponível sob a licença do MIT. Todas as sugestões de melhoria são recebidas de forma extremamente positiva:

https://gitlab.com/demensdeum/space-jaguar -action-rpg

Capturas de tela da versão nativa para Linux:

Desenvolvimento de jogos para ZX Spectrum em C

Esse post bobo é dedicado ao desenvolvimento de um jogo para o antigo computador ZX Spectrum em C. Vamos dar uma olhada no bonitão:

Iniciou a produção em 1982 e foi produzido até 1992. Características técnicas da máquina: processador Z80 de 8 bits, 16-128kb de memória e outras extensões, como o chip de som AY-3-8910.

Como parte da competição Yandex Retro Games Battle 2019 para esta máquina, escrevi um jogo chamado Interceptor 2020. Como não tive tempo de aprender linguagem assembly para o Z80, resolvi desenvolvê-lo em C. Como conjunto de ferramentas, escolhi um conjunto pronto – z88dk, que contém compiladores C e diversas bibliotecas auxiliares para agilizar a implementação de aplicações para o Spectrum. Ele também suporta muitas outras máquinas Z80, como MSX e calculadoras Texas Instruments.

A seguir, descreverei meu voo superficial sobre a arquitetura do computador, o conjunto de ferramentas z88dk, e mostrarei como consegui implementar a abordagem OOP e usar padrões de design.

Recursos de instalação

A instalação do z88dk deve ser realizada de acordo com o manual do repositório, porém, para usuários do Ubuntu, gostaria de destacar um recurso – Se você já possui compiladores para Z80 instalados a partir de pacotes deb, então você deve removê-los, já que z88dk os acessará da pasta bin por padrão devido à incompatibilidade das versões do compilador do conjunto de ferramentas, você provavelmente não conseguirá compilar nada.< /p>

Olá, mundo

Escrever Hello World é muito fácil:

#include 

void main()
{
    printf("Hello World");
}

É ainda mais fácil compilar um arquivo tap:

zcc +zx -lndos -create-app -o helloworld helloworld.c

Para rodar, use qualquer emulador ZX Spectrum com suporte a arquivo tap, por exemplo online:
http://jsspeccy.zxdemo.org/

Desenhe na imagem em tela cheia

tl;dr As imagens são desenhadas em blocos, blocos de tamanho 8×8 pixels, os próprios blocos são incorporados na fonte Spectrum e, em seguida, a imagem é impressa como uma linha a partir dos índices.

A biblioteca de saída de sprites e blocos sp1 produz blocos usando UDG. A imagem é traduzida em um conjunto de UDGs (blocos) individuais e depois montados na tela usando índices. Deve ser lembrado que UDG é usado para exibir texto, e se sua imagem contiver um conjunto muito grande de blocos (por exemplo, mais de 128 blocos), você terá que ir além dos limites do conjunto e apagar o Spectrum padrão fonte. Para contornar essa limitação, utilizei uma base de 128 – 255 simplificando as imagens, deixando a fonte original no lugar. Sobre simplificar as imagens abaixo.

Para desenhar imagens em tela cheia você precisa se munir de três utilitários:
Gimp
img2spec
png2c-z88dk

Existe uma maneira para verdadeiros homens ZX, verdadeiros guerreiros retrô, é abrir um editor gráfico usando a paleta Spectrum, conhecendo os recursos de saída da imagem, prepará-la manualmente e carregá-la usando png2c-z88dk ou png2scr.< /p>

A maneira mais fácil – pegue uma imagem de 32 bits, mude o número de cores para 3-4 no Gimp, edite-a levemente e importe-a para img2spec para não trabalhar com restrições de cores manualmente, exporte png e converta-a em um array C usando png2c- z88dk.

Lembre-se de que, para uma exportação bem-sucedida, cada bloco não pode conter mais de duas cores.

Como resultado, você receberá um arquivo h que contém o número de blocos únicos, se houver mais de ~128, simplifique a imagem no Gimp (aumente a repetibilidade) e execute o procedimento de exportação sobre um novo .

Após a exportação, você literalmente carrega a “fonte” dos blocos e imprime o “texto” dos índices dos blocos na tela. Abaixo está um exemplo da “classe” de renderização:

// грузим шрифт в память
    unsigned char *pt = fullscreenImage->tiles;

    for (i = 0; i < fullscreenImage->tilesLength; i++, pt += 8) {
            sp1_TileEntry(fullscreenImage->tilesBase + i, pt);
    }

    // ставим курсор в 0,0
    sp1_SetPrintPos(&ps0, 0, 0);

    // печатаем строку
    sp1_PrintString(&ps0, fullscreenImage->ptiles);

Desenhando sprites na tela

A seguir descreverei um método para desenhar sprites de 16×16 pixels na tela. Não cheguei na animação e na mudança de cores, porque… É trivial que já nesta fase, presumo, tenha ficado sem memória. Portanto, o jogo contém apenas sprites monocromáticos transparentes.

Desenhamos uma imagem png monocromática 16×16 no Gimp, depois usando png2sp1sprite a traduzimos em um arquivo assembly asm, em código C declaramos arrays do arquivo assembly e adicionamos o arquivo na fase de montagem.< /p>

Após a etapa de declaração do recurso sprite, ele deve ser adicionado na tela na posição desejada, segue abaixo um exemplo do código da “classe” do objeto do jogo:

    struct sp1_ss *bubble_sprite = sp1_CreateSpr(SP1_DRAW_MASK2LB, SP1_TYPE_2BYTE, 3, 0, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2,    SP1_TYPE_2BYTE, col2-col1, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2RB,  SP1_TYPE_2BYTE, 0, 0);
    sp1_IterateSprChar(bubble_sprite, initialiseColour);

A partir dos nomes das funções você pode entender aproximadamente o significado de – aloque memória para o sprite, adicione duas colunas 8×8, adicione uma cor para o sprite.

A posição do sprite é indicada em cada quadro:

sp1_MoveSprPix(gameObject->gameObjectSprite, Renderer_fullScreenRect, gameObject->sprite_col, gameObject->x, gameObject->y);

Emulando OOP

Não existe sintaxe para OOP em C. O que você deve fazer se ainda quiser? Você precisa conectar sua mente e ser iluminado pela ideia de que não existe equipamento OOP; em última análise, tudo se resume a uma das arquiteturas de máquina, na qual simplesmente não existe o conceito de objeto e outras abstrações associadas a ele.< /p>

Esse fato me impediu por muito tempo de entender por que OOP é necessário, por que é necessário usá-lo se no final tudo se trata de código de máquina.

Porém, após trabalhar no desenvolvimento de produtos, descobri as delícias desse paradigma de programação, principalmente, é claro, flexibilidade de desenvolvimento, mecanismos de proteção de código, com abordagem correta, reduzindo entropia, simplificando o trabalho em equipe. Todos os benefícios acima decorrem de três pilares – polimorfismo, encapsulamento, herança.

Vale ressaltar também a simplificação da resolução de questões relacionadas à arquitetura de aplicações, pois 80% dos problemas de arquitetura foram resolvidos por cientistas da computação no século passado e descritos na literatura sobre padrões de projeto. A seguir, descreverei maneiras de adicionar sintaxe semelhante a OOP em C.

É mais conveniente tomar estruturas C como base para armazenar dados de uma instância de classe. Claro, você pode usar um buffer de bytes, criar sua própria estrutura para classes, métodos, mas por que reinventar a roda? Afinal, já estamos reinventando a sintaxe.

Dados da turma

Exemplo de campos de dados de “classe” do GameObject:

struct GameObjectStruct {
    struct sp1_ss *gameObjectSprite;
    unsigned char *sprite_col;
    unsigned char x;
    unsigned char y;
    unsigned char referenceCount;
    unsigned char beforeHideX;
    unsigned char beforeHideY;
};
typedef struct GameObjectStruct GameObject;

Salve nossa classe como “GameObject.h”, faça #include “GameObject.h” no lugar certo e use-a.

Métodos de classe

Levando em consideração a experiência dos desenvolvedores da linguagem Objective-C, a assinatura de um método de classe serão funções em escopo global, o primeiro argumento será sempre a estrutura de dados, seguido dos argumentos do método. Abaixo está um exemplo de “método” da “classe” GameObject:

void GameObject_hide(GameObject *gameObject) {
    gameObject->beforeHideX = gameObject->x;
    gameObject->beforeHideY = gameObject->y;
    gameObject->y = 200;
}

A chamada do método é semelhante a esta:

GameObject_hide(gameObject);

Construtores e destruidores são implementados da mesma maneira. É possível implementar um construtor como alocador e inicializador de campo, mas prefiro controlar a alocação e inicialização de objetos separadamente.

Trabalhando com memória

Gerenciamento manual de memória do formulário usando malloc e free agrupados em macros novas e de exclusão para corresponder ao C++:

#define new(X) (X*)malloc(sizeof(X))
#define delete(X) free(X)

Para objetos que são usados ​​por várias classes ao mesmo tempo, o gerenciamento de memória semi-manual é implementado com base na contagem de referências, à imagem e semelhança do antigo mecanismo ARC do Objective-C Runtime:

void GameObject_retain(GameObject *gameObject) {
    gameObject->referenceCount++;
}

void GameObject_release(GameObject *gameObject) {
    gameObject->referenceCount--;

    if (gameObject->referenceCount < 1) { sp1_MoveSprAbs(gameObject->gameObjectSprite, &Renderer_fullScreenRect, NULL, 0, 34, 0, 0);
        sp1_DeleteSpr(gameObject->gameObjectSprite);
        delete(gameObject);
    }
}

Assim, cada classe deve declarar o uso de um objeto compartilhado usando reter, liberando propriedade por meio de release. A versão moderna do ARC usa chamadas automáticas de retenção/liberação.

Som!

O Spectrum possui um tweeter capaz de reproduzir música de 1 bit; os compositores da época conseguiam reproduzir nele até 4 canais de som simultaneamente.

O Spectrum 128k contém um chip de som separado AY-3-8910, que pode reproduzir música rastreada.

Uma biblioteca é oferecida para usar o tweeter no z88dk

O que resta aprender

Eu estava interessado em conhecer o Spectrum, implementar o jogo usando o z88dk e aprender muitas coisas interessantes. Ainda tenho muito que aprender, por exemplo, o montador Z80, pois ele me permite usar toda a potência do Spectrum, trabalhar com bancos de memória e trabalhar com o chip de som AY-3-8910. Espero participar da competição no próximo ano!

Links

https://rgb.yandex
https://vk.com/sinc_lair
https://www.z88dk.org/forum/

Código fonte

https://gitlab.com/demensdeum/ zx-projects/tree/master/interceptor2020

Máscara Mortal Beta Selvagem

O jogo Death-Mask entra em status de beta público (wild beta)
A tela do menu principal do jogo foi redesenhada, foi adicionada uma visão da zona azul do labirinto tecnológico, com uma música agradável de fundo.

Em seguida, pretendo retrabalhar o controlador de jogo, adicionar movimentos suaves como nos jogos de tiro antigos, modelos 3D de caixas, armas, inimigos de alta qualidade, a capacidade de passar para outros níveis do labirinto tecnológico não apenas através de portais ( elevadores, portas, quedas por buracos no chão, buracos nas paredes), acrescentarei um pouco de variedade ao ambiente do labirinto gerado. Também trabalharei no equilíbrio do jogo.
A animação do esqueleto será adicionada como uma fase de polimento antes do lançamento.< /p>