Un nuevo juego con un ambiente antiguo en Three.js. Parte 2

En la primera parte, hablé sobre los problemas que encontré en el proceso de creación de un juego 3D para el navegador usando Three.js. Ahora me gustaría detenerme en detalles para resolver algunos problemas importantes al escribir un juego, como construir niveles, detectar colisiones y adaptar la imagen a cualquier proporción de la ventana del navegador.


Diagramas de nivel


En realidad, los niveles mismos se crean en el editor 3D, es decir, su geometría, mapeo de texturas, horneado de sombras, etc. Describí todo esto en la primera parte. ¿Por qué hay otros esquemas? El hecho es que Three.js no ofrece ningún tipo de motor físico, y uso esquemas de nivel para identificar obstáculos.


Three.js para resolver el problema de colisión solo ofrece trazado de rayos, la forma más sencilla de determinar la intersección de la geometría de los objetos. En principio, se puede usar, e incluso lo hice en uno de mis otros proyectos. Era una ciudad virtual justo en el sitio, en el navegador. Puedes moverte por la ciudad y no atravesar las paredes.


En el caso de que la intersección de la geometría del jugador y el edificio ocurra durante el movimiento, implementé la repulsión del jugador por una cierta distancia en la dirección opuesta a la pared. Pero para esto, los objetos deben ser paralelepípedos. Alrededor de algunos objetos complejos, creé colisionadores (llamaremos a los objetos invisibles que juegan el papel de obstáculos y evitan que el jugador pase a través de ellos), a través del cual se resolvieron las intersecciones. Y las partes inferiores de algunos edificios, que son simplemente "cajas", a veces se usaban como colisionadores.


En objetos geométricamente complejos, el trazado de rayos puede no funcionar o comportarse de manera inapropiada. Y, como solución, puede incrustar en el objeto no uno, sino varios pequeños colisionadores invisibles en forma de paralelepípedos con 100% de transparencia, dibujados uno al lado del otro y uno encima del otro, repitiendo aproximadamente la forma del objeto.

En el juego sobre mazmorras, el nivel es un solo objeto largo con movimientos de corte para mover al jugador. En realidad, para resolver el problema de colisión, uno podría pegar colisionadores invisibles donde sea necesario y usar trazado de rayos. Sin embargo, decidí ir por el otro lado.


  • En primer lugar, quería automatizar el proceso de creación de una serie de colisionadores.
  • En segundo lugar, puede usar solo información sobre los colisionadores, es decir, sus coordenadas en el espacio, y no cargar la escena 3D con algunos objetos vacíos adicionales.
  • En tercer lugar, dado que el juego usa solo una vista lateral y una de las coordenadas nunca cambia cuando se mueve, puede usar el cálculo de intersecciones en solo dos coordenadas.
  • Y cuarto, después de todo, de hecho, habrá un esquema de nivel. Además, crear nuevos niveles es conveniente comenzar con un esquema de este tipo. Simplemente puede arrastrar bloques alrededor de la pantalla en cualquier editor de gráficos, construir nuevos corredores y obstáculos, y luego ejecutar el script y obtener información sobre los colisionadores. Es decir, el problema del editor de niveles está parcialmente resuelto.

Escribí un script que toma parámetros de entrada como el nombre del archivo de esquema de nivel (png) y el color, cuyo relleno se interpreta como un obstáculo. El color predeterminado del espacio libre es el negro. Para el procesamiento por el script, el esquema de cada nivel debe guardarse en un archivo png separado. Por ejemplo, para el nivel más bajo, se ve así:


Estuve de acuerdo en que un bloque debe tener 80 píxeles de ancho y 48 píxeles de alto. Esto corresponde a 4 x 2.4 metros en el mundo 3D. Sería posible hacer 40 x 24 píxeles, es decir, diez veces, pero en la imagen parece pequeño.

El resultado del script en el primer nivel (la imagen se recorta a la derecha):


El script se ejecuta en el navegador. Creo que no tiene sentido el marcado html, es elemental: campos de entrada de datos y un botón de inicio. A continuación, la imagen leída se muestra en el lienzo. Y como resultado del script, se muestra una matriz debajo de la imagen en la escala mundial 3D, que contiene las coordenadas inferior izquierda y superior derecha de cada bloque, y con el desplazamiento especificado en el script para cada nivel. Este conjunto se puede copiar y pegar en la lista de colisionadores para usar en el juego (más sobre eso a continuación), se almacenará en algún tipo de constante. Las coordenadas también aparecen en la imagen en sí, pero en el marco de referencia de la imagen 2D. Estos números se muestran en el centro de cada bloque y le permiten verificar si todos los bloques están incluidos en el cálculo. Por sí mismos, estos números no son necesarios para nada excepto para la inspección visual. Algunos bloques, como las columnas entre las que pasa el jugador, no deben contarse. Acerca de qué objetos se excluyen del cálculo, a continuación.

Además, por ejemplo, en el segundo nivel hay delgadas placas horizontales sobre las que camina el jugador. Deben ser considerados. En consecuencia, debe asegurarse de que los números también aparezcan en ellos. En el diagrama, haga que tengan 2 píxeles de altura.



Ahora, sobre cómo el script toma en cuenta los bloques:

  • El esquema se procesa mediante bloques de 80x48, en cada uno de los cuales se toma un área desde el segundo al 79º píxeles horizontalmente y desde el segundo al 47º píxeles verticalmente. El primer y el último píxel no se utilizan para que alrededor de los bloques pueda crear un marco negro con un ancho de 1 píxel, esto mejora la percepción visual del circuito y facilita su creación.
  • Se ven todos los píxeles de la fila superior del bloque. Si hay colores entre ellos, entonces las coordenadas del bloque van del primer al último píxel coloreado horizontalmente y a la altura total del bloque verticalmente en la matriz final. Este será un bloque en blanco de ancho total o parcial.
  • Se ven todos los píxeles de la línea inferior del bloque. Si hay colores entre ellos, pero no hay ninguno en la fila superior, entonces las coordenadas del bloque van del primer al último píxel coloreado horizontalmente y 3 píxeles verticalmente desde la parte inferior a la matriz final. Esta será una plataforma sobre la cual caminar. Dentro de un bloque puede haber varias plataformas horizontales. Las plataformas se reconocen solo en la parte inferior del bloque. Las coordenadas de la plataforma están "hundidas" en un bloque, que se encuentra debajo, de modo que la superficie de la plataforma está al mismo nivel que los bloques vecinos, no las plataformas.
  • Las columnas y otras decoraciones dentro de un bloque vacío no se procesan, ya que solo se consideran las filas superior e inferior de píxeles. Por lo tanto, dentro del bloque, puede colocar decoración, explicaciones para el diagrama, punteros, columnas, etc., sin temor a que esto afecte de alguna manera el resultado del guión.

Luego, todas las coordenadas recibidas de la matriz se traducen a la escala del mundo 3D, multiplicadas por el coeficiente de su escala (que se seleccionó en el editor 3D cuando se creó). La matriz está lista para usar en el juego. El código del script se escribió a toda prisa, por lo que no pretende ser elegante, pero realiza su tarea.

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; } }; 


Ahora, sobre cómo funciona el juego con una variedad de bloques. El juego utiliza pasillos (niveles) que se cruzan. Cuando un jugador se convierte en un corredor, se conecta una nueva matriz de bloques: y para cada corredor, en consecuencia, se obtiene su propia matriz, obtenida de su esquema de nivel. Durante el movimiento del jugador, se verifica que sus coordenadas estén dentro de cada bloque. Y si él está dentro de cualquier bloque, entonces tenemos una colisión. Pero con cada movimiento del jugador no necesitamos buscar intersecciones con todos los bloques del nivel, porque puede haber muchos de ellos. Crea una matriz de solo los bloques más cercanos al jugador.

 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; }, 

Aquí, en la entrada x, y son las coordenadas actuales del jugador, dw, dh es la distancia a la que desea buscar bloques horizontal y verticalmente, por ejemplo, 12 y 8 metros. En otras palabras, toma todos los bloques alrededor del jugador en un cuadrado de 24x16 metros. Participarán en la búsqueda de enfrentamientos. ap.v.lv.d [i] es un elemento de una matriz de bloques del nivel actual, de hecho, él mismo también es una matriz de 4 números que definen los límites de un bloque - [x1, y1, x2, y2], por lo tanto, para verificar el cuadrado horizontalmente tomamos elementos con índices 0 y 2, y verticalmente - 1 y 3. Si hay una coincidencia, agregue este bloque a la lista para colisiones ap.v.coll.

Cuando el jugador se mueve, actualizaremos esta lista de colisiones, pero, para ahorrar rendimiento, no lo haremos en cada paso (o más bien, renderizando el marco), sino cuando el jugador deja un cierto cuadrado, un poco más pequeño, especificado en ap.v.collwStep y ap.v.collhStep, por ejemplo, 8 y 4 metros. Es decir, volveremos a montar la matriz de colisión nuevamente cuando el jugador pase un cierto camino horizontal o verticalmente desde su posición original. Al mismo tiempo, recordemos su posición en la que volvimos a ensamblar la matriz para usarla en la próxima iteración. pers [ax] - aquí por ax nos referimos al eje de coordenadas (ax), puede ser x o z, dependiendo de la dirección del corredor a lo largo del cual camina el jugador.

 //   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 qué tantas dificultades? ¿Por qué no utilizar todo el conjunto de colisiones a nivel y no tomar un baño de vapor? El hecho es que la detección de colisión se lleva a cabo de acuerdo con un algoritmo mucho más complejo, y no es rentable verificar la colisión con absolutamente todos los bloques de nivel, y no los más cercanos, en cada renderizado de cuadros. (Aunque esto no es exacto).

La definición de colisiones en cada representación de un cuadro utilizando la matriz de colisión preparada anteriormente:

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 }; }, 


Aquí x, y, xOld, yOld son las coordenadas nuevas y actuales del jugador. Los nuevos se calculan con solo tocar un botón, en función de una velocidad de movimiento dada, es decir, estas son coordenadas posibles. Se verifican para ver si caen dentro de cualquier bloque de la lista de colisiones. Si se caen, vuelven a los viejos y el jugador no pasa el obstáculo. Y si no caen, se vuelven actuales. pw2 y ph2 son la mitad del ancho y la altura del colisionador imaginario del jugador (ancho del jugador / 2, altura del jugador / 2). La salida se emite si hay una colisión horizontal y vertical (collw, collh), si hay un bloque de soporte debajo del jugador (supportf); esto deja en claro si comenzar más la animación de caída o si el jugador simplemente cambió a un bloque vecino, y así sucesivamente. Simplemente no pregunte por qué agregué 0.001 allí y resté 0.11. Esta es una muleta terrible que evita la caída a través de los bloques y el efecto de la fluctuación cuando choca con un obstáculo horizontal ... Esta función funciona, pero debe reescribirse de la manera normal. La optimización de esta función también falta todavía.

Creo que con las colisiones vale la pena terminar aquí.

Es difícil decir qué tan rápido es mi método o quizás más lento que el trazado de rayos, pero en el caso de este último, Three.js también almacena una variedad de objetos que participan en el sistema de colisión. Es solo que las colisiones están determinadas por el método de emisión del haz y su intersección con los planos de los lados de los objetos, y conmigo, determinando si las coordenadas de un objeto están dentro del otro a lo largo de cada uno de los dos ejes.

El juego también tiene objetos en movimiento (tiburón) y objetos marcadores que desencadenan algún tipo de animación (por ejemplo, el contacto con el agua desencadena el movimiento de un tiburón). Todos estos objetos también participan en colisiones, y algunos con coordenadas que varían en el tiempo. Allí, por extraño que parezca, todo es más simple: durante el movimiento del objeto, sus coordenadas se comparan con las coordenadas del jugador.


Gamepad


En general, mantener un gamepad javascript en un navegador no es una tarea trivial. No hay eventos de presionar y soltar botones. Solo hay eventos que conectan y desconectan el dispositivo y el estado que puede obtenerse mediante un sondeo periódico y luego compararlo con el anterior.

Un video que muestra el funcionamiento del gamepad en un navegador en una tableta en Windows 8.1 y una PC en Windows 10. Sin embargo, la tableta es antigua, lanzada en 2014, por lo que la iluminación dinámica está apagada en el juego.


Para sondear el gamepad, se usa una función llamada una vez cada 100 milisegundos. Se configura con la función de mi 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(); }; }; } }); 

Aquí globalTimer es el sistema de administración de eventos de temporizador javascript setInterval que escribí. Allí, simplemente se agrega una serie de eventos a una determinada matriz que debe llamarse a diferentes intervalos. Luego, se establece un temporizador setInterval con la frecuencia correspondiente al evento con la frecuencia más alta de todas. El temporizador sondea la función m3d.lib.globalTimer.update (), que recorre la lista de todos los eventos y ejecuta las funciones de aquellos que han llegado a ejecutarse. Al agregar o eliminar eventos, la frecuencia del intervalo también puede cambiar (por ejemplo, si elimina el evento más rápido).

El juego también establece controladores para cada tecla del gamepad: 'a' es para el eje (ax), 'b' para el botón (botón), y 11 es la desviación izquierda a lo largo del eje horizontal de la cruz (como si fuera el botón 1), 12 - la desviación derecha a lo largo del eje horizontal de la cruz (como si fuera su botón 2), 21 y 22 - para el eje vertical. Por ejemplo:

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

significa que la siguiente función se establecerá al mismo tiempo para la desviación a lo largo del eje horizontal hacia la izquierda y para el botón 3 (izquierda). Bueno, entonces se establece una función que se ejecutará cuando se presione el botón, y luego cuando se suelte.

  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(); } ); 

Aquí apcontrolsRenderStart () es una función que inicia un render si aún no se está ejecutando. En general, el soporte para el gamepad está estrechamente vinculado a mi biblioteca m3d, por lo que si continúo describiendo todas sus características, se extenderá durante mucho tiempo ...

Solo te daré una parte: gamepad, en el que implementé la inicialización del gamepad, la instalación de controladores y el sondeo de estado de la manera más simple.

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 


En general, el soporte del gamepad en el juego todavía está incompleto: solo se implementa el soporte para el gamepad más simple, pero no el que, por ejemplo, se usa en XBox, porque no lo tengo. Si lo consigo, lo programaré y trabajaré con él. Allí será posible ajustar la velocidad del personaje, es decir, será posible moverse a cualquier velocidad en el rango de un paso a otro. Esto se logra tomando parámetros fraccionarios de los ejes. Mi gamepad solo devuelve los enteros -1 y 1. Además, mi gamepad tiene una cruz repugnante, y cuando se presiona hacia la izquierda o hacia la derecha, se presiona simultáneamente hacia arriba o hacia abajo. Por lo tanto, no utilicé la parte superior e inferior de la cruz y la dupliqué con los botones a la derecha del gamepad ... Al lanzar el juego, planeo crear varios perfiles de gamepads. Además, en el caso de conectar varios gamepads, solo se utilizará este último hasta ahora.

Pantalla sensible


El juego está diseñado para una relación de aspecto de 16: 9. Pero agregué un ajuste horizontal automático de ± 10% para que en la ventana expandida del navegador no hubiera tales barras negras a los lados:


Y sería así:


En modo de pantalla completa, habrá 16: 9 reales. Sería posible adaptar la imagen en general a cualquier relación de aspecto de la ventana del navegador, pero no lo hice, ya que una ventana ancha y baja daría lugar a un ángulo de visión demasiado grande, lo que no es bueno desde el punto de vista del juego: callejones sin salida distantes, objetos, enemigos serán inmediatamente visibles y todo lo demás que el jugador no necesita ver todavía. Por lo tanto, me limité a ajustar dentro de ± 10% de 16: 9. Sin embargo, para monitores estrechos (4: 3), sin embargo, me di cuenta de la capacidad de cambiar de 16: 9 al modo de adaptación de 4: 3 a 16: 9 presionando la tecla Y. Pero no más amplio, así que, de nuevo, para no romper el juego. Es decir, puede jugar en la clásica relación de 16: 9, o puede ampliar la imagen a la altura de la ventana recortándola horizontalmente. Aunque, esto tampoco es muy bueno, por ejemplo, en situaciones de arcade, cuando algo vuela hacia el jugador desde un lado. Queda poco tiempo para la reacción. Pero siempre puede volver rápidamente al modo clásico.


La adaptación de la pantalla, así como todas las teclas de acceso rápido utilizadas en el juego se muestran en el siguiente video:


En realidad, la relación de aspecto se establece en la configuración del juego.

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

Y en el juego cuando presionas Y, cambia:

 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(); }; 

Mi biblioteca tiene un evento que se cuelga en la ventana de cambio de tamaño. Aquí hay un fragmento de ella:

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; }; }; 


m3d.lib.inBlock también es una función de mi biblioteca, que inscribe un rectángulo en otro rectángulo con parámetros tales como centrar, escalar o recortar, y muestra las nuevas dimensiones del rectángulo inscrito, así como los tamaños de los campos que se forman en este proceso. Según esta información, se coloca el contenedor div de la ventana. 'renderizador' es un elemento de contexto de bloque de una escena 3D. Luego, el lienzo se escala allí de acuerdo con los parámetros obtenidos.

La IU se muestra en el contenedor en un elemento de lienzo separado. En general, el árbol de documentos consta de tres bloques DIV transparentes con posicionamiento absoluto (más o menos posible, según las necesidades del juego): en la parte inferior está el lienzo de la escena 3D, arriba está el lienzo para IU y la parte superior se usa para animar elementos de interfaz y otros efectos visuales . Es decir, la interfaz de usuario no se representa en 3D, sino en su knavass o capa. La tarea de combinar capas en una sola imagen se deja al navegador. Para trabajar con la interfaz de usuario, tengo un objeto especial en la biblioteca. Brevemente, la esencia es la siguiente. Se cargan las listas de sprites con elementos de IU en formato png con transparencia. A partir de ahí, se toman los elementos necesarios: fondos, botones. Y se dibujan en el lienzo del medio utilizando la función js drawImage (img, ix, iy, iw, ih, x, y, w, h). Es decir, los fragmentos necesarios de la imagen se muestran en las posiciones necesarias en la pantalla. Los botones se muestran en la parte superior de los fondos adjuntos: todas sus posiciones y tamaños se configuran en la configuración de la interfaz de usuario. Al cambiar el tamaño de una ventana, las posiciones de los elementos en el lienzo de destino (en el que se muestran) se recalculan, dependiendo de si este o ese elemento está centrado horizontal y verticalmente o se ajusta a alguna esquina o cara de la pantalla. Esto crea una interfaz de usuario adaptativa que no depende de la relación de aspecto de la pantalla. Solo es necesario establecer la resolución mínima posible horizontal y verticalmente y no caer debajo de ella para que los elementos no se superpongan entre sí. Hablaré sobre la interfaz de usuario otra vez, porque el artículo resultó ser voluminoso, y todavía estoy trabajando en la interfaz de usuario, porque todavía faltan muchas funciones que necesito. Por ejemplo, en monitores de alta resolución, la interfaz se verá pequeña. Puede multiplicar el tamaño de los elementos por un cierto coeficiente, dependiendo de la resolución de la pantalla. Por otro lado, ¿quizás los enormes botones en la pantalla no son necesarios? Si la resolución de la pantalla es enorme, entonces la pantalla en sí es bastante grande.


Y puede darle una opción al programador, ya sea escalar la UI dinámicamente con el tamaño de la ventana o distribuir los elementos en las esquinas. En el caso del tamaño dinámico, también hay preguntas propias, por ejemplo, el "jabón" de una interfaz cuando se muestra en una escala demasiado grande. Si crea sprites de elementos de interfaz en una resolución deliberadamente enorme, ocuparán mucho espacio y, probablemente, no serán útiles para dispositivos pequeños; todavía no necesitan sprites grandes, pero consumirán memoria.

Creo que es suficiente por hoy. Todavía hay algo en qué pensar, cómo implementar esto o aquello. Mientras tanto, me alejo un poco de la programación y hago promociones. Planeo participar en un par de exhibiciones independientes y participo activamente en la promoción del juego en las redes sociales, ya que en noviembre planeo ir a la plataforma de crowdfunding: necesitaré especialistas en el campo de los gráficos 3D y la animación esquelética para completar el juego.

En los siguientes artículos, hablaré sobre el control táctil en los navegadores para dispositivos móviles: no todos conectan un gamepad o un teclado a la tableta, sobre la optimización de gráficos 3D para dispositivos de baja potencia y mucho más.

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


All Articles