Descrição do algoritmo lógico e análise de um exemplo de trabalho na forma de um jogo de demo tecno
Versão WebGL2 desta demonstração https://danilw.itch.io/flat-maze-web para outros links, consulte o artigo.
O artigo está dividido em duas partes, primeiro sobre lógica e a segunda parte sobre a aplicação no jogo, a primeira parte :
- Principais Funcionalidades
- Links e uma breve descrição.
- O algoritmo da lógica.
- As limitações da lógica. Erros / recursos e erros de ângulo.
- Acesso aos dados do índice.
Mais descrição da demo do jogo, a segunda parte :
- Recursos usados dessa lógica. E renderização rápida de um milhão de partículas de pixel.
- Implementação, alguns comentários sobre o código, descrição da colisão em duas direções. E interação com o jogador.
- Links para gráficos usados com opengameart e um shader para sombras. E o link do artigo para cyberleninka.ru
Parte 1
1. Principais Recursos
A idéia é uma colisão / física de centenas de milhares de partículas entre si, em tempo real, onde cada partícula tem um identificador único.
Quando cada partícula é indexada, é possível controlar quaisquer parâmetros de qualquer partícula , por exemplo, massa, sua saúde (hp) ou dano, aceleração, desaceleração, que objetos encontram e reagem ao evento dependendo do tipo / índice da partícula, também temporizadores exclusivos para cada partícula e assim por diante, conforme necessário.
Toda a lógica no GLSL é totalmente portátil para qualquer mecanismo de jogo e qualquer sistema operacional com suporte ao GLES3.
O número máximo de partículas é igual ao tamanho do buffer de estrutura (fbo, todos os pixels).
Um número confortável de partículas (quando há espaço para as partículas interagirem) é (Resolution.x*Resolution.y/2)/2
é cada segundo pixel em x
cada segundo pixel em y
, e é por isso que a descrição lógica diz isso.
Na primeira parte do artigo, a lógica mínima é mostrada, na segunda no exemplo do jogo, lógica com um grande número de condições de interação.
2. Links e breve descrição
Eu fiz três demos sobre essa lógica:
1. No GLSL fragment-shader , no shadertoy https://www.shadertoy.com/view/tstSz7 , consulte o código BufferC em toda a lógica. Esse código também permite exibir centenas de milhares de partículas com seus UV, em uma posição arbitrária, em um shader de fragmentos sem usar partículas instanciadas.

2. Portando a lógica para partículas instanciadas (usadas por Godot como um motor)
Links Versão da Web , exe (win) , fontes de projeto de partículas_2D_self_collision .
Breve descrição: Esta é uma péssima demonstração de partículas instanciadas , devido ao fato de eu fazer o aumento máximo em que todo o mapa é visível, partículas de 640x360 (230k) são sempre processadas, isso é muito. Veja abaixo na descrição do jogo, lá fiz o certo, sem partículas extras. (há um erro no índice de partículas no vídeo, isso é corrigido no código)
3. O jogo, sobre isso abaixo na descrição do jogo. Links Versão Web , exe (vitória) , fontes
3. O algoritmo da lógica
Resumidamente:
A lógica é semelhante à areia caindo, cada pixel preserva o valor fracionário da posição (alterna dentro do pixel) e a aceleração atual.
A lógica verifica os pixels no raio 1, se sua próxima posição deseja ir para esse pixel (por causa dessa restrição, veja as restrições abaixo) , também os pixels no raio 2 para repulsão (colisão).
O índice exclusivo é salvo convertendo a lógica em int-float e reduzindo o tamanho da posição pos
e velocidade pos
.
Os dados são armazenados desta maneira: (devido a esse bug, consulte restrições)
pixel.rgba r=[0xfffff-posx, 0xf-data] g=[0xfffff-posy, 0xf-data] b=[0xffff-velx, 0xff-data] a=[0xffff-vely, 0xff-data]

No código , os números de linha do BufC https://www.shadertoy.com/view/tstSz7 , 115 verificação de transição, 139 verificações de colisão.
Estes são loops simples para obter valores adjacentes. E a condição é que, se a posição for tomada igual à posição do pixel atual, moveremos esses dados para esse pixel (por causa dessa restrição) , e o valor de vel
mudará dependendo dos pixels vizinhos, se houver.
Isso tudo é lógica de partículas.
É melhor colocar partículas a uma distância de 1 pixel uma da outra, se estiverem mais próximas de 1 pixel, haverá repulsa, como um exemplo de mapa com um labirinto no jogo, as partículas ficam em seus lugares sem se moverem por causa da distância de 1 pixel entre elas.
Em seguida, vem a renderização (renderização), no caso do fragment-shader, os pixels são tirados no raio de 1 para exibir as áreas que se cruzam. No caso de partículas instanciadas, um pixel é obtido no endereço INSTANCE_ID
convertido de uma visualização linear em uma matriz bidimensional.
4. Limitações da lógica. Bugs / recursos e ANGLE bugs
- O tamanho do pixel ,
BALL_SIZE
no código, deve estar dentro dos limites de cálculo, maiores que sqrt(2)/2
e menores que 1
. Quanto mais próximo de 1, menos espaço para caminhar dentro do pixel (o próprio pixel), menor é o espaço. Esse tamanho é necessário para que os pixels não caiam um no outro, menos de 1 pode ser definido quando você tiver objetos pequenos; é criada uma ilusão de objetos com menos de 1 pixel (calculado). - A velocidade não pode ser superior a
1
pixel, caso contrário, os pixels desaparecerão. Mas você pode ter uma velocidade superior a 1
por quadro - se você fizer vários framebuffer (fbo / viewport) e processar várias etapas lógicas ao mesmo tempo, a velocidade do quadro aumentará pelo número de vezes igual ao número de fbo adicionais. Foi o que fiz na demonstração de frutas e usando o link para shadertoy (o bufC copiou para o bufD). - Limitação de pressão (como gravidade ou outro mapa de força normal). Se vários pixels vizinhos tomarem a posição disso (veja a imagem acima), apenas um será salvo, os pixels restantes desaparecerão. Isso é fácil de notar na demonstração do shadertoy, defina o mouse para Forçar, altere o valor de
MOUSE_F
em Comum para 10
e direcione as partículas para o canto da tela, elas desaparecerão uma na outra. Ou o mesmo com o valor da gravidade maxG
em Comum . - Bug em ângulo. Para que essa lógica funcione nas partículas da GPU (instanciadas), é melhor (mais barato, mais rápido) calcular a posição e todos os outros parâmetros de partículas para exibição no shader de instância . Como Angle não permite o uso de mais de uma textura fbo para um shader, portanto, o cálculo de parte da lógica deve ser transferido para o Vertex-shader, para onde transferir o número de índice do shader da instância. Foi o que fiz nas duas demos com partículas de GPU.
- Um bug sério nas duas demos (exceto no jogo), o valor da posição será perdido se não for múltiplo de
1/0xfffff
teste de bug está aqui https://www.shadertoy.com/view/WdtSWS
Mais precisamente, isso não é um bug, e deve ser assim, por simplicidade, como parte desse algoritmo, chamei de bug.
Corrigir bug:
Não converta o valor da posição em int-float , pois esse 0xff
desaparecerá, 8 bits disponíveis para dados, mas o valor 0xffff
para dados permanecerá, o que pode ser suficiente para muitas coisas.
Fiz exatamente isso na demo do jogo , uso apenas 0xffff
para os dados em que o tipo de partícula, o cronômetro de animação e a saúde estão armazenados e ainda há espaço livre.
5. Acesso aos dados do índice
partícula instanciada tem seu próprio INSTANCE_ID
, pega um pixel da textura do buffer de estrutura com lógica de partículas (bufC, exemplo para sombreador), se descompactarmos a partícula (consulte o armazenamento de dados) ID dessa partícula , por esse ID , lemos a textura com dados para partículas (bufB , um exemplo em um sombreador).
No exemplo shadertoy, o bufB armazena apenas a cor de cada partícula, mas é óbvio que pode haver dados, como massa, aceleração, desaceleração escritos anteriormente, bem como quaisquer ações lógicas (por exemplo, você pode mover qualquer partícula para qualquer posição (teleporte), se isso for feito. ação lógica correspondente no código), você também pode controlar o movimento de qualquer partícula ou grupo do teclado ...
Quero dizer que você pode fazer qualquer coisa com cada uma das partículas como se fossem partículas comuns em uma matriz no processador; o acesso bidirecional da partícula da GPU pode mudar seu estado, mas também da CPU você pode alterar o estado da partícula por índice (usando ações e textura lógicas buffer de dados).
Parte 2
1. Recursos usados dessa lógica. E renderização rápida de um milhão de partículas de pixel
O tamanho do buffer de estrutura (fbo / viewport) para partículas é 1280x720, as peças estão localizadas após 1; são 230 mil partículas ativas (elementos ativos no labirinto).
Sempre há no máximo 12 mil partículas instanciadas na GPU na tela.
Usos lógicos:
- A lógica do player é separada da lógica de partículas e somente lê dados do buffer de partículas.
- O jogador diminui a velocidade ao colidir com objetos.
- Objetos do tipo monstro causam dano ao jogador.
- O jogador tem 2 ataques, um repele tudo ao redor, o segundo cria partículas como bola de fogo (a imagem é assim)
- O tipo bola de fogo tem sua própria massa, e o rastreamento bilateral de colisões com outras partículas funciona.
- outras partículas como elenco e zumbis (um tipo de elenco é invulnerável) são destruídas em uma colisão com uma bola de fogo
- bola de fogo apaga após uma colisão
- níveis de física - árvores e quadrados são repelidos pelo jogador, outras partículas não interagem, nenhuma aceleração atua na bola de fogo
- temporizadores de animação são exclusivos para cada partícula
Comparado à demonstração de frutas, onde há sobrecarga, neste jogo o número de partículas instanciadas por GPU é de apenas 12 mil.
É assim:

Seu número depende do zoom atual ( zoom ) do mapa e o aumento é limitado a um determinado valor; portanto, apenas aqueles que são visíveis na tela são considerados.
A tela muda com o player, a lógica para calcular os turnos é um pouco complexa e muito situacional, duvido que ela encontre aplicação em outro projeto.
2. Implementação, alguns comentários sobre o código.
Todo o código do jogo está na GPU.
A lógica para calcular o deslocamento de partículas na tela com um aumento na função de vértice no arquivo /shaders/scene2/particle_logic2.shader é um arquivo de sombreador de partículas (vértice e fragmento), não um sombreador instanciado, o sombreador instanciado não faz nada, apenas passa seu índice devido a bug descrito acima.
partículas por tipo e toda a lógica da interação de partículas em um arquivo, este é um arquivo de um arquivo shader / frame2 particle / shader / particle_fbo_logic.shader shader
// 1-2 ghost // 3-zombi // 4-18 blocks // +20 is on fire // 40 is bullet(right) 41 left 42 top 43 down
pixel de armazenamento de dados [pos.x, pos.y, [0xffff-vel.x, 0xff-data1],[0xffff-vel.y, 0xff-data2]]
data1 é um tipo, data2 é um HP ou timer.
O cronômetro entra em quadros em cada partícula , o valor máximo do cronômetro é 255, não preciso muito, uso apenas 1-16 no máximo ( 0xf
) e 0xf
permanece sem uso, onde, por exemplo, você pode armazenar o valor real da HP, não é usado para mim. (ou seja, sim, eu uso 0xff
para o timer , mas na verdade só tenho menos de 16 quadros de animação e 0xf
suficiente, mas não 0xf
dados adicionais)
Na verdade, 0xff
usado apenas no cronômetro de queima de árvores, elas se transformam em zumbis após 255 quadros. A lógica do timer está parcialmente no type_hp_logic
no shader de framebuffer de partículas (link acima).
Um exemplo de operação de colisão bidirecional quando uma bola de fogo dispara no primeiro golpe, e o objeto com o qual ele foi atingido também executa sua ação.
Arquivo shaders / scene2 / particulas_fbo_logic.shader linha 438:
if (((real_index == 40) || (real_index == 41) || (real_index == 42) || (real_index == 43)) && (type_hp.y > 22)) { int h_id = get_id(fragCoord + vec2(float(x), float(y))); ivec2 htype_hp = unpack_type_hp(h_id); int hreal_index = htype_hp.x; if ((hreal_index != 40) && (hreal_index != 41) && (hreal_index != 42) && (hreal_index != 43)) type_hp.y = 22; } else { if (!need_upd) { int h_id = get_id(fragCoord + vec2(float(x), float(y))); ivec2 htype_hp = unpack_type_hp(h_id); int hreal_index = htype_hp.x; if (((hreal_index == 40) || (hreal_index == 41) || (hreal_index == 42) || (hreal_index == 43)) && (htype_hp.y > 22)) { need_upd = true; } } }
real_index
é um tipo, os tipos estão listados acima, 40-43 é uma bola de fogo .
type_hp.y > 22
é o valor do timer, se for maior que 22, a bola de fogo não encontrou nada.
h_id = get_id(...
pega o valor do tipo e HP (timer) da partícula encontrada
hreal_index != 40...
tipo ignorado (outra bola de fogo )
type_hp.y = 22
um timer está definido como 22, este é um indicador de que esta bola de fogo colidiu com um objeto.
else { if (!need_upd)
variável need_upd verifica se não há colisões repetidas, já que a função está em loop, encontramos uma bola de fogo .
h_id = get_id(...
se ainda não houve uma colisão, pegamos um objeto de tipo e timer.
hreal_index == 40...htype_hp.y > 22
que o objeto de colisão é bola de fogo e não sai.
need_upd = true
sinalizador de que é necessário atualizar o tipo, pois encontrou uma bola de fogo .
linha adicional 481
if((need_upd)&&(real_index<24)){
real_index <24 por tipo menor que 24, existem árvores zumbis e fantasmas que não queimam e, nessa condição, atualizamos o tipo, dependendo do tipo atual.
Assim, quase qualquer interação de objetos pode ser feita.
Interação com o jogador:
Arquivo shaders / scene2 / logic.shader linha 143 function player_collision
Essa lógica lê os pixels ao redor do player em um raio de 4x4 pixels, toma a posição de cada um dos pixels e a compara com a posição do jogador; se um elemento for encontrado, a verificação de tipo será a próxima; se este for um monstro, tiraremos HP do player.
Isso funciona um pouco impreciso e eu não queria corrigi-lo , essa função pode ser mais precisa.
Partículas se afastam do jogador e o efeito repulsivo durante um ataque:
Um framebuffer (viewport) é usado para escrever o normal das ações atuais, e as partículas ( particulas_fbo_logic.shader ) tomam essa textura (do normal) em sua posição e aplicam o valor à sua velocidade e posição. Todo o código dessa lógica é literalmente apenas algumas linhas, arquivo force_collision.shader
Com o clique do botão esquerdo do mouse, os projéteis de bola de fogo voam; sua aparência não é muito natural , eles não corrigiram e saíram desta forma.
Você pode criar uma zona (forma) normal para gerar partículas com um turno aparecendo em relação ao jogador (isso não é feito).
Ou você pode transformar a bola de fogo em um objeto separado como jogador e atrair o normal para um buffer para afastar as partículas da bola de fogo , ou seja, por analogia com o jogador ...
Quem precisa pensar que descobrirá por si mesmo.
3. Links para os gráficos usados com opengameart e o shadow shader
Recebi um link para um artigo sobre cyberleninka.ru
Na qual a descrição do algoritmo que eu usei, talvez haja uma descrição mais detalhada e correta do que neste, meu artigo.
O sombreador de sombra funciona de maneira muito simples, com base nesse sombreador https://www.shadertoy.com/view/XsK3RR (eu tenho um código modificado)
Shader cria mapa de luz radial 1D

e sombreamento no código de pintura do piso shaders / scene2 / mainImage.shader
Links para os gráficos utilizados , todos os gráficos do jogo no site https://opengameart.org
bola de fogo https://opengameart.org/content/animated-traps-and-obstacles
personagem https://opengameart.org/content/legend-of-faune
árvores e blocos https://opengameart.org/content/lolly-set-01
(e mais algumas fotos com opengameart)
Os gráficos no menu foram obtidos pelo sombreador 2D_GI, um utilitário para criar esses menus:
Quem leu até o fim - muito bem :)
Se você tiver alguma dúvida, pergunte, eu posso complementar a descrição mediante solicitação.