Um novo jogo com uma atmosfera antiga no Three.js. Parte 2

Na primeira parte, falei sobre os problemas que encontrei no processo de criação de um jogo em 3D para o navegador usando o Three.js. Agora, gostaria de me aprofundar na solução de alguns problemas importantes ao escrever um jogo, como construir níveis, detectar colisões e adaptar a imagem a qualquer proporção da janela do navegador.


Diagramas de nível


Na verdade, os próprios níveis são criados no editor 3D, a saber, sua geometria, mapeamento de textura, cozimento de sombras, etc. Eu descrevi tudo isso na primeira parte. Por que existem outros esquemas? O fato é que o Three.js não oferece nenhum tipo de mecanismo físico, e eu uso esquemas de níveis para identificar obstáculos.


O Three.js para resolver o problema de colisão oferece apenas raytracing - a maneira mais simples de determinar a interseção da geometria dos objetos. Em princípio, ele pode ser usado, e eu até fiz isso em um dos meus outros projetos. Era uma cidade virtual diretamente no site, no navegador. Você pode se deslocar pela cidade e não passar pelas paredes.


No caso em que a interseção da geometria do jogador com o edifício ocorre durante o movimento, implementei a repulsa do jogador por uma certa distância na direção oposta à parede. Mas para isso, os objetos devem ser paralelepípedos. Em torno de alguns objetos complexos, criei colisores (chamaremos os objetos invisíveis que desempenham o papel de obstáculos e impedem o jogador de passar por eles mesmos), através do qual as interseções foram realizadas. E as partes inferiores de alguns edifícios, que são simplesmente “caixas”, eram algumas vezes usadas como colididores.


Em objetos geometricamente complexos, o traçado de raios pode não funcionar ou se comportar de maneira inadequada. E, como solução, você pode incorporar no objeto não um, mas vários pequenos coletores invisíveis na forma de paralelepípedos com 100% de transparência, desenhados um ao lado do outro e uns sobre os outros, repetindo aproximadamente a forma do objeto.

No jogo sobre masmorras, o nível é um único objeto longo com movimentos cortados para mover o jogador. Na verdade, para resolver o problema da colisão, é possível colar coletores invisíveis sempre que necessário e usar raytracing. No entanto, eu decidi seguir o outro caminho.


  • Em primeiro lugar, eu queria automatizar o processo de criação de uma variedade de coletores.
  • Em segundo lugar, você pode usar apenas informações sobre os coletores, ou seja, suas coordenadas no espaço, e não carregar a cena 3D em si com alguns objetos vazios extras.
  • Em terceiro lugar, como o jogo usa apenas uma vista lateral e uma das coordenadas nunca muda ao se mover, você pode usar o cálculo de interseções em apenas duas coordenadas.
  • E quarto, afinal, de fato, haverá um esquema de níveis. Além disso, criar novos níveis é apenas conveniente, começando com esse esquema. Você pode simplesmente arrastar blocos pela tela em qualquer editor de gráficos, construindo novos corredores e obstáculos e, em seguida, execute o script e obtenha informações sobre os coletores. Ou seja, o problema do editor de níveis está parcialmente resolvido.

Eu escrevi um script que usa parâmetros de entrada como o nome do arquivo do esquema de níveis (png) e a cor, cujo preenchimento é interpretado como um obstáculo. A cor padrão do espaço livre é preta. Para processamento pelo script, o esquema de cada nível deve ser salvo em um arquivo png separado. Por exemplo, para o nível mais baixo, fica assim:


Concordei que um bloco deveria ter 80 pixels de largura e 48 pixels de altura. Isso corresponde a 4 x 2,4 metros no mundo 3D. Seria possível criar 40 x 24 pixels, ou seja, dez vezes, mas na imagem parece pequena.

O resultado do script no primeiro nível (a imagem é cortada à direita):


O script é executado no navegador. Eu acho que não há nenhum ponto na marcação html, é fundamental: campos de entrada de dados e um botão Iniciar. Em seguida, a imagem lida é exibida na tela. E, como resultado do script, uma matriz é exibida sob a imagem na escala mundial 3D, que contém as coordenadas inferior esquerda e superior direita de cada bloco e com o deslocamento especificado no script para cada nível. Essa matriz pode ser copiada e colada na lista de coletores para usar no jogo (mais sobre isso abaixo), ela será armazenada em algum tipo de constante. As coordenadas também aparecem na própria imagem, mas no quadro de referência da imagem 2D. Esses números são exibidos no centro de cada bloco e permitem verificar se todos os blocos estão incluídos no cálculo. Sozinhos, esses números não são necessários para nada, exceto para inspeção visual. Alguns blocos, como colunas entre as quais o jogador passa, não devem ser contados. Sobre quais objetos são excluídos do cálculo - abaixo.

Além disso, por exemplo, no segundo nível, existem finas placas horizontais nas quais o jogador caminha. Eles devem ser considerados. Portanto, você precisa garantir que os números também apareçam neles. No diagrama, faça 2 pixels de altura.



Agora, sobre como o script leva em conta os blocos:

  • O esquema é processado por blocos de 80x48, em cada um dos quais uma área é obtida do 2º ao 79º pixels horizontalmente e do 2º ao 47º pixels verticalmente. O primeiro e o último pixels não são usados ​​para que, ao redor dos blocos, você possa criar uma moldura preta com uma largura de 1 pixel, isso melhore a percepção visual do circuito e facilite sua criação.
  • Todos os pixels da linha superior do bloco são visualizados. Se houver cores entre elas, as coordenadas do bloco vão do primeiro ao último pixel colorido horizontalmente e até a altura total do bloco verticalmente na matriz final. Este será um bloco em branco na largura total ou parcial.
  • Todos os pixels da linha inferior do bloco são visualizados. Se houver cores entre elas, mas não houver uma colorida na linha superior, as coordenadas do bloco vão do primeiro ao último pixel colorido horizontalmente e 3 pixels verticalmente da parte inferior à matriz final. Esta será uma plataforma sobre a qual andar. Dentro de um bloco, pode haver várias plataformas horizontais. As plataformas são reconhecidas apenas na parte inferior do bloco. As coordenadas da plataforma são "afundadas" em um bloco, localizado abaixo, para que a superfície da plataforma fique no mesmo nível dos blocos vizinhos - não das plataformas.
  • As colunas e outras decorações dentro de um bloco vazio não são processadas, pois apenas a linha superior e inferior de pixels é considerada. Portanto, dentro do bloco, você pode colocar uma decoração, explicações para o diagrama, ponteiros, colunas, etc., sem medo de que isso afete de alguma forma o resultado do script.

Todas as coordenadas recebidas da matriz são traduzidas para a escala do mundo 3D, multiplicada pelo coeficiente de sua escala (que foi selecionada no editor 3D quando foi criada). A matriz está pronta para uso no jogo. O código do script foi escrito às pressas, por isso não finge ser elegante, mas executa sua tarefa.

Código
ap = { //      (  ),   3D   lvd: { 'lv01.png': { invw: false, invh: true, level_dw: -8.5, level_dh: -1.5 }, 'lv02.png': { invw: true, invh: true, level_dw: -19.5, level_dh: -5.5 } }, blockw: 80, //   2D blockh: 48, //   2D sc3d: 0.05, //,   3D  ex: 100, //  3D (-   ) v: { data: [] }, i: 0, par: {}, datai: [], resi: [], ars: [], fStopEncode: false, blockColor: function(cl) { document.getElementById('input_cl').value = cl; }, startEncode: function() { //      for (var key in ap.lvd) { ap.lvd[key].dw = ap.lvd[key].level_dw * ap.blockw; ap.lvd[key].dh = ap.lvd[key].level_dh * ap.blockh; }; document.getElementById('startbtn').style.display = 'none'; document.getElementById('startmsg').style.display = 'block'; var cl = document.getElementById('input_cl').value; var fld = document.getElementById('input_fld').value; var nm = document.getElementById('input_nm').value; ap.nm = nm; ap.par = { path: [fld + '/', nm], key: [nm], cl: aplib.hexToRgb(cl.substring(1, 7)) }; setTimeout(function() { ap.datai[ap.par.key] = new Image(); ap.datai[ap.par.key].onload = function() { ap.parseData(); }; ap.datai[ap.par.key].src = ap.par.path[0] + ap.par.path[1]; }, 500); }, stopEnode: function(e) { if (typeof ap !== "undefined") { if (e.keyCode == 27) { console.log('stop'); ap.fStopEncode = true; }; }; }, parseData: function() { ap.w = ap.datai[ap.par.key[0]].width, ap.h = ap.datai[ap.par.key[0]].height; aplib.initCanv(ap.w, ap.h); ctx.drawImage(ap.datai[ap.par.key[0]], 0, 0, ap.w, ap.h, 0, 0, ap.w, ap.h); ap.ars = []; ap.i = 0; setTimeout(function() { ap.parseData1(); }, 1000); }, parseData1: function() { if (ap.i < ap.par.key.length) { document.getElementById('info').innerHTML = '' + ap.nm; ap.blocksw = Math.floor(ap.w / ap.blockw); ap.blocksh = Math.floor(ap.h / ap.blockh); ap.ar = []; ap.arv = {}; ap.hi = 0; ctx.fillStyle = '#CCCCCC'; ap.parseData2(); } else { document.getElementById('startbtn').style.display = 'block'; document.getElementById('startmsg').style.display = 'none'; }; }, parseData2: function() { if (ap.hi < ap.blocksh) { ap.ar.push([]); ap.wi = 0; ap.parseData3(); } else { ap.parseData4(); }; }, parseData3: function() { var k = ''; if (ap.wi < ap.blocksw) { var fground = true, fvari = false, fempty = true; var upx1 = 0, upx2 = 0, dnx1 = 0, dnx2 = 0; var upxf = false, dnxf = false; for (var wii = 1; wii < ap.blockw - 2 + 2; wii++) { pixelDatai = ctx.getImageData(ap.wi * ap.blockw + wii, ap.hi * ap.blockh + 1, 1, 1).data; //  pixelDatai2 = ctx.getImageData(ap.wi * ap.blockw + wii, (ap.hi + 1) * ap.blockh - 3, 1, 1).data; //  if ((pixelDatai[0] == ap.par.cl.r) & (pixelDatai[1] == ap.par.cl.g) & (pixelDatai[2] == ap.par.cl.b)) { //   ground    if (upxf == false) { upxf = true; upx1 = wii; }; } else { //    if (upxf == true) { upx2 = wii + 1; upx1--; //   dy = -1; // 3D       1 ap.v.data.push([ap.wi * ap.blockw + upx1, ap.hi * ap.blockh + dy, ap.wi * ap.blockw + upx2, ap.hi * (ap.blockh) + ap.blockh - 1]); upxf = false; upx1 = 0; upx2 = 0; }; }; if ((pixelDatai2[0] == ap.par.cl.r) & (pixelDatai2[1] == ap.par.cl.g) & (pixelDatai2[2] == ap.par.cl.b)) { //   ground     if (upxf == false) { if (dnxf == false) { dnxf = true dnx1 = wii; }; }; } else { if (upxf == false) { if (dnxf == true) { dnx2 = wii + 1; dnx1--; //   dy = 2; // 3D    2 ap.v.data.push([ap.wi * ap.blockw + dnx1, (ap.hi + 1) * ap.blockh - 3 + dy, ap.wi * ap.blockw + dnx2, (ap.hi + 1) * ap.blockh - 3 + 2 + dy]); dnxf = false; dnx1 = 0; dnx2 = 0; }; }; }; }; if (ap.fStopEncode == true) { ap.hi = ap.h, ap.wi = ap.w, i = ap.par.key.length; }; setTimeout(function() { ap.wi++; ap.parseData3(); }, 10); } else { ap.hi++; ap.parseData2(); }; }, parseData4: function() { setTimeout(function() { var t, tw, tx, ty, ar = []; //  for (var i = 0; i < ap.v.data.length; i++) { ar = ap.v.data[i]; t = ar[0] + ';' + (ar[1]+1) + '<br/>' + ar[2] + ';' + (ar[3]+1); tw = ar[2] - ar[0]; tx = ar[0]; ty = ar[1] + Math.floor((ar[3] - ar[1]) / 2) - 0; aplib.Tex2Canvas(ctx, t, 'normal 10px Arial', 10, '#CCCCCC', tx, ty, tw, 0, 'center', 'top'); }; ap.parseData5(); }, 10); }, parseData5: function() { var t, tw, tx, ty, ar = [], n; //   3D var lv = ap.lvd[ap.nm]; for (var i = 0; i < ap.v.data.length; i++) { ar = ap.v.data[i]; ar[0] += lv.dw; ar[1] += lv.dh; ar[2] += lv.dw; ar[3] += lv.dh; if (lv.invh == true) { n = -ar[1]; ar[1] = -ar[3]; ar[3] = n; }; if (lv.invw == true) { n = -ar[0] ar[0] = -ar[2]; ar[2] = n; }; ar[0] = Math.round(ap.sc3d * ar[0] * ap.ex) / ap.ex; ar[1] = Math.round(ap.sc3d * ar[1] * ap.ex) / ap.ex; ar[2] = Math.round(ap.sc3d * ar[2] * ap.ex) / ap.ex; ar[3] = Math.round(ap.sc3d * ar[3] * ap.ex) / ap.ex; }; //    ap.v.data.sort(aplib.sortBy0); console.log(ap.v.data); document.getElementById('divresult').innerHTML = JSON.stringify(ap.v.data); } }; aplib = { hexToRgb: function(hex) { var arrBuff = new ArrayBuffer(4); var vw = new DataView(arrBuff); vw.setUint32(0, parseInt(hex, 16), false); var arrByte = new Uint8Array(arrBuff); return { r: arrByte[1], g: arrByte[2], b: arrByte[3], s: arrByte[1] + "," + arrByte[2] + "," + arrByte[3] }; }, //   canvas Tex2Canvas: function(ctx, t, font, lin, fcolor, x, y, w, h, haln, valn) { //left, right, center, center-lim- ctx.font = font; ctx.fillStyle = fcolor; var l = 0; var tx = x; var ftw = false; var tw = 1; var arr = t.split('<br/>'); for (var i = 0; i < arr.length; i++) { arr[i] = arr[i].split(' '); }; for (var i = 0; i < arr.length; i++) { var s = '', slen = 0, s1 = '', j = 0; while (j < arr[i].length) { var wordcount = 0; while ((slen < w) & (j < arr[i].length)) { s = s1; s1 = s + arr[i][j] + ' '; slen = ctx.measureText(s1).width; if (slen < w) { j++; wordcount++; } else { if (wordcount > 0) { s1 = s; } else { j++; }; }; }; ftw = false; tw = ctx.measureText(s1).width; if (haln == 'center') { tx = x + Math.round((w - tw) / 2); }; if (haln == 'right') { tx = x + Math.round((w - tw)); }; if (haln == 'center-lim') { if (tw > w) { tw = w; }; if (tw < 1) { tw = 1; }; tx = x + Math.round((w - tw) / 2); ftw = true; }; if (ftw == false) { ctx.fillText(s1, tx, l * lin + y); } else { ctx.fillText(s1, tx, l * lin + y, tw); }; if (s1 == '') { j = arr[i].length + 1; }; l++; s1 = ''; slen = 0; }; }; return Math.round(tw); }, // canvas initCanv: function(w, h) { function canvErr() { document.getElementById('divcanv').innerHTML = '<div style="height:130px"></div><div style="width:440px; border:#FFFFFF 1px solid; margin:10px; padding:4px; background-color:#000000"><p class="txterr">---> Error<br/>HTML5 Canvas is not supported!<br/>Please, update your browser!</p></div>'; }; if (w == 0) { w = 740; h = 680; }; elcanv = document.getElementById('divcanv'); elcanv.innerHTML = '<canvas id="canv" style="width:' + w + 'px; height:' + h + 'px; display:block;" width="' + w + '" height="' + h + '"></canvas>'; canvas1 = document.getElementById('canv'); if (!canvas1) { canvErr(); return 0; } else { if (canvas1.getContext) { ctx = canvas1.getContext('2d'); ctx.clearRect(0, 0, w, h); return 1; } else { canvErr(); }; }; }, sortBy0: function(i, ii) { if (i[0] > ii[0]) return 1; else if (i[0] < ii[0]) return -1; else return 0; } }; 


Agora - sobre como o jogo funciona com uma variedade de blocos. O jogo usa corredores que se cruzam (níveis). Quando um jogador se transforma em um corredor, um novo conjunto de blocos é conectado: e para cada corredor, consequentemente, seu próprio conjunto é obtido, obtido a partir de seu esquema de níveis. Durante o movimento do jogador, suas coordenadas são verificadas quanto a estar dentro de cada bloco. E se ele estiver dentro de qualquer quarteirão, teremos uma colisão. Mas a cada movimento do jogador, não precisamos procurar cruzamentos com todos os blocos do nível, porque pode haver muitos deles. Crie uma matriz apenas dos blocos mais próximos do jogador.

 collisionsUpdate: function(x, y, dw, dh) { var coll = []; var o; for (var i = 0; i < ap.v.lv.d.length; i++) { o = ap.v.lv.d[i]; if ((o[0] >= x - ap.v.dw) & (o[2] <= x + ap.v.dw)) { if ((o[1] >= y - ap.v.dh) & (o[3] <= y + ap.v.dh)) { coll.push(o); }; }; }; ap.v.coll = coll; }, 

Aqui, na entrada x, y, estão as coordenadas atuais do jogador, dw, dh, a distância na qual você deseja procurar blocos na horizontal e na vertical, por exemplo, 12 e 8 metros. Em outras palavras, pegue todos os blocos ao redor do jogador em um quadrado de 24x16 metros. Eles participarão da busca por confrontos. ap.v.lv.d [i] é um elemento de uma matriz de blocos do nível atual; na verdade, ele próprio também é uma matriz de 4 números que definem os limites de um bloco - [x1, y1, x2, y2], portanto, para verificar o quadrado horizontalmente, pegamos elementos com os índices 0 e 2 e verticalmente - 1 e 3. Se houver uma correspondência, adicione esse bloco à lista de colisões ap.v.coll.

Quando o jogador se move, atualizaremos esta lista de colisões, mas para economizar desempenho, não faremos isso a cada passo (ou melhor, renderizar o quadro), mas quando o jogador deixar um determinado quadrado, um pouco menor, especificado em ap.v.collwStep e ap.v.collhStep, por exemplo, 8 e 4 metros. Ou seja, remontaremos a matriz de colisão novamente quando o jogador passar por um determinado caminho horizontal ou verticalmente de sua posição original. Ao mesmo tempo, lembremos da posição em que remontamos o array para usá-lo na próxima iteração. pers [ax] - aqui por ax, queremos dizer o eixo de coordenadas (ax), pode ser x ou z, dependendo da direção do corredor ao longo do qual o jogador está andando.

 //   if ((Math.abs(pers[ax] - ap.v.collw) > ap.v.collwStep) || (Math.abs(pers.y - ap.v.collh) > ap.v.collhStep)) { ap.v.collw = pers[ax]; ap.v.collh = pers.y; ap.collisionsUpdate(pers[ax], pers.y, 12, 8); }; 

Por que essas dificuldades? Por que não usar todo o conjunto de colisões no nível e não tomar banho de vapor. O fato é que a detecção de colisão é realizada de acordo com um algoritmo muito mais complexo, e não é rentável verificar a colisão com absolutamente todos os blocos de nível, e não os mais próximos, a cada renderização de quadro. (Embora isso não seja preciso.)

A definição de colisões em cada renderização de um quadro usando a matriz de colisões preparada acima:

Código
 collisionsDetect: function(x, y, xOld, yOld, up) { //up=-1 -  var res = false, o; var collw = false, collh = false, collwi = false, collhi = false, collhsup = false, support = [], supportf = false, fw = false, upb = -1; var bub = -1, bubw = 0; var pw2 = ap.v.player.pw2, ph2 = ap.v.player.ph2, supportd = ap.v.supportd; for (var i = 0; i < ap.v.coll.length; i++) { o = ap.v.coll[i]; collwi = false; collhi = false; collhsup = false; fw = false; if ((x + pw2 >= o[0]) & (x - pw2 <= o[2])) { if ((y + ph2 > o[1]) & (y - ph2 < o[3])) { collwi = true; }; }; //     if ((xOld + pw2 >= o[0]) & (xOld - pw2 <= o[2])) { if ((yOld + ph2 > o[1]) & (yOld - ph2 < o[3])) { bub = i; if (Math.abs(xOld - o[0]) < Math.abs(xOld - o[2])) { bubw = -1; } else { bubw = 1; }; }; }; if ((x >= o[0]) & (x <= o[2])) { fw = true; //  i   }; if ((y + ph2 >= o[1]) & (y - ph2 <= o[3])) { if ((x > o[0]) & (x < o[2])) { collhi = true; //  if (y + ph2 > o[3]) { collhsup = true; supportf = true; support = o; upb = 1; }; //  if (y - ph2 < o[1]) { upb = -1; }; }; }; if ((y - ph2 >= o[3] + supportd - 0.11) & (y - ph2 <= o[3] + supportd + 0.001)) { if (fw == true) { collhi = true; collh = true; res = true; collhsup = true; supportf = true; support = o; }; }; if (collwi & collhi) { res = true; }; if (collwi) { collw = true; }; if (collhi) { collh = true; }; }; return { f: res, w: collw, h: collh, support: support, supportf: supportf, upb: upb, bub: bub, bubw: bubw }; }, 


Aqui x, y, xOld, yOld são as coordenadas novas e atuais do player. Os novos são calculados com o toque de um botão, com base em uma determinada velocidade, ou seja, são possíveis coordenadas. Eles são verificados para ver se estão dentro de algum bloco da lista de colisões. Se eles caírem, eles reverterão para os antigos e o jogador não passará pelo obstáculo. E se não caem, tornam-se atuais. pw2 e ph2 são metade da largura e altura do colisor imaginário do jogador (largura do jogador / 2, altura do jogador / 2). A saída é emitida se houver uma colisão horizontal e vertical (collw, collh), se houver um bloco de suporte (supportf) sob o player - isso deixa claro se é necessário iniciar a animação de queda ainda mais ou se o player simplesmente mudou para um bloco vizinho e assim por diante. Apenas não pergunte por que adicionei 0,001 lá e subtraí 0,11. É uma muleta terrível que evita cair através dos blocos e o efeito de tremulação ao colidir com um obstáculo horizontal ... Essa função funciona, mas precisa ser reescrita da maneira normal. A otimização dessa função também está ausente ainda.

Eu acho que com colisões vale a pena terminar aqui.

É difícil dizer quão rápido meu método é ou talvez mais lento que o traçado de raios, mas no último, o Three.js também armazena uma variedade de objetos que participam do sistema de colisão. Apenas as colisões são determinadas pelo método de emissão da viga e sua interseção com os planos dos lados dos objetos, e comigo, determinando se as coordenadas de um objeto estão dentro do outro ao longo de cada um dos dois eixos.

O jogo também possui objetos em movimento (tubarão) e objetos marcadores que acionam algum tipo de animação (por exemplo, o contato com a água aciona o movimento de um tubarão). Todos esses objetos também participam de colisões, e alguns com coordenadas que variam no tempo. Ali, curiosamente, tudo é mais simples: durante o movimento do objeto, suas coordenadas são comparadas com as coordenadas do jogador.


Gamepad


Em geral, manter um gamepad javascript em um navegador não é uma tarefa trivial. Não há eventos de pressionar e liberar botões. Existem apenas eventos conectando e desconectando o dispositivo e o estado que pode ser obtido por pesquisas periódicas e, em seguida, compare-o com o anterior.

Um vídeo demonstrando a operação do gamepad em um navegador em um tablet no Windows 8.1 e um PC no Windows 10. O tablet, no entanto, é antigo, lançado em 2014, de modo que a iluminação dinâmica é desativada no jogo.


Para pesquisar o gamepad, uma função chamada uma vez a cada 100 milissegundos é usada. É definido usando a função da minha biblioteca m3d.lib.globalTimer.addEvent.

 m3d.lib.globalTimer.addEvent({ name: 'gamepad', ti: 100, f: function() { var st = m3d.gamepad.state(); if (st == false) { if (contr.gpDownFlag == true) { m3d.gamepad.resetH(); }; }; } }); 

Aqui globalTimer é o sistema de gerenciamento de eventos de timer javascript setInterval que eu escrevi. Lá, simplesmente uma série de eventos é adicionada a uma determinada matriz que precisa ser chamada em intervalos diferentes. Então, um temporizador setInterval é definido com a frequência correspondente ao evento com a frequência mais alta de todas. O timer pesquisa a função m3d.lib.globalTimer.update (), que percorre a lista de todos os eventos e executa as funções daqueles que foram executados. Ao adicionar ou remover eventos, a frequência do intervalo também pode ser alterada (por exemplo, se você excluir o evento mais rápido).

O jogo também define manipuladores para cada tecla do gamepad: 'a' é para o eixo (machado), 'b' é para o botão (botão) e 11 é o desvio esquerdo ao longo do eixo horizontal da cruz (como se fosse o botão 1), 12 - o desvio correto ao longo do eixo horizontal da cruz (como se fosse o botão 2), 21 e 22 - para o eixo vertical. Por exemplo:

['a', 11],
['b', 3]

significa que a próxima função será definida ao mesmo tempo para o desvio no eixo horizontal à esquerda e para o botão 3 (esquerda). Bem, então é definida uma função que será executada quando o botão for pressionado e depois liberada.

  m3d.gamepad.setHandler( [ ['a', 11], ['b', 3] ], function(v) { if (contr.btState.lt == false) { contr.keyDownFlag = true; contr.btState.lt = true; contr.gpDownFlag = true; apcontrolsRenderStart(); }; }, function(v) { contr.btState.lt = false; m3d.contr.controlsCheckBt(); apcontrolsRenderStart(); } ); 

Aqui apcontrolsRenderStart () é uma função que inicia uma renderização se ainda não estiver em execução. Em geral, o suporte ao gamepad está intimamente ligado à minha biblioteca m3d, por isso, se eu continuar descrevendo todos os seus recursos, ele se estenderá por muito tempo ...

Vou dar apenas uma parte: gamepad, no qual implementei a inicialização do gamepad, instalação de manipuladores e pesquisas de estado da maneira mais simples.

Código
 gamepad: { connected: false, gamepad: {}, gamepadKey: '', axesCount: 0, buttonsCount: 0, f: [], //  fup: [], //  fval: [], //      fupCall: [], //   buttons: [], //link to f [0.. ] axes: [], //link to f [0.. ] initCb: function() {}, resetH: function() {}, init: function(gp) { var f = false; for (var key in gp) { if (f == false) { if (gp[key] != null) { if (typeof gp[key].id !== "undefined") { f = true; this.connected = true; this.gamepad = gp[key]; this.gamepadKey = key; }; }; }; }; if (typeof this.gamepad.axes !== "undefined") { this.axesCount = this.gamepad.axes.length; }; if (typeof this.gamepad.buttons !== "undefined") { this.buttonsCount = this.gamepad.buttons.length; }; this.f = []; this.fup = []; this.fval = []; this.fupCall = []; this.axes = []; for (var i = 0; i < this.axesCount * 2; i++) { this.axes.push(-1); }; this.buttons = []; for (var i = 0; i < this.buttonsCount; i++) { this.buttons.push(-1); }; this.initCb(); }, setHandlerReset: function(f) { this.resetH = f; }, setHandler: function(ar, f, fup) { //ar['b',3] ['a',11] var fi, bt, ax, finext, finexta; finexta = false; for (var i = 0; i < ar.length; i++) { if (ar[i][0] == 'a') { ax = Math.floor(ar[i][1] / 10); bt = ar[i][1] - (ax * 10); bt = ax * 2 + bt - 3; fi = this.axes[bt]; if (fi == -1) { //   fi = this.f.length; if (finexta == false) { finexta = true; this.f.push(f); this.fup.push(fup); this.fval.push(0); this.fupCall.push(true); this.axes[bt] = fi; } else { fi--; this.f[fi] = f; this.fup[fi] = fup; this.axes[bt] = fi; }; } else { this.f[fi] = f; this.fup[fi] = fup; }; } else if (ar[i][0] == 'b') { bt = ar[i][1] - 1; fi = this.buttons[bt]; if (fi == -1) { //   fi = this.f.length; if (finexta == false) { finexta = true; this.f.push(f); this.fup.push(fup); this.fval.push(0); this.fupCall.push(true); this.buttons[bt] = fi; } else { fi--; this.f[fi] = f; this.fup[fi] = fup; this.buttons[bt] = fi; }; } else { this.f[fi] = f; this.fup[fi] = fup; }; }; }; }, state: function() { var pressed = false; var fi, fval, axesval; for (var i = 0; i < this.fval.length; i++) { this.fval[i] = 0; }; //   var gp = navigator.getGamepads()[this.gamepadKey]; for (var i = 0; i < this.axesCount; i++) { axesval = Math.round(gp.axes[i]); if (axesval < 0) { pressed = true; fi = this.axes[i * 2]; if (fi != -1) { this.fval[fi] = gp.axes[i]; this.fupCall[fi] = true; }; } else if (axesval > 0) { pressed = true; fi = this.axes[i * 2 + 1]; if (fi != -1) { this.fval[fi] = gp.axes[i]; this.fupCall[fi] = true; }; }; }; for (var i = 0; i < this.buttonsCount; i++) { if (gp.buttons[i].pressed == true) { pressed = true; fi = this.buttons[i]; if (fi != -1) { this.fval[fi] = 1; this.fupCall[fi] = true; }; }; }; for (var i = 0; i < this.fval.length; i++) { fval = this.fval[i]; if (fval != 0) { this.f[i](this.fval[i]); } else { if (this.fupCall[i] == true) { this.fupCall[i] = false; this.fup[i](this.fval[i]); }; }; }; return pressed; } }, //gamepad 


Em geral, o suporte ao gamepad ainda está incompleto: apenas o suporte ao gamepad mais simples é implementado, mas não o que, por exemplo, é usado no XBox, porque eu não o tenho. Se eu conseguir, vou programá-lo e trabalhar com ele. Lá será possível ajustar a velocidade do personagem, ou seja, será possível mover-se a qualquer velocidade no intervalo de etapa a corrida. Isso é obtido usando parâmetros fracionários dos eixos. Meu gamepad retorna apenas números inteiros -1 e 1. Além disso, meu gamepad tem uma cruz repugnante e, quando pressionado para a esquerda ou para a direita, pressiona simultaneamente para cima ou para baixo. Portanto, eu não usei a parte superior e inferior na cruz e a dupliquei com os botões à direita do gamepad ... Com o lançamento do jogo, pretendo criar vários perfis de gamepads. Além disso, no caso de conectar vários gamepads, apenas o último será usado até o momento.

Tela responsiva


O jogo foi projetado para uma proporção de 16: 9. Mas adicionei o ajuste horizontal automático de ± 10% para que na janela expandida do navegador não houvesse barras pretas nas laterais:


E seria assim:


No modo de tela cheia, haverá 16: 9 reais. Seria possível adaptar a imagem em geral a qualquer proporção da janela do navegador, mas não o fiz, pois uma janela baixa e larga levaria a um ângulo de visão muito grande, o que não é bom do ponto de vista da jogabilidade: becos sem saída distantes, objetos, inimigos serão imediatamente visíveis e tudo o mais que o jogador ainda não precisa ver. Portanto, limitei-me a ajustar dentro de ± 10% de 16: 9. No entanto, para monitores estreitos (4: 3), percebi a capacidade de alternar de 16: 9 para o modo de adaptação de 4: 3 para 16: 9 pressionando a tecla Y. Mas não mais amplo - então, novamente, para não quebrar a jogabilidade. Ou seja, você pode reproduzir na proporção clássica de 16: 9 ou aumentar a imagem até a altura da janela cortando-a horizontalmente. Embora isso também não seja muito bom, por exemplo, em situações de fliperama, quando algo voa na direção do jogador pelo lado. Pouco tempo resta para a reação. Mas você sempre pode retornar rapidamente ao modo clássico.


A adaptação da tela, bem como todas as teclas de atalho usadas no jogo, são mostradas no vídeo a seguir:


Na verdade, a proporção é definida nas configurações do jogo.

 aspect1:{w:1280, h:720, p:10}, //16x9 +- 10% aspect2:{w:960, h:720, p:34}, //4x3 +- 34% 

E no jogo, quando você pressiona Y, ele alterna:

 contr.btCodesDn[89] = function() { //'y' if (m3dcache.setup.aspect.swch == 1) { m3dcache.setup.aspect = m3dcache.setup.aspect2; m3dcache.setup.aspect.swch = 2; } else { m3dcache.setup.aspect = m3dcache.setup.aspect1; m3dcache.setup.aspect.swch = 1; }; m3d.core.onWindowResize(0); m3d.contr.renderAll(); }; 

Minha biblioteca tem um evento que trava na janela de redimensionamento. Aqui está um fragmento:

Código
 m3dcache.v.vw = window.innerWidth; m3dcache.v.vh = window.innerHeight; m3dcache.v.vclipw = 0; m3dcache.v.vcliph = 0; if (typeof m3dcache.setup.aspect !== "undefined") { if ((m3dcache.setup.aspect.w == 0) || (m3dcache.setup.aspect.h == 0)) {} else { var o = m3d.lib.inBlock(0, 0, m3dcache.setup.aspect.w, m3dcache.setup.aspect.h, 0, 0, m3dcache.v.vw, m3dcache.v.vh, 'center', 'center', 'resize'); if (typeof m3dcache.setup.aspect.p !== "undefined") { if (o.clipx > 0) { ow = ow * (m3dcache.setup.aspect.p / 100 + 1); if (ow > m3dcache.v.vw) { ow = m3dcache.v.vw; }; o = m3d.lib.inBlock(0, 0, ow, oh, 0, 0, m3dcache.v.vw, m3dcache.v.vh, 'center', 'center', 'resize'); }; }; m3dcache.v.vclipw = o.clipx; m3dcache.v.vcliph = o.clipy; var margx = o.clipx + 'px', margy = o.clipy + 'px'; document.getElementById('m3dcontainer').style.marginLeft = margx; document.getElementById('m3dcontainer').style.marginTop = margy; if (document.getElementById('renderer') !== null) { document.getElementById('renderer').style.marginLeft = margx; document.getElementById('renderer').style.marginTop = margy; }; m3dcache.v.vw = ow; m3dcache.v.vh = oh; }; }; 


O m3d.lib.inBlock também é uma função da minha biblioteca, que inscreve um retângulo em outro retângulo com parâmetros como centralização, redimensionamento ou corte e exibe as novas dimensões do retângulo inscrito, bem como os tamanhos dos campos formados nesse processo. Com base nessas informações, o contêiner div da janela é posicionado. 'renderer' é um elemento de contexto de bloco de uma cena 3D. Em seguida, a tela é dimensionada para lá de acordo com os parâmetros obtidos.

A interface do usuário é exibida no contêiner em um elemento de tela separado. Em geral, a árvore de documentos consiste em três blocos DIV transparentes com posicionamento absoluto (mais ou menos possível, dependendo das necessidades do jogo): na parte inferior está a tela da cena 3D, acima está a tela para IU e a parte superior é usada para animar elementos da interface e outros efeitos visuais . Ou seja, a interface do usuário não é renderizada em 3D, mas em seu knavass ou camada. A tarefa de combinar camadas em uma única imagem é deixada para o navegador. Para trabalhar com a interface do usuário, tenho um objeto especial na biblioteca. Resumidamente - a essência é a seguinte. As listas de sprites com elementos da interface do usuário no formato png com transparência são carregadas. A partir daí, os elementos necessários são retirados - planos de fundo, botões. E eles são desenhados na tela do meio usando a função js drawImage (img, ix, iy, iw, ih, x, y, w, h). Ou seja, os fragmentos necessários da imagem são exibidos nas posições necessárias na tela. Os botões são exibidos na parte superior dos fundos anexados a eles - todas as suas posições e tamanhos são definidos na configuração da interface do usuário. Ao redimensionar uma janela, as posições dos elementos na tela de destino (na qual são exibidas) são recalculadas, dependendo se esse ou aquele elemento está centralizado horizontal e verticalmente ou se está encaixado em algum canto ou face da tela. Isso cria uma interface do usuário adaptável que não depende da proporção da tela. Apenas é necessário definir a resolução mínima possível horizontal e verticalmente e não ficar abaixo dela para que os elementos não se sobreponham. Falarei sobre a interface do usuário outra vez, porque o artigo acabou sendo volumoso e ainda estou trabalhando na interface, porque ainda há muitas funções ausentes necessárias. Por exemplo, em monitores de alta resolução, a interface parecerá pequena. Você pode multiplicar o tamanho dos elementos por um determinado coeficiente, dependendo da resolução da tela. Por outro lado, talvez os enormes botões na tela não sejam necessários? Se a resolução da tela for grande, a tela em si é bastante grande.


E você pode dar ao programador uma escolha - escalar a IU dinamicamente com o tamanho da janela ou distribuir os elementos nos cantos. No caso do tamanho dinâmico, também existem questões próprias - por exemplo, o "sabão" de uma interface quando ela é exibida em uma escala muito grande. Se você criar sprites de elementos de interface em uma resolução deliberadamente grande, eles ocuparão muito espaço e também, provavelmente, não será útil para dispositivos pequenos - eles ainda não precisam de sprites grandes, mas consumirão memória.

Eu acho que é o suficiente por hoje. Ainda há algo em que pensar, como implementar isso ou aquilo. Enquanto isso, discordo por algum tempo da programação e faço promoção. Eu pretendo participar de um par de vitrines independentes e estou ativamente envolvido na promoção do jogo nas redes sociais, já que em novembro planejo ir para a plataforma de crowdfunding: precisarei de especialistas na área de gráficos 3D e animação esquelética para concluir o jogo.

Nos artigos a seguir, falarei sobre o controle de toque nos navegadores para dispositivos móveis - nem todos conectam um gamepad ou teclado ao tablet, sobre a otimização de gráficos 3D para dispositivos de baixa potência e muito mais.

Source: https://habr.com/ru/post/pt472272/


All Articles