Juego en 3D en three.js, nw.js

Decidí lanzar una nueva versión de mi antiguo juego de navegador, que durante un par de años ha tenido éxito como aplicación en las redes sociales. Esta vez me propuse diseñarlo también como una aplicación para Windows (7-8-10) y colocarlo en varias tiendas. Por supuesto, en el futuro puede hacer ensamblajes para MacOS y Linux.


El código del juego está escrito completamente en javascript puro. Para mostrar gráficos en 3D, la biblioteca three.js se usa como un enlace entre el script y WebGL. Sin embargo, este fue el caso en la versión anterior del navegador. Lo más importante en este proyecto para mí fue la razón, en paralelo con el juego, para agregar mi propia biblioteca, diseñada para complementar three.js con las herramientas para un trabajo conveniente con objetos de escena, su animación y muchas otras características. Luego lo abandoné por mucho tiempo. Es hora de volver a ella.


Mi biblioteca contiene herramientas convenientes para agregar y eliminar objetos de la escena, cambiar las propiedades de partes individuales de objetos (mallas), animación independiente de la velocidad de fotogramas de objetos 3D, un sombreador de cielo con una textura de cielo estrellado por la noche y mucho más. Te contaré sobre algunos de ellos. Con respecto al cielo, implementé su creación con una única función que toma una serie de parámetros de entrada, inicializa el sombreador, carga la textura de la nube (si es necesario) y comienza a actualizar el cielo con una iteración dada.

Sin embargo, todo es un poco más complicado allí: para periódicamente, pero raramente llamadas funciones, otra construcción realmente funciona, usando setInterval (), en el que los eventos se pueden lanzar a diferentes intervalos, y reducirá todo esto a un denominador común y lo resolverá a la derecha tiempo necesario eventos en la lista. Allí también puedes lanzar el intervalo de actualización del cielo. Pero el movimiento de los objetos de juego en 3D para una mayor suavidad ya se ha implementado a través de requestAnimationFrame () ...

Entonces, como estamos hablando del cielo, comenzaremos con él.

El cielo



Agregar el firmamento a la escena es la siguiente.

Primero debe agregar la luz estándar three.js a la escena con sus valores máximos (iniciales) de brillo. Toda la escena con sus objetos, luz y otros atributos, para no saturar el espacio global, se almacenará en el espacio de nombres de apscene.

//  (AmbLight)   apscene.AmbLight=new THREE.AmbientLight(0xFFFFFF); apscene.scene.add(apscene.AmbLight); //  (AmbLightBk)   (      ) apscene.AmbLightBk=new THREE.AmbientLight(0xFFFFFF); apscene.sceneb.add(apscene.AmbLightBk); // (, )   var SFog=new THREE.FogExp2(0xaaaaaa, 0.0007); apscene.scene.fog=SFog; // (, )   var SFogBk=new THREE.FogExp2(0xa2a2aC, 0.0006); apscene.sceneb.fog=SFogBk; //  apscene.hemiLight=new THREE.HemisphereLight(0xFFFFFF, 0x999999, 1); apscene.scene.add(apscene.hemiLight); //        .       .        three.js   ,   ,     ,  ,   ... apscene.dirLight=new THREE.DirectionalLight(...) apscene.dirLightBk=new THREE.DirectionalLight(...) 

Después de eso, ya puedes ejecutar la animación del cielo con sombreadores, texturas (blackjack y ... bueno, está bien) a través de una de mis funciones:

 m3d.graph.skydom.initWorld( //  ,  ,         {saveStart:false}, { //ltamb (light ambient,    ) -       ltamb:{a1:-2, a2:8, k1:0.2, k2:0.75, obj:[ {obj:apscene.AmbLight.color, key:['r','g','b']} ]}, //a1  a2 -      (altitude,      ),     ,   obj,     (k1..k2),   k1  k2 -         // ,    ltamb :     -2  8 (    ),    (apscene.AmbLight.color),    r, g  b,    0.2  0.75     ( ,    ,    0xFFFFFF). //key -    ,        three.js //        (apscene.AmbLightBk.color).    (  altitude)  -4  12          0.3 ... 0.99    .  -4 , ,    0.3  ,   12   0.99   ltambb:{a1:-4, a2:12, k1:0.3, k2:0.99, obj:[ {obj:apscene.AmbLightBk.color, key:['r','g','b']} ]}, //      : ltdir:{a1:-2, a2:8, k1:0.0, k2:1, obj:[ {obj:apscene.dirLight, key:['intensity']}, {obj:apscene.dirLightBk, key:['intensity']} ]}, //       (apscene.dirLightBk)   ,    ,       . // ,      three.js    - inensity ( , apscene.dirLight.intensity).  ,  ,       three.js    . //  .   ,     ( 0.15  )     12  8 (, ,    8  12), : lthem:{a1:8, a2:12, k1:0.15, k2:0.3, obj:[ {obj:apscene.hemiLight, key:['intensity']} ]}, //         .       . //    ()   : ltambfog:{a1:-2, a2:8, k1:0.4, k2:1, obj:[ {obj:apscene.scene.fog.color, key:['r','g','b']} ]}, //  : ltambbfog:{a1:-2, a2:12, k1:0.25, k2:1, obj:[ {obj:apscene.sceneb.fog.color, key:['r','g','b']} ]}, //     .  ,         ,  . ltambfogd:{a1:8, a2:12, k1:0.2, k2:0.35, obj:[ {obj:apscene.scene.fog, key:['density']} ]}, //  ltambbfogd:{a1:6, a2:12, k1:0.2, k2:0.28, obj:[ {obj:apscene.sceneb.fog, key:['density']} ]}, //  ,      ( ,   ),   ,     ,   , -«»     ,      skyplane1..6: planeAmb:{a1:-5, a2:12, k1:0.5, k2:1.0, obj:[ {obj:apscene.user.skyplane1.material.color, key:['r','g','b']}, {obj:apscene.user.skyplane2.material.color, key:['r','g','b']}, {obj:apscene.user.skyplane3.material.color, key:['r','g','b']}, {obj:apscene.user.skyplane4.material.color, key:['r','g','b']}, {obj:apscene.user.skyplane5.material.color, key:['r','g','b']}, {obj:apscene.user.skyplane6.material.color, key:['r','g','b']} ]} //,     ,     ,  . }; 

Como resultado, aparece un cielo dinámico en la escena con un cambio suave en la altura del sol y, en consecuencia, un cambio en la hora del día.

No es necesario utilizar todo tipo de iluminación en el escenario. Y no es necesario cambiar todos los parámetros según la hora del día. Pero, jugando con su brillo, sin embargo, puede crear una imagen bastante realista del cambio de día y de noche. Puede nombrar los parámetros como desee, lo principal es observar las claves de los objetos dentro de ellos tal como se especifican en three.js.

Cómo se ve, puede ver el video de la escena de demostración:


Este es un juego diferente. Es solo que el horizonte no está abarrotado de varios objetos y, por lo tanto, el trabajo de este script es más claramente visible. Pero en el juego sobre el que se discute la historia, se usa exactamente el mismo enfoque. La alta velocidad del paso del tiempo aquí se establece solo con fines de demostración, por lo que el tiempo fluye, por supuesto, más lentamente, con el mismo paso de la iteración actualizando el firmamento. En esta demostración, por cierto, está involucrado un sombreador de agua, también con parámetros variables, dependiendo de la altura del sol ... Pero aún no lo he finalizado.

Rendimiento


Todo esto es muy poco exigente con el hierro. Trabajando en el navegador Chrome, carga el Xeon E5440 bajo LGA775 (y con 4 gigas de RAM) en un 20%, y el núcleo de la tarjeta gráfica GT730 en un 45%. Pero esto se debe únicamente a la animación del agua. Si hablamos de un juego donde no hay agua, pero hay una ciudad, este:


luego, en el momento en que el automóvil se mueve por la ciudad: porcentaje 45%, tarjeta de video 50%. En principio, con una reducción de fps (hasta aproximadamente 30 cuadros por segundo), funciona razonablemente bien incluso en Pentium4 3GHz (1Gb RAM) y en una tableta en Intel Atom 1.3GHz (2Gb RAM).

Todo este hardware es extremadamente débil y otros juegos similares en WebGL y HTML5, incluso algunos en 2D, me ralentizan sin piedad, hasta el punto de que es imposible jugarlos. Como dicen, escribe el juego tú mismo, según lo necesites, y juega.

Escena



La escena 3D en three.js es un objeto de escena y su matriz secundaria es, de hecho, todos los modelos 3D cargados en la escena. Para no registrar una llamada al gestor de arranque para cada modelo, decidí que toda la escena del juego se establecería en forma de una determinada configuración, con una gran matriz asociativa locd: {} (como datos de ubicación), que contendría todos los ajustes: luces, caminos de texturas precargadas e imágenes para la interfaz, rutas a todos los modelos que deben cargarse en el escenario, y más. En general, esta es la configuración completa de la escena. Se configura una vez en el archivo js del juego y se alimenta a mi cargador de escenas.

Y este objeto locd: {}, en particular, contiene las rutas a los modelos 3D individuales que deben cargarse. La ruta cero es la ruta común, y luego las rutas relativas para cada objeto, como:
 ['path/myObj', scale, y, x,z, r*Math.PI, 1, '', '', '', 1, ['','','',''], 'scene'] 

Se entiende que todos los modelos se exportan desde el editor 3D al formato json, es decir, tienen rutas como path / myObj.json. Esto es seguido por la escala (ya que el editor se puede guardar con una escala que no es adecuada para el juego), la posición del objeto en altura (y), a lo largo de los ejes (x) y (z), luego el ángulo de rotación ® del modelo en (y), una serie de parámetros opcionales y El nombre de la escena donde cargar el modelo: en la escena principal (escena) o en el fondo (escenab).

Sí, era necesario implementar esto no en forma simple, sino en forma de matriz asociativa. Por lo tanto, el orden de los parámetros es incomprensible incluso sin documentación, o al menos sin el tipo de función que toma estos parámetros, no lo entenderá. Creo que en el futuro rehaceré estas líneas en matrices asociativas. Mientras tanto, se ve así:
 landobj: [ ['gamePath/'], [ ['landscape/ground', 9.455, 0, 0,0, 0*Math.PI, 1, '', '', '', 1, ['','','',''], 'scene'], ['landscape/plants', 9.455, 0, 0,0, 0*Math.PI,1, '', '', '', 1, ['','','',''], 'scene'], ['landscape/buildings/house01', 2, 0, -420,420, -0.75*Math.PI, 1, '', '', '', 1, ['','','',''], 'scene'], ... ] ], 

Estos modelos se cargan en el escenario y se colocan en las coordenadas dadas aquí en el espacio. En principio, todos los modelos pueden cargarse como un solo objeto, es decir, exportarse desde el editor como una escena de juego completa y cargarse en coordenadas (0; 0; 0). Entonces solo habrá una línea: paisaje / tierra - tengo ground.json - esta es la parte principal del mundo del juego. Pero en este caso será difícil manipular objetos individuales de la escena, ya que primero deberá buscar en la consola del navegador y recordar cuál de los hijos de este enorme terreno es. Y luego contáctelos por números. Por lo tanto, los modelos de juegos itinerantes se cargan mejor con objetos separados. Luego se puede acceder a ellos por nombre desde la matriz asociativa, que se creará automáticamente especialmente para este propósito.

La configuración completa del juego puede verse, por ejemplo, así:
 locd:{ // name: 'SeaBattle', type: 'game', menulabel: '', //    x:-420, y:70, z:-420, rot: -0.5, //    intsdistance: 200, // ambLtColor: 0xFFFFFF, ambLtColorBk: 0xFFFFFF, lightD: [0xDDDDDD,0.3,1000, 200000,0.3,-0.0003, -190,200000,-140,0,0,0, 200,-200,200,-200], lightDBk: [0xFFFFFF,0.3,10000, 40000,0.3,-0.0035, -190,1200,-140,0,0,0, 50000,-50000,50000,-50000], lightH: [0xFFFFFF,0x999999,1, 0,500,0], //      lightsP: [], //     lightsPDynamicAr: [ [0xffffff, 4, [0, -2000, 0], [50, 1.5] ] //[[distance], [decay]] ], //         (x,y,z) userPointLights: [ [0, -2000, 0] ], //   lightsS: [ [0xffffbb, 1.0, [0, 250, 180], [0, 0, 180], 0.5, 600,600,600, -0.0005] ], //  shadowMapCullFace:0, shadowsMode: 'all', //all,list,flag //  imagePaths: [ 'game/img/', 'interface.png', 'interface2.png' ], // 3D  landobj: [ ['game/models/'], [ ['landscape/land',1, 0, 0,0, 0.0*Math.PI,1,'','','',1,['','','',''],'scene'], ['landscape/sbp',1, 0, 0,180, 1.0*Math.PI,1,'','','',1,['','','',''],'scene'], ['landscape/sbu',1, 0, 0,1120, 1.0*Math.PI,1,'','','',1,['','','',''],'scene'] ] ], //        staffobj: [ ['game/models2/'], [ ] ], //  progobj: [ [ ] ] }, 

Sí, es mejor rehacer todas estas submatrices en matrices asociativas, de lo contrario, el orden de los parámetros en ellas no está claro ...

Modelos 3D



Otra cosa interesante Cargando modelos. Mi biblioteca acepta modelos 3D con texturas y establece automáticamente algunos parámetros para sus elementos individuales (mallas), según los nombres. El hecho es que si, por ejemplo, el modelo está configurado para proyectar una sombra, entonces será proyectado por cada malla incluida en su composición. No siempre es necesario que todo el modelo arroje una sombra por completo o adquiera otras propiedades que afecten fuertemente el rendimiento. Por lo tanto, si activa un determinado indicador que indica que es necesario considerar cada malla por separado, al cargarla es posible determinar qué malla tendrá esta o aquella propiedad y cuáles no. Bueno, por ejemplo, no hay absolutamente ninguna necesidad de que la sombra sea proyectada por el techo horizontal plano de la casa o por muchos detalles menores irrelevantes del modelo sobre un fondo grande. De todos modos, el reproductor no podrá ver estas sombras, y la potencia del procesador de video se usará para procesarlas.

Para hacer esto, en el editor de gráficos (Blender, Max, etc.), puede especificar inmediatamente los nombres de las mallas (en el campo de nombre del objeto) de acuerdo con una determinada regla. Debe haber un guión bajo (_). Los caracteres de control condicional deben ir en el lado izquierdo, por ejemplo: d - doble cara (la malla es bidireccional, de lo contrario - unidireccional), c (sombra proyectada) - proyecta una sombra, r (sombra recibida) - toma sombras. Es decir, por ejemplo, el nombre de la malla de la tubería en la casa puede ser cr_tube. También se usan muchas otras letras. Por ejemplo, "l" es un colisionador, es decir, la pared de la casa, que tiene el nombre crl_wall01, no permitirá que el jugador pase a través de sí mismo, y también lanzará una sombra. No hay necesidad de hacer colisionadores, como un techo o una manija de la puerta, y así degradar el rendimiento. Como ya entendió, al cargar un modelo, mi biblioteca analiza los nombres de las mallas y les da las propiedades correspondientes en la escena. Pero para esto es necesario nombrar correctamente todas las mallas antes de exportar el modelo desde el editor 3D. Esto ahorrará significativamente el rendimiento.

Todos los indicadores de control para mallas dentro de un objeto:
col_ ... es el colisionador. Dicha malla se mostrará simplemente como un colisionador transparente e invisible. En el editor, puede verse como cualquier cosa, solo su forma es importante. Por ejemplo, puede ser un paralelepípedo alrededor de todo el modelo si es necesario que el jugador no pueda pasar por este modelo (edificio, piedra grande, etc.).

l_ ... es un objeto colisionable. Dando a cualquier malla una propiedad de colisionador.

i_ ... - intersecciones. La malla se agregará a la lista de intersecciones, que se puede usar, por ejemplo, para hacer clic en ella, es decir, para dar interactividad al juego.

j_ ... también intersecciones. Igual que el anterior, solo una versión más nueva, con un algoritmo mejorado para encontrar intersecciones en el juego y menos consumo de recursos.

e_ ... - intersecciones para las puertas de las casas (entrada / salida). Excluye la intersección sobre otras mallas de objetos. Se utiliza si es necesario en las casas en algún momento para hacer que solo las puertas sean interactivas, excluyendo todos los demás elementos interactivos. Con imaginación, puedes llegar a esto y a muchos otros usos.

c_ ... - proyecta sombras. La malla proyecta una sombra.

r_ ... - recibe sombras. Una malla acepta sombras de todas las otras mallas que las proyectan.

d_ ... - bilateral (doble cara). Visible en ambos lados, la textura se superpone en ambos lados.

t_ ... - transparente (transparente), si todo el objeto se establece en alphatest en three.js.

u_ ... - transparente (transparente), con una densidad fija (0.4), si todo el objeto no especifica alphatest en three.js.

g_ ... - vidrio. Se establece la transparencia fija (0.2).

h_ ... - invisible (oculto). Para partes del objeto (mallas) que deben ocultarse al agregar el objeto a la escena. Entró en la lista de ocultos.

v_ ... visible. Todos los objetos, excepto los marcados con "h", ya son visibles, pero con la bandera "v" se ingresan en una lista separada visible para su posterior ocultación u otras manipulaciones.

Como resultado, el nombre de la malla puede ser algo como esto: crltj_box1 (proyecta, acepta una sombra, colisionador, transparente, interactivo). Y otra malla como parte del mismo modelo: cr_box2 (solo proyecta y toma sombras). Naturalmente, los caracteres de control se pueden configurar en cualquier orden. Por lo tanto, desde el editor puedes controlar la visualización futura de partes del objeto en el juego, o más bien, algunas de sus propiedades, ahorrando, al mismo tiempo, potencia informática.

La esencia del juego


El significado, de hecho, del juego sobre el que trata la historia, es moverse por el perímetro del campo cuadrado y comprar empresas. El campo está hecho en forma de calles 3D. El modelo económico del juego es significativamente diferente de los de su tipo. En mi juego, cuando alguien inicia un negocio, su beneficio esperado cae. Y viceversa, cuando descubres algo, aumenta. Todos los cálculos de pérdidas y ganancias se realizan en la Inspección fiscal en el campo Inicio. También puede obtener un préstamo de un banco, negociar valores y hacer otras cosas. He mejorado el comportamiento de la IA en comparación con la versión anterior. Redidió casi todos los modelos y texturas 3D del juego y optimizó el rendimiento. Realizó más configuraciones y mucho más.

Animación



Para animar el movimiento de los objetos del juego, se utiliza el motor más simple, que con una velocidad de fotogramas determinada (se limita a la 60) cambia los parámetros numéricos para un intervalo de tiempo determinado, pasando valores intermedios al controlador. Y en el controlador, por ejemplo, se muestra el modelo.

Por ejemplo Necesitamos mover el objeto obj en el espacio desde la posición (10; 10; 50) al punto (100; 300; 60). Establecemos 3 parámetros indicando sus valores iniciales y finales. Nuestra coordenada x variará de 10 a 100, y de 10 a 300 y z de 50 a 60. Y todo esto debería suceder, por ejemplo, en 4 segundos.
 m3d.lib.anim.add( 'moveobj', 1, 4000, 'and', {userpar1:111, obj:my3DObject}, [ {lim1:10, lim2:100, sstart:10, sfin:100, t:0}, {lim1:10, lim2:300, sstart:10, sfin:300, t:0}, {lim1:50, lim2:60, sstart:50, sfin:60, t:0} ], function(){ myPeriodicFun(this); }, function(){ myFinishFun(this); } ); m3d.lib.anim.play(); 

La primera línea de 5 parámetros: moveobj: nombre de la animación (cualquiera), 1: número de secuencia (puede animar objetos en paralelo en un número ilimitado de secuencias), 4000: tiempo de animación de 4 segundos y un parámetro no utilizado que será responsable de la lógica de transición en el futuro entre animaciones dentro de la misma secuencia, userpar es cualquier matriz asociativa que se pasará al controlador como un parámetro, por ejemplo, con un radio calculado, senos, cosenos y, en general, cualquier valor calculado para esta animación, para no ser calculado en sobre el tiempo de cada iteración. O con referencia a un objeto 3D, que, de hecho, se animará.

Lo siguiente es una matriz con parámetros mutables. Encontramos que el primer parámetro es el cambio en la coordenada x, el segundo es y, el tercero es z. Escribimos para cada uno en lim1 y lim2, de qué y a qué tamaño cambiará. En sstart y sfin especificamos los mismos valores. Aquí puede especificar un inicio, por ejemplo, a partir de algún otro valor, luego el parámetro se "desplazará" en un círculo hacia él, omitiendo lim2 y comenzando una nueva "revolución" con lim1. Bueno, por ejemplo, esto es necesario si nuestra animación se enlaza entre algunos valores (lim1 y lim2), pero necesitamos comenzar no desde el principio (es decir, no desde lim1), sino desde algún valor intermedio.

t: 0 simplemente establece que la animación para este parámetro se realice 1 vez, de acuerdo con el tiempo total (4000), como si se extendiera a él. Si establecemos otro número, menor que el tiempo principal, este parámetro se repetirá y se repetirá hasta el momento de la animación principal (4000). Esto es conveniente, por ejemplo, para establecer la rotación del objeto cuando el ángulo debe cruzar repetidamente la línea de 360 ​​grados y restablecer a 0.

Luego vienen 2 devoluciones de llamada: la que se ejecutará con cada iteración y la que se ejecutará una vez que se complete la animación completa (punto de salida).

La primera devolución de llamada myPeriodicFun (this), por ejemplo, puede ser así:
 myPeriodicFun:function(self) { var state=self.par[0].state, state_old=self.par[0].state_old; var state2=self.par[1].state, state_old2=self.par[1].state_old; var state3=self.par[2].state, state_old3=self.par[2].state_old; if ((state!=state_old)||(state2!=state_old2)||(state3!=state_old3)) { var obj=self.userpar.obj; obj.position.x=state; obj.position.y=state2; obj.position.z=state3; ap.cameraFollowObj(obj); }; }, 

Es decir, en cada iteración del movimiento, se arroja un parámetro (self) en esta función que contiene los valores intermedios calculados para todos los parámetros de animación dados: self.par [0] .state, self.par [1] .state, self.par [2] Estado. Esta es nuestra x, y y z en el momento actual. Por ejemplo, si la animación dura 4 segundos, después de 2 segundos x será igual a (100-10) / 2 = 45. Y así para todas las coordenadas. En consecuencia, en myPeriodicFun simplemente mostramos nuestro objeto en estas coordenadas. , : , -.

f ((state!=state_old)..., , ( state_old ), , , , , - , , . state state_old , , , , . 1 , , .

, -. 4- , . , - . , . (m3d.lib.anim.add()) , . , . , -.

PS . .., javascript .

«» , 2- - . , , .



, fps . . . , .

. three.js , (shadowCameraLeft, shadowCameraRight, shadowCameraTop, shadowCameraBottom). , , . , — , . , . , .

3D contr, . contr.cameraForShadowStep, , .

, , 700x700x700, contr.cameraForShadowStep , , 20. 20 , . 3D , , 3D . , 700x700x700 20, 7000x7000x7000 200. .

, , . , , . .



fps, . «». , , ( ) . , . , 3D , (2, 4 8) « ».


, . , userPointLights lightsPDynamicAr. userPointLights . lightsPDynamicAr 8 . , .

2-8 userPointLights. . , 2-8 , . , fps, . 60 , , — .

(Xeon E5440, GeForce GT730):



( ), bat-, Google Closure Compiler *.js . nwjc.exe nw.js — js (*.bin). :
java -jar D:\webservers\Closure\compiler.jar --js D:\webservers\proj\m3d\www\game\bus\bus.js --js_output_file D:\webservers\proj\nwProjects\bus\game\bus\bus.js

cd D:\«Program Files»\Web2Exe\down\nwjs-sdk-v0.35.5-win-ia32

D:\«Program Files»\Web2Exe\down\nwjs-sdk-v0.35.5-win-ia32\nwjc.exe D:\webservers\proj\nwProjects\bus\game\bus\bus.js D:\webservers\proj\nwProjects\bus\game\bus\bus.bin

del D:\webservers\proj\nwProjects\bus\game\bus\bus.js

Web2Executable exe Windows. nw.js 0.35.5, , . - , .


nw.js . . 35 , , . — node-webkit. locales , , - . . , ( ). , . .

167 .


- , , Businessman3DSetup.exe 70,2 .




. . itch. , , — $3. , . GOG , , . , , . Epic Store, , .

— , 1 5. — . . — -, Windows, Linux, iOs MacOs, , WebGL. , , webkit — , Firefox, Edge IE11 Windows, , — .

Conclusiones



, . , , , -, , «» . , . , . , , - . , , .

, , - . , javascript. , . , , html5, 2D. , ( ) .

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


All Articles