Haz un Neon Vector Shooter en jMonkeyEngine enemigos y sonidos

En la primera parte de esta serie sobre la construcción de un juego inspirado en Geometry Wars en jMonkeyEngine, implementamos el barco del jugador y lo dejamos mover y disparar. Esta vez, vamos a añadir los enemigos y efectos de sonido..


Visión general

Esto es para lo que estamos trabajando en toda la serie:


... y esto es lo que tendremos al final de esta parte:


Necesitaremos algunas clases nuevas para implementar las nuevas características:

  • SeekerControl: Esta es una clase de comportamiento para el enemigo buscador..
  • WandererControl: Esta es también una clase de comportamiento, esta vez para el enemigo errante..
  • Sonar: Gestionaremos la carga y reproducción de efectos de sonido y música con esto..

Como habrás adivinado, agregaremos dos tipos de enemigos. El primero se llama buscador; Perseguirá activamente al jugador hasta que muera. El otro, el vagabundo, solo deambula por la pantalla en un patrón aleatorio.


Añadiendo enemigos

Engendraremos a los enemigos en posiciones aleatorias en la pantalla. Para darle al jugador algo de tiempo para reaccionar, el enemigo no estará activo de inmediato, sino que se desvanecerá lentamente. Después de que se haya desvanecido por completo, comenzará a moverse por el mundo. Cuando choca con el jugador, el jugador muere; Cuando choca con una bala, se muere..

Enemigos engendros

En primer lugar, necesitamos crear algunas nuevas variables en el MonkeyBlasterMain clase:

 privado larga enemigosPapacolturarse; flotador privado enemySpawnChance = 80; Nodo privado enemigoNodo;

Podremos usar los dos primeros muy pronto. Antes de eso, necesitamos inicializar el nodo enemigo en simpleInitApp ():

 // configura enemyNode enemyNode = nuevo Nodo ("enemigos"); guiNode.attachChild (enemyNode);

Bien, ahora al código de reproducción real: anularemos simpleUpdate (float tpf). Este método es llamado por el motor una y otra vez, y simplemente sigue llamando a la función de generación del enemigo mientras el jugador esté vivo. (Ya configuramos los datos de usuario viva a cierto en el último tutorial.)

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); 

Y así es como engendramos a los enemigos:

 private void spawnEnemies () if (System.currentTimeMillis () - enemySpawnCooldown> = 17) enemySpawnCooldown = System.currentTimeMillis (); si (enemyNode.getQuantity () < 50)  if (new Random().nextInt((int) enemySpawnChance) == 0)  createSeeker();  if (new Random().nextInt((int) enemySpawnChance) == 0)  createWanderer();   //increase Spawn Time if (enemySpawnChance >= 1.1f) enemySpawnChance - = 0.005f; 

No te confundas por la Enemigo Engendrar variable. No está ahí para hacer que los enemigos aparezcan a una frecuencia decente, 17ms sería un intervalo demasiado corto.

Enemigo Engendrar está realmente allí para garantizar que la cantidad de enemigos nuevos sea la misma en todas las máquinas. En computadoras mas rapidas, simpleUpdate (float tpf) se llama mucho más a menudo que en los más lentos. Con esta variable verificamos cada 17ms si debemos generar nuevos enemigos..
¿Pero queremos engendrarlos cada 17ms? En realidad queremos que aparezcan en intervalos aleatorios, por lo que introducimos un Si declaración:

 if (new Random (). nextInt ((int) enemySpawnChance) == 0) 

Cuanto menor sea el valor de EnemigoSpawnChance, lo más probable es que se genere un nuevo enemigo en este intervalo de 17 ms, y por lo tanto, el enemigo tendrá que lidiar con más enemigos. Es por eso que restamos un poco de EnemigoSpawnChance Cada tick: significa que el juego se volverá más difícil con el tiempo..

Crear buscadores y vagabundos es similar a crear cualquier otro objeto:

 private void createSeeker () Spatial seeker = getSpatial ("Seeker"); seeker.setLocalTranslation (getSpawnPosition ()); seeker.addControl (nuevo SeekerControl (jugador)); seeker.setUserData ("activo", falso); enemyNode.attachChild (buscador);  private void createWanderer () Spatial wanderer = getSpatial ("Wanderer"); wanderer.setLocalTranslation (getSpawnPosition ()); wanderer.addControl (nuevo WandererControl ()); wanderer.setUserData ("activo", falso); enemyNode.attachChild (vagabundo); 

Creamos el espacio, lo movemos, agregamos un control personalizado, lo configuramos como no activo y lo adjuntamos a nuestro nodo enemigo. ¿Qué? ¿Por qué no activo? Eso es porque no queremos que el enemigo comience a perseguir al jugador tan pronto como aparezca; Queremos darle al jugador algo de tiempo para reaccionar..

Antes de entrar en los controles, necesitamos implementar el método. getSpawnPosition (). El enemigo debe aparecer al azar, pero no justo al lado del jugador:

 Vector3f privado getSpawnPosition () Vector3f pos; do pos = new Vector3f (new Random (). nextInt (settings.getWidth ()), new Random (). nextInt (settings.getHeight ()), 0);  while (pos.distanceSquared (player.getLocalTranslation ()) < 8000); return pos; 

Calculamos una nueva posición aleatoria. pos. Si está demasiado cerca del jugador, calculamos una nueva posición y repetimos hasta que está a una distancia decente.

Ahora solo necesitamos hacer que los enemigos se activen y comiencen a moverse. Lo haremos en sus controles..

Controlando el comportamiento del enemigo

Nos ocuparemos de la SeekerControl primero:

 clase pública SeekerControl extiende AbstractControl jugador espacial privado; Vector3f privado de velocidad; tiempo de desove privado largo; público SeekerControl (jugador espacial) this.player = player; velocidad = nuevo Vector3f (0,0,0); spawnTime = System.currentTimeMillis ();  @ Anular el control void protegido Update Up (float tpf) if ((Boolean) spatial.getUserData ("active")) // traducir el buscador Vector3f playerDirection = player.getLocalTranslation (). Restar (spatial.getLocalTranslation ()); playerDirection.normalizeLocal (); playerDirection.multLocal (1000f); velocity.addLocal (playerDirection); velocity.multLocal (0.8f); spatial.move (velocity.mult (tpf * 0.1f)); // rotar el buscador si (velocidad! = Vector3f.ZERO) spatial.rotateUpTo (velocity.normalize ()); spatial.rotate (0,0, FastMath.PI / 2f);  else else // maneja el estado "activo" -dif dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spatial.setUserData ("activo", verdadero);  ColorRGBA color = nuevo ColorRGBA (1,1,1, dif / 1000f); Nodo spatialNode = (Nodo) espacial; Picture pic = (Picture) spatialNode.getChild ("Seeker"); pic.getMaterial (). setColor ("Color", color);  @ Anular la protección de void controladoRender (RenderManager rm, ViewPort vp) 

Centrémonos en controlUpdate (float tpf):

Primero, necesitamos verificar si el enemigo está activo. Si no es así, tenemos que desvanecerlo lentamente.
Luego, verificamos el tiempo transcurrido desde que engendramos al enemigo y, si es lo suficientemente largo, lo activamos..

Independientemente de si lo hemos activado, debemos ajustar su color. La variable local espacial contiene el espacio al que se ha adjuntado el control, pero puede recordar que no adjuntamos el control a la imagen real; la imagen es un elemento secundario del nodo al que adjuntamos el control. (Si no sabes de qué estoy hablando, echa un vistazo al método getSpatial (nombre de la cadena) Implementamos el último tutorial.

Asi que; nos hacemos la foto como un niño de espacial, obtener su material y establecer su color en el valor adecuado. Nada especial una vez que estás acostumbrado a los espacios, materiales y nodos..

Información: Usted puede preguntarse por qué establecemos el color del material en blanco. (Los valores RGB son todos 1 en nuestro código). ¿No queremos un enemigo amarillo y rojo??
Es porque el material mezcla el color del material con los colores de la textura, por lo que si queremos mostrar la textura del enemigo tal como es, debemos mezclarlo con el blanco..

Ahora tenemos que echar un vistazo a lo que hacemos cuando el enemigo está activo. Este control se llama SeekerControl por una razón: queremos que los enemigos con este control adjunto sigan al jugador.

Para lograr eso, calculamos la dirección del buscador al jugador y agregamos este valor a la velocidad. Después de eso, disminuimos la velocidad en un 80% para que no pueda crecer infinitamente, y movemos al buscador en consecuencia.

La rotación no es nada especial: si el buscador no está parado, lo giramos en la dirección del jugador. Luego lo giramos un poco más porque el buscador en Buscador.png No está apuntando hacia arriba, sino a la derecha..

Información: los rotateUpTo (Vector3f sentido) método de Espacial gira un espacio para que su eje y apunte en la dirección dada.

Así que ese fue el primer enemigo. El código del segundo enemigo, el vagabundo, no es muy diferente:

 la clase pública WandererControl extiende AbstractControl private int screenWidth, screenHeight; Vector3f privado de velocidad; Flotador privado directionAngle; tiempo de desove privado largo; WandererControl público (int screenWidth, int screenHeight) this.screenWidth = screenWidth; this.screenHeight = screenHeight; velocidad = nuevo Vector3f (); directionAngle = new Random (). nextFloat () * FastMath.PI * 2f; spawnTime = System.currentTimeMillis ();  @ Anular el control void protegidoUpdate (float tpf) if ((Boolean) spatial.getUserData ("active")) // traducir the wanderer // cambiar la direcciónAngle un bit directionAngle + = (new Random (). NextFloat () * 20f - 10f) * tpf; System.out.println (directionAngle); Vector3f directionVector = MonkeyBlasterMain.getVectorFromAngle (directionAngle); directionVector.multLocal (1000f); velocity.addLocal (directionVector); // disminuye un poco la velocidad y mueve el errante velocity.multLocal (0.8f); spatial.move (velocity.mult (tpf * 0.1f)); // hacer que el wanderer rebote en los bordes de la pantalla Vector3f loc = spatial.getLocalTranslation (); if (loc.x screenWidth || loc.y> screenHeight) Vector3f newDirectionVector = new Vector3f (screenWidth / 2, screenHeight / 2,0) .subtract (loc); directionAngle = MonkeyBlasterMain.getAngleFromVector (newDirectionVector);  // gira el wanderer spatial.rotate (0,0, tpf * 2);  else // maneja el estado "activo" -dif dif = System.currentTimeMillis () - spawnTime; if (dif> = 1000f) spatial.setUserData ("activo", verdadero);  ColorRGBA color = nuevo ColorRGBA (1,1,1, dif / 1000f); Nodo spatialNode = (Nodo) espacial; Picture pic = (Picture) spatialNode.getChild ("Wanderer"); pic.getMaterial (). setColor ("Color", color);  @ Anular la protección de void controladoRender (RenderManager rm, ViewPort vp) 

Lo fácil primero: desvanecer al enemigo es lo mismo que en el control del buscador. En el constructor, elegimos una dirección aleatoria para el vagabundo, en la que volará una vez activado.

Propina: Si tienes más de dos enemigos, o simplemente quieres estructurar el juego de forma más limpia, puedes agregar un tercer control: EnemyControl Manejaría todo lo que todos los enemigos tenían en común: mover al enemigo, desvanecerlo, activarlo ...

Ahora a las principales diferencias:

Cuando el enemigo está activo, primero cambiamos su dirección un poco, para que el vagabundo no se mueva en línea recta todo el tiempo. Hacemos esto cambiando nuestra dirección de ángulo un poco y añadiendo el direcciónvector al velocidad. Luego aplicamos la velocidad tal como lo hacemos en el SeekerControl.

Necesitamos verificar si el errante está fuera de los bordes de la pantalla y, si es así, cambiamos el dirección de ángulo a una dirección más apropiada para que se aplique en la próxima actualización.

Finalmente, giramos un poco el vagabundo. Esto es solo porque un enemigo girando se ve más fresco.

Ahora que hemos terminado de implementar a los dos enemigos, puedes comenzar el juego y jugar un poco. Te da una pequeña mirada a cómo se jugará el juego, aunque no puedas matar a los enemigos y ellos tampoco puedan matarte a ti. Vamos a añadir eso a continuación.

Detección de colisiones

Para hacer que los enemigos maten al jugador, necesitamos saber si están chocando. Para ello, añadiremos un nuevo método., manejarColisiones, Llamada entrante simpleUpdate (float tpf):

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); manejarColisiones (); 

Y ahora el método real:

 private void handleCollisions () // ¿Debería morir el jugador? para (int i = 0; i 

Recorremos a través de todos los enemigos obteniendo la cantidad de hijos del nodo y luego obteniendo cada uno de ellos. Además, solo tenemos que comprobar si el enemigo mata al jugador cuando el enemigo está realmente activo. Si no es así, no necesitamos preocuparnos por ello. Entonces, si está activo, verificamos si el jugador y el enemigo chocan. Lo hacemos en otro método., checkCollisoin (Spatial a, Spatial b):

 private boolean checkCollision (Spatial a, Spatial b) float distance = a.getLocalTranslation (). distance (b.getLocalTranslation ()); float maxDistance = (Float) a.getUserData ("radio") + (Float) b.getUserData ("radio"); distancia de retorno <= maxDistance; 

El concepto es bastante simple: primero, calculamos la distancia entre los dos espaciales. A continuación, necesitamos saber qué tan cerca deben estar los dos espacios para ser considerados como colisionados, de modo que obtengamos el radio de cada espacio y los agreguemos. (Establecemos los datos de usuario "radio" en getSpatial (nombre de la cadena) en el tutorial anterior.) Entonces, si la distancia real es más corta o igual a esta distancia máxima, el método retorna cierto, lo que significa que chocaron.

¿Ahora que? Necesitamos matar al jugador. Vamos a crear otro método:

 private void killPlayer () player.removeFromParent (); player.getControl (PlayerControl.class) .reset (); player.setUserData ("vivo", falso); player.setUserData ("dieTime", System.currentTimeMillis ()); enemyNode.detachAllChildren (); 

Primero, separamos al jugador de su nodo principal, que lo elimina automáticamente de la escena. A continuación, tenemos que restablecer el movimiento en Control de jugador-de lo contrario, el jugador todavía podría moverse cuando vuelva a aparecer.

Luego ponemos los datos de usuario. viva a falso y crea un nuevo userdata dieTime. (Necesitaremos eso para reaparecer al jugador cuando esté muerto).

Finalmente, separamos a todos los enemigos, ya que al jugador le resultaría difícil combatir a los enemigos existentes justo cuando se genera..

Ya hemos mencionado el respawn, así que vamos a manejar eso a continuación. Una vez más, modificaremos el simpleUpdate (float tpf) método:

 @Override public void simpleUpdate (float tpf) if ((Boolean) player.getUserData ("alive")) spawnEnemies (); manejarColisiones ();  else if (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver) // spawn player player.setLocalTranslation (500,500,0); guiNode.attachChild (jugador); player.setUserData ("vivo", verdadero); 

Entonces, si el jugador no está vivo y ha estado muerto el tiempo suficiente, establecemos su posición en el centro de la pantalla, lo agregamos a la escena y finalmente establecemos sus datos de usuario. viva a cierto otra vez!

Ahora puede ser un buen momento para comenzar el juego y probar nuestras nuevas funciones. Sin embargo, le costará más que durar más de veinte segundos, porque su arma no vale nada, así que hagamos algo al respecto.

Para hacer que las balas maten a los enemigos, añadiremos un código a la manejarColisiones () método:

 // ¿Debería morir un enemigo? int i = 0; mientras yo < enemyNode.getQuantity())  int j=0; while (j < bulletNode.getQuantity())  if (checkCollision(enemyNode.getChild(i),bulletNode.getChild(j)))  enemyNode.detachChildAt(i); bulletNode.detachChildAt(j); break;  j++;  i++; 

El procedimiento para matar enemigos es prácticamente el mismo que para matar al jugador; recorremos todos los enemigos y todas las balas, verificamos si chocan y, si lo hacen, los separamos.

Ahora corre el juego y mira hasta dónde llegas!

Información: Iterar a través de cada enemigo y comparar su posición con la posición de cada bala es una muy mala manera de verificar si hay colisiones. Está bien en este ejemplo por simplicidad, pero en una real juego tendría que implementar mejores algoritmos para hacer eso, como la detección de colisiones de quadtree. Afortunadamente, el motor jMonkey utiliza el motor de física de Bullet, por lo que siempre que tenga una física 3D complicada, no tiene que preocuparse por esto..

Ahora hemos terminado con la jugabilidad principal. Seguiremos implementando agujeros negros y mostrando la puntuación y la vida del jugador, y para hacer el juego más divertido y emocionante, agregaremos efectos de sonido y mejores gráficos. Esto último se logrará a través del filtro de post-procesamiento de la floración, algunos efectos de partículas y un efecto de fondo fresco.

Antes de considerar que esta parte de la serie está terminada, agregaremos un poco de audio y el efecto de flor.


Tocando sonidos y musica

Para obtener algo de audio para nuestro juego, crearemos una nueva clase, llamada simplemente Sonar:

 Sonido público de clase música privada de AudioNode; tomas de AudioNodo [] privadas; AudioNodo privado [] explosiones; privado AudioNode [] genera; AssetManager privado assetManager; sonido público (AssetManager assetManager) this.assetManager = assetManager; disparos = nuevo AudioNodo [4]; explosiones = nuevo AudioNodo [8]; engendra = nuevo AudioNodo [8]; loadSounds ();  private void loadSounds () music = new AudioNode (assetManager, "Sounds / Music.ogg"); music.setPositional (false); music.setReverbEnabled (false); music.setLooping (true); para (int i = 0; i 

Aquí, empezamos por configurar los necesarios. AudioNodo Variables e inicializar las matrices..

A continuación, cargamos los sonidos, y para cada sonido hacemos casi lo mismo. Creamos un nuevo AudioNodo, con la ayuda del gestor de activos. Entonces, lo configuramos no posicional y deshabilitamos la reverb. (No necesitamos que el sonido sea posicional porque no tenemos salida estéreo en nuestro juego 2D, aunque podría implementarlo si lo desea). Desactivar la reverberación hace que el sonido se reproduzca tal como está en el audio real. expediente; si lo habilitamos, podríamos hacer que jME deje que el audio suene como si estuviéramos en una cueva o mazmorra, por ejemplo. Después de eso, configuramos el bucle a cierto para la musica y para falso para cualquier otro sonido.

Tocar los sonidos es bastante simple: solo llamamos sonidoX.play ().

Información: Cuando simplemente llamas jugar() en algún sonido, simplemente reproduce el sonido. Pero a veces queremos reproducir el mismo sonido dos veces o incluso más veces simultáneamente. Eso es lo que playInstance () está ahí para: crea una nueva instancia para cada sonido para que podamos reproducir el mismo sonido varias veces al mismo tiempo.

Dejaré el resto del trabajo a tu disposición: debes llamar startMusic, disparar(), explosión() (para los enemigos moribundos), y desovar() en los lugares apropiados en nuestra clase principal MonkeyBlasterMain ().

Cuando hayas terminado, verás que el juego ahora es mucho más divertido; esos pocos efectos de sonido realmente se suman a la atmósfera. Pero vamos a pulir los gráficos un poco también.


Agregando el filtro de post-procesamiento Bloom

Habilitar bloom es muy sencillo en jMonkeyEngine, ya que todos los códigos y sombreadores necesarios ya están implementados para usted. Sólo sigue adelante y pega estas líneas en simpleInitApp ():

 FilterPostProcessor fpp = new FilterPostProcessor (assetManager); BloomFilter bloom = nuevo BloomFilter (); bloom.setBloomIntensity (2f); bloom.setExposurePower (2); bloom.setExposureCutOff (0f); bloom.setBlurScale (1.5f); fpp.addFilter (floración); guiViewPort.addProcessor (fpp); guiViewPort.setClearColor (true);

He configurado el BloomFilter un poco; Si desea saber para qué están disponibles todas estas configuraciones, debe consultar el tutorial de jME en bloom.


Conclusión

Felicitaciones por terminar la segunda parte. Quedan tres partes más, ¡así que no te distraigas jugando por mucho tiempo! La próxima vez, añadiremos la GUI y los agujeros negros..