Haz un Neon Vector Shooter en jMonkeyEngine The Basics

En esta serie de tutoriales, explicaré cómo crear un juego inspirado en Geometry Wars, utilizando jMonkeyEngine. El jMonkeyEngine ("jME" para abreviar) es un motor de juegos Java 3D de código abierto. Obtenga más información en su sitio web o en nuestra guía Cómo aprender jMonkeyEngine..

Si bien jMonkeyEngine es intrínsecamente un motor de juegos en 3D, también es posible crear juegos en 2D con él..

Artículos Relacionados
Esta serie de tutoriales se basa en la serie de Michael Hoffman que explica cómo hacer el mismo juego en XNA:
  • Haz un Neon Vector Shooter en XNA

Los cinco capítulos del tutorial estarán dedicados a ciertos componentes del juego:

  1. Inicialice la escena 2D, cargue y visualice algunos gráficos, maneje la entrada.
  2. Añade enemigos, colisiones y efectos de sonido..
  3. Agregue la GUI y los agujeros negros.
  4. Añadir algunos efectos de partículas espectaculares..
  5. Añadir la cuadrícula de fondo de deformación.

Como un pequeño anticipo visual, aquí está el resultado final de nuestros esfuerzos:


... Y aquí están nuestros resultados después de este primer capítulo:


La música y los efectos de sonido que puedes escuchar en estos videos fueron creados por RetroModular, y puedes leer sobre cómo lo hizo aquí..

Los sprites son de Jacob Zinman-Jeanes, nuestro diseñador residente de Tuts +. Todas las ilustraciones se pueden encontrar en el archivo fuente de descarga zip.


La fuente es Nova Square, por Wojciech Kalinowski.

El tutorial está diseñado para ayudarte a aprender los conceptos básicos de jMonkeyEngine y crear tu primer juego con él. Si bien aprovecharemos las características del motor, no usaremos herramientas complicadas para mejorar el rendimiento. Siempre que haya una herramienta más avanzada para implementar una función, enlazaré con los tutoriales apropiados de jME, pero me limitaré a la forma simple en el tutorial en sí. Cuando mire más sobre jME, más adelante podrá desarrollar y mejorar su versión de MonkeyBlaster.

Aquí vamos!


Visión general

El primer capítulo incluirá cargar las imágenes necesarias, manejar las entradas y hacer que la nave del jugador se mueva y dispare.

Para lograr esto, necesitaremos tres clases:

  • MonkeyBlasterMain: Nuestra clase principal que contiene el bucle del juego y el juego básico.
  • Control de jugador: Esta clase determinará cómo se comporta el jugador..
  • BulletControl: Similar al anterior, esto define el comportamiento de nuestras balas..

Durante el curso del tutorial lanzaremos el código de juego general en MonkeyBlasterMain y administrar los objetos en la pantalla principalmente a través de controles y otras clases. Las características especiales, como el sonido, también tendrán sus propias clases..


Cargando el barco del jugador

Si aún no has descargado el SDK de jME, ¡ya es hora! Lo puedes encontrar en la página de inicio de jMonkeyEngine..

Crea un nuevo proyecto en el SDK de jME. Generará automáticamente la clase principal, que se verá similar a esta:

paquete monkeyblaster; import com.jme3.app.SimpleApplication; importar com.jme3.renderer.RenderManager; la clase pública MonkeyBlasterMain extiende SimpleApplication public static void main (String [] args) Main app = new Main (); app.start ();  @Override public void simpleInitApp ()  @Override public void simpleUpdate (float tpf)  @Override public void simpleRender (RenderManager rm) 

Empezaremos por anular simpleInitApp (). Este método se llama cuando se inicia la aplicación. Este es el lugar para configurar todos los componentes:

 @Override public void simpleInitApp () // configurar la cámara para juegos en 2D cam.setParallelProjection (true); cam.setLocation (nuevo Vector3f (0,0,0.5f)); getFlyByCamera (). setEnabled (false); // desactiva la vista de estadísticas (puedes dejarla activada, si quieres) setDisplayStatView (false); setDisplayFps (false); 

Primero tendremos que ajustar la cámara un poco, ya que jME es básicamente un motor de juegos en 3D. La vista de estadísticas en el segundo párrafo puede ser muy interesante, pero así es como se apaga.

Cuando comienzas el juego ahora, puedes ver ... nada.

Bueno, ¡necesitamos cargar al jugador en el juego! Crearemos un pequeño método para manejar la carga de nuestras entidades:

 Private Spatial getSpatial (nombre de cadena) Nodo nodo = nuevo Nodo (nombre); // cargar imagen Imagen pic = nueva Imagen (nombre); Texture2D tex = (Texture2D) assetManager.loadTexture ("Textures /" + name + ". Png"); pic.setTexture (assetManager, tex, true); // ajustar el ancho de flotación de la imagen = tex.getImage (). getWidth (); altura flotante = tex.getImage (). getHeight (); pic.setWidth (ancho); pic.setHeight (altura); pic.move (-width / 2f, -height / 2f, 0); // agregar un material a la imagen Material picMat = nuevo Material (assetManager, "Common / MatDefs / Gui / Gui.j3md"); picMat.getAdditionalRenderState (). setBlendMode (BlendMode.AlphaAdditive); node.setMaterial (picMat); // establece el radio del // espacial (usando ancho solo como una aproximación simple) node.setUserData ("radio", ancho / 2); // adjunte la imagen al nodo y devuélvala node.attachChild (pic); nodo de retorno; 

Al principio creamos un nodo que contendrá nuestra imagen..

Propina: La gráfica de escena jME consiste en espaciales (nodos, imágenes, geometrías, etc.). Cada vez que agregues algo espacial a la guiNodo, se hace visible en la escena. Usaremos el guiNodo Porque estamos creando un juego en 2D. Puede adjuntar spatials a otros spatials y, por lo tanto, organizar su escena. Para convertirse en un verdadero maestro de la gráfica de escenas, recomiendo este tutorial de gráficas de escenas jME.

Después de crear el nodo, cargamos la imagen y aplicamos la textura adecuada. Aplicar el tamaño correcto a la imagen es bastante fácil de entender, pero ¿por qué necesitamos moverlo??

Cuando carga una imagen en jME, el centro de rotación no está en el medio, sino en una esquina de la imagen. Pero podemos mover la imagen a la mitad de su ancho a la izquierda y a la mitad de su altura hacia arriba, y agregarla a otro nodo. Luego, cuando giramos el nodo padre, la imagen en sí gira alrededor de su centro.

El siguiente paso es agregar un material a la imagen. Un material determina cómo se mostrará la imagen. En este ejemplo, usamos el material GUI predeterminado y configuramos Modo de mezcla a Aditivo alfa. Esto significa que las partes transparentes superpuestas de varias imágenes se harán más brillantes. Esto será útil más adelante para hacer las explosiones 'más brillantes'..

Finalmente, agregamos nuestra imagen al nodo y la devolvemos..

Ahora tenemos que añadir el jugador a la guiNodo. Vamos a extender simpleInitApp un poco más:

// configura el jugador player = getSpatial ("Player"); player.setUserData ("vivo", verdadero); player.move (settings.getWidth () / 2, settings.getHeight () / 2, 0); guiNode.attachChild (jugador);

En resumen: cargamos el reproductor, configuramos algunos datos, lo movemos a la mitad de la pantalla y lo adjuntamos a la guiNodo para que se muestre.

Datos del usuario es simplemente algunos datos que puede adjuntar a cualquier espacio. En este caso, añadimos un booleano y lo llamamos viva, para que podamos ver si el jugador está vivo. Usaremos eso mas tarde.

Ahora, ejecuta el programa! Deberías poder ver al jugador en el medio. Por el momento es bastante aburrido, lo admito. Así que vamos a añadir algo de acción!


Manejo de entrada y movimiento del jugador

La entrada de jMonkeyEngine es bastante simple una vez que lo has hecho una vez. Comenzamos por implementar un Oyente de acción:

La clase pública MonkeyBlasterMain extiende los implementos SimpleApplication ActionListener 

Ahora, para cada clave, agregaremos la asignación de entrada y el oyente en simpleInitApp ():

 inputManager.addMapping ("left", nuevo KeyTrigger (KeyInput.KEY_LEFT)); inputManager.addMapping ("right", nuevo KeyTrigger (KeyInput.KEY_RIGHT)); inputManager.addMapping ("up", nuevo KeyTrigger (KeyInput.KEY_UP)); inputManager.addMapping ("down", nuevo KeyTrigger (KeyInput.KEY_DOWN)); inputManager.addMapping ("return", nuevo KeyTrigger (KeyInput.KEY_RETURN)); inputManager.addListener (este, "left"); inputManager.addListener (this, "right"); inputManager.addListener (este, "arriba"); inputManager.addListener (este, "abajo"); inputManager.addListener (este, "return");

Cada vez que se presiona o suelta alguna de esas teclas, el método onAcción se llama. Antes de entrar en lo que realmente hacer cuando se presiona alguna tecla, debemos agregar un control a nuestro jugador.

Información: Los controles representan ciertos comportamientos de los objetos en la escena. Por ejemplo, podría agregar un FightControl y un IdleControl a un enemigo AI. Dependiendo de la situación, puede habilitar y deshabilitar o adjuntar y desconectar controles.

Nuestro Control de jugador simplemente se encargará de mover el jugador cada vez que se presione una tecla, girándola en la dirección correcta y asegurándose de que el jugador no abandone la pantalla.

Aqui tienes:

la clase pública PlayerControl extiende AbstractControl private int screenWidth, screenHeight; // ¿El jugador se está moviendo actualmente? booleano público arriba, abajo, izquierda, derecha; // velocidad del jugador velocidad de flotación privada = 800f; // lastRotation del jugador private float lastRotation; Public PlayerControl (int ancho, int alto) this.screenWidth = ancho; this.screenHeight = altura;  @ Anular el control void protegido Actualizar Actualizar (float tpf) // mover el jugador en una dirección determinada // si no está fuera de la pantalla si (arriba) si (spatial.getLocalTranslation (). Y < screenHeight - (Float)spatial.getUserData("radius"))  spatial.move(0,tpf*speed,0);  spatial.rotate(0,0,-lastRotation + FastMath.PI/2); lastRotation=FastMath.PI/2;  else if (down)  if (spatial.getLocalTranslation().y > (Flotante) spatial.getUserData ("radio")) spatial.move (0, tpf * -speed, 0);  spatial.rotate (0,0, -lastRotation + FastMath.PI * 1.5f); lastRotation = FastMath.PI * 1.5f;  else if (izquierda) if (spatial.getLocalTranslation (). x> (Float) spatial.getUserData ("radio")) spatial.move (tpf * -speed, 0,0);  spatial.rotate (0,0, -lastRotation + FastMath.PI); lastRotation = FastMath.PI;  else if (derecha) if (spatial.getLocalTranslation (). x < screenWidth - (Float)spatial.getUserData("radius"))  spatial.move(tpf*speed,0,0);  spatial.rotate(0,0,-lastRotation + 0); lastRotation=0;   @Override protected void controlRender(RenderManager rm, ViewPort vp)  // reset the moving values (i.e. for spawning) public void reset()  up = false; down = false; left = false; right = false;  

Bueno; Ahora, echemos un vistazo al código pieza por pieza..

 private int screenWidth, screenHeight; // ¿El jugador se está moviendo actualmente? booleano público arriba, abajo, izquierda, derecha; // velocidad del jugador velocidad de flotación privada = 800f; // lastRotation del jugador private float lastRotation; Public PlayerControl (int ancho, int alto) this.screenWidth = ancho; this.screenHeight = altura; 

Primero, inicializamos algunas variables, definiendo en qué dirección y qué tan rápido se mueve el jugador, y qué tan lejos está girado. Entonces, establecemos la Ancho de pantalla y screenHeight, El cual necesitaremos en el próximo gran método..

controlUpdate (float tpf) Se llama automáticamente por jME cada ciclo de actualización. La variable tpf Indica el tiempo transcurrido desde la última actualización. Esto es necesario para controlar la velocidad: si algunas computadoras tardan el doble en calcular una actualización que otras, entonces el jugador debería moverse dos veces más lejos en una sola actualización en esas computadoras.

Ahora a la primera Si declaración:

 if (arriba) if (spatial.getLocalTranslation (). y < screenHeight - (Float)spatial.getUserData("radius"))  spatial.move(0,tpf*speed,0); 

Verificamos si el jugador está subiendo y, si es así, verificamos si puede seguir subiendo. Si está lo suficientemente lejos de la frontera, simplemente lo movemos un poco hacia arriba.

Ahora en la rotación:

 spatial.rotate (0,0, -lastRotation + FastMath.PI / 2); lastRotation = FastMath.PI / 2;

Rotamos el jugador de vuelta por lastRotation Para enfrentar su dirección original. Desde esta dirección, podemos rotar el jugador en la dirección que queremos que mire. Finalmente, salvamos la rotación real..

Usamos el mismo tipo de lógica para las cuatro direcciones. los Reiniciar() El método solo está aquí para configurar todos los valores a cero nuevamente, para usarlos cuando se reaparece el jugador.

Entonces, finalmente tenemos el control de nuestro jugador. Es hora de agregarlo al espacio real. Simplemente agregue la siguiente línea a la simpleInitApp () método:

player.addControl (nuevo PlayerControl (settings.getWidth (), settings.getHeight ()));

El objeto ajustes esta incluido en la clase SimpleApplication. Contiene datos sobre la configuración de pantalla del juego..

Si empezamos el juego ahora, todavía no está sucediendo nada. Necesitamos decirle al programa qué hacer cuando se presiona una de las teclas asignadas. Para ello, anularemos la onAcción método:

 public void onAction (Nombre de cadena, booleano isPressed, float tpf) if ((Boolean) player.getUserData ("alive")) if (name.equals ("up")) player.getControl (PlayerControl.class). arriba = está presionado;  else if (name.equals ("down")) player.getControl (PlayerControl.class) .down = isPressed;  else if (name.equals ("left")) player.getControl (PlayerControl.class) .left = isPressed;  else if (name.equals ("right")) player.getControl (PlayerControl.class) .right = isPressed; 

Para cada tecla pulsada, le decimos al Control de jugador El nuevo estado de la clave. Ahora es el momento de comenzar nuestro juego y ver algo que se mueve en la pantalla!

Cuando está contento de comprender los conceptos básicos de la gestión de entrada y comportamiento, es hora de hacer lo mismo otra vez, esta vez, para las balas.


Añadiendo un poco de acción de bala

Si queremos tener alguna real acción en marcha, tenemos que ser capaces de disparar a algunos enemigos. Vamos a seguir el mismo procedimiento básico que en el paso anterior: administrar la información, crear algunas viñetas y agregarles un comportamiento.

Para manejar la entrada del mouse, implementaremos otro oyente:

La clase pública MonkeyBlasterMain extiende los implementos SimpleApplication ActionListener, AnalogListener 

Antes de que suceda algo, debemos agregar el mapeo y el oyente como hicimos la última vez. Lo haremos en el simpleInitApp () Método, junto con la otra inicialización de entrada:

 inputManager.addMapping ("mousePick", nuevo MouseButtonTrigger (MouseInput.BUTTON_LEFT)); inputManager.addListener (este, "mousePick");

Cada vez que pulsamos con el ratón, el método. onAnalog se llama. Antes de entrar en el rodaje real, necesitamos implementar un pequeño método de ayuda., Vector3f getAimDirection (), lo que nos dará la dirección para disparar restando la posición del jugador de la del ratón:

 Vector3f privado getAimDirection () Vector2f mouse = inputManager.getCursorPosition (); Vector3f playerPos = player.getLocalTranslation (); Vector3f dif = nuevo Vector3f (mouse.x-playerPos.x, mouse.y-playerPos.y, 0); return dif.normalizeLocal (); 
Propina: Al unir objetos a la guiNodo, sus unidades de traducción locales son iguales a un píxel. Esto nos facilita el cálculo de la dirección, ya que la posición del cursor también se especifica en unidades de píxeles..

Ahora que tenemos una dirección para disparar, implementemos el disparo real:

 public void onAnalog (nombre de cadena, valor flotante, tpf flotante) if ((Boolean) player.getUserData ("alive")) if (name.equals ("mousePick")) // dispara Bullet if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = nuevo Vector3f (aim.y / 3, -aim.x / 3,0); // init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nuevo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nuevo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2); 

Bien, entonces, vamos a pasar por esto:

 if (System.currentTimeMillis () - bulletCooldown> 83f) bulletCooldown = System.currentTimeMillis (); Vector3f aim = getAimDirection (); Vector3f offset = nuevo Vector3f (aim.y / 3, -aim.x / 3,0);

Si el jugador está vivo y se hace clic en el botón del mouse, nuestro código primero verifica si el último disparo se realizó hace al menos 83 ms (bulletCooldown es una variable larga que inicializamos al comienzo de la clase). Si es así, entonces podemos disparar y calculamos la dirección correcta para apuntar y el desplazamiento.

// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nuevo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nuevo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

Queremos generar balas gemelas, una al lado de la otra, así que tendremos que agregar un poco de compensación a cada una de ellas. Un desplazamiento apropiado es ortogonal a la dirección del objetivo, que se logra fácilmente cambiando el X y y Valores y negando uno de ellos. El segundo será simplemente una negación del primero..

// init bullet 1 Spatial bullet = getSpatial ("Bullet"); Vector3f finalOffset = aim.add (offset) .mult (30); Vector3f trans = player.getLocalTranslation (). Add (finalOffset); bullet.setLocalTranslation (trans); bullet.addControl (nuevo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet); // init bullet 2 Spatial bullet2 = getSpatial ("Bullet"); finalOffset = aim.add (offset.negate ()). mult (30); trans = player.getLocalTranslation (). add (finalOffset); bullet2.setLocalTranslation (trans); bullet2.addControl (nuevo BulletControl (aim, settings.getWidth (), settings.getHeight ())); bulletNode.attachChild (bullet2);

El resto debe parecer bastante familiar: inicializamos la bala utilizando nuestra propia getSpatial Método desde el principio. Luego lo traducimos al lugar correcto y lo adjuntamos al nodo. Pero espera, ¿qué nodo?

Organizaremos nuestras entidades en nodos específicos, por lo que tiene sentido crear un nodo en el que podamos adjuntar todas nuestras viñetas. Para mostrar los hijos de ese nodo, tendremos que adjuntarlo a la guiNodo.

La inicialización en simpleInitApp () es bastante sencillo:

// configura el bulletNode bulletNode = new Node ("bullets"); guiNode.attachChild (bulletNode);

Si sigues adelante y comienzas el juego, podrás ver las balas que aparecen, ¡pero no se están moviendo! Si quieres ponerte a prueba, haz una pausa en la lectura y piensa por ti mismo lo que tenemos que hacer para que se muevan.

...

Lo averiguaste?

Necesitamos agregar un control a cada bala que se ocupará de su movimiento. Para ello, crearemos otra clase llamada. BulletControl:

la clase pública BulletControl extiende AbstractControl private int screenWidth, screenHeight; velocidad de flotación privada = 1100f; dirección pública Vector3f; rotación de flotadores privados; Public BulletControl (Vector3f direction, int screenWidth, int screenHeight) this.direction = direction; this.screenWidth = screenWidth; this.screenHeight = screenHeight;  @ Anular el control void ControlUpdate (float tpf) // movement spatial.move (direction.mult (speed * tpf)); // rotación float actualRotation = MonkeyBlasterMain.getAngleFromVector (direction); if (actualRotation! = rotación) spatial.rotate (0,0, actualRotation - rotación); rotación = actualRotación;  // comprueba los límites Vector3f loc = spatial.getLocalTranslation (); if (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent();   @Override protected void controlRender(RenderManager rm, ViewPort vp)  

Un rápido vistazo a la estructura de la clase muestra que es bastante similar a la Control de jugador clase. La principal diferencia es que no tenemos ninguna clave para verificar, y sí tenemos una dirección variable. Simplemente movemos la bala en su dirección y la giramos en consecuencia.

 Vector3f loc = spatial.getLocalTranslation (); if (loc.x> screenWidth || loc.y> screenHeight || loc.x < 0 || loc.y < 0)  spatial.removeFromParent(); 

En el último bloque, verificamos si la viñeta está fuera de los límites de la pantalla y, de ser así, la eliminamos de su nodo principal, que eliminará el objeto..

Es posible que haya atrapado este método de llamada:

MonkeyBlasterMain.getAngleFromVector (direction);

Se refiere a un método auxiliar matemático estático corto en la clase principal. Creé dos de ellos, uno que convierte un ángulo en un vector en el espacio 2D y el otro que convierte esos vectores nuevamente en un valor de ángulo.

 getAngleFromVector (Vector3f vec) Vector2f vec2 = new Vector2f (vec.x, vec.y); devolver vec2.getAngle ();  público estático Vector3f getVectorFromAngle (ángulo de flotación) devolver nuevo Vector3f (FastMath.cos (ángulo), FastMath.sin (ángulo), 0); 
Propina: Si te sientes bastante confundido por todas esas operaciones vectoriales, hazte un favor y profundiza en algunos tutoriales sobre matemáticas vectoriales. Es esencial tanto en el espacio 2D como en el 3D. Mientras lo hace, también debe buscar la diferencia entre grados y radianes. Y si quiere involucrarse más en la programación de juegos en 3D, los cuaterniones también son increíbles ...

Ahora volvamos a la vista general principal: creamos una escucha de entrada, inicializamos dos viñetas y creamos una BulletControl clase. Lo único que queda es agregar un BulletControl a cada bala al inicializarlo:

bullet.addControl (nuevo BulletControl (aim, settings.getWidth (), settings.getHeight ()));

Ahora el juego es mucho más divertido.!



Conclusión

Si bien no es exactamente un desafío volar y disparar algunas balas, al menos puedes hacer alguna cosa. Pero no te desesperes, después del siguiente tutorial tendrás dificultades para escapar de las hordas de enemigos.!