Haz un Neon Vector Shooter con jME Warping Grid

En la serie hasta ahora, hemos codificado el juego, agregado enemigos, y hemos mejorado las cosas con efectos de floración y partículas. En esta parte final, crearemos una cuadrícula de fondo dinámica y combada..


Visión general

Este video muestra la cuadrícula en acción:


Haremos la cuadrícula utilizando una simulación de resorte: en cada intersección de la cuadrícula, colocaremos un peso pequeño (una masa puntual), y conectaremos estos pesos mediante resortes. Estos resortes solo tirarán y nunca empujarán, como una banda de goma. Para mantener la cuadrícula en posición, las masas alrededor del borde de la cuadrícula se anclarán en su lugar.

A continuación se muestra un diagrama de la disposición..


Vamos a crear una clase llamada Cuadrícula para crear este efecto. Sin embargo, antes de trabajar en la propia cuadrícula, necesitamos hacer dos clases auxiliares: Primavera y PointMass.


La clase de PointMass

los PointMass clase representa las masas a las que uniremos los manantiales. Los resortes nunca se conectan directamente a otros resortes. En cambio, aplican una fuerza a las masas que conectan, que a su vez pueden estirar otros resortes..

 clase pública PointMass posición Vector3f privada; Vector3f privado velocidad = Vector3f.ZERO; Flotador privado inverseMass; Aceleración privada de Vector3f = Vector3f.ZERO; Amortiguación del flotador privado = 0.98f; PointMass pública (posición Vector3f, flotación inverseMass) this.position = position; this.inverseMass = inverseMass;  public void applyForce (Vector3f force) acceleration.addLocal (force.mult (inverseMass));  aumento de vacío públicoDamping (factor flotante) amortiguamiento * = factor;  actualización de vacío público (float tpf) velocity.addLocal (acceleration.mult (1f)); position.addLocal (velocity.mult (0.6f)); aceleración = Vector3f.ZERO.clone (); if (velocity.lengthSquared () < 0.0001f)  velocity = Vector3f.ZERO.clone();  velocity.multLocal(damping); damping = 0.98f; damping = 0.8f; position.z *= 0.9f; if (position.z < 0.01) position.z = 0;  public Vector3f getPosition()  return position;  public Vector3f getVelocity()  return velocity;  

Hay algunos puntos interesantes sobre esta clase. Primero, note que almacena el inverso de la misa, 1 / masa. A menudo, esto es una buena idea en las simulaciones de física porque las ecuaciones de la física tienden a usar la inversa de la masa con mayor frecuencia, y porque nos brinda una manera fácil de representar objetos inmóviles, pesados ​​e infinitos al establecer la masa inversa en cero..

La clase también contiene un mojadura Variable, que actúa para desacelerar gradualmente la masa hacia abajo. Esto se usa aproximadamente como fricción o resistencia del aire. Esto ayuda a que la cuadrícula se detenga y aumente la estabilidad de la simulación del resorte..

los Actualizar() El método hace el trabajo de mover la masa puntual de cada cuadro. Comienza haciendo una integración de Euler simpléctica, lo que significa que solo agregamos la aceleración a la velocidad y luego agregamos la velocidad actualizada a la posición. Esto difiere de la integración estándar de Euler en la que actualizaríamos la velocidad. después actualizando la posición.

Propina: Symplectic Euler es mejor para las simulaciones de primavera porque conserva la energía. Si utiliza la integración regular de Euler y crea resortes sin amortiguación, tenderán a estirarse más y más a medida que ganen energía, rompiendo su simulación..

Después de actualizar la velocidad y la posición, verificamos si la velocidad es muy pequeña y, si lo es, la ponemos a cero. Esto puede ser importante para el rendimiento debido a la naturaleza de los números de punto flotante desnormalizados.

los IncreaseDamping () El método se utiliza para aumentar temporalmente la cantidad de amortiguación. Usaremos esto más adelante para ciertos efectos..


La clase de primavera

Un resorte conecta dos masas puntuales y, si se extiende más allá de su longitud natural, aplica una fuerza que une las masas. Los resortes siguen una versión modificada de la Ley de Hooke con amortiguación:

\ [f = -kx - bv \]

  • \ (f \) es la fuerza producida por el resorte.
  • \ (k \) es la constante del resorte, o la "rigidez" del resorte.
  • \ (x \) es la distancia que se extiende el resorte más allá de su longitud natural.
  • \ (b \) es el factor de amortiguamiento.
  • \ (v \) es la velocidad.

El código para el Primavera La clase es la siguiente:

 clase pública Spring private PointMass end1; PointMass privado end2; flotador privado targetLength; rigidez del flotador privado; amortiguación de flotadores privados; público Spring (PointMass end1, PointMass end2, rigidez del flotador, amortiguación del flotador, nodo del gridnode, booleano visible, línea predeterminada de geometría) this.end1 = end1; this.end2 = end2; esta rigidez = rigidez; this.damping = amortiguamiento; targetLength = end1.getPosition (). distance (end2.getPosition ()) * 0.95f; if (visible) defaultLine.addControl (new LineControl (end1, end2)); gridNode.attachChild (defaultLine);  actualización pública vacía (float tpf) Vector3f x = end1.getPosition (). restar (end2.getPosition ()); longitud de flotación = x.length (); if (length> targetLength) x.normalizeLocal (); x.multLocal (length - targetLength); Vector3f dv = end2.getVelocity (). Restar (end1.getVelocity ()); Vector3f force = x.mult (rigidez); force.subtract (dv.mult (amortiguamiento / 10f)); end1.applyForce (force.negate ()); end2.applyForce (fuerza); 

Cuando creamos un resorte, establecemos que la longitud natural del resorte sea ligeramente menor que la distancia entre los dos puntos finales. Esto mantiene la rejilla tensa, incluso cuando está en reposo, y mejora un poco la apariencia..

los Actualizar() El método primero verifica si el resorte se estira más allá de su longitud natural. Si no se estira, no pasa nada. Si es así, usamos la Ley de Hooke modificada para encontrar la fuerza del resorte y aplicarla a las dos masas conectadas.

Hay otra clase que necesitamos crear para mostrar las líneas correctamente. los LineControl Se encargará de mover, escalar y rotar las líneas:

 la clase pública LineControl extiende AbstractControl private PointMass end1, end2; LineControl público (PointMass end1, PointMass end2) this.end1 = end1; this.end2 = end2;  @ Anular el control void ControlUpdate (float tpf) // movement spatial.setLocalTranslation (end1.getPosition ()); // scale Vector3f dif = end2.getPosition (). restar (end1.getPosition ()); spatial.setLocalScale (dif.length ()); // rotación spatial.lookAt (end2.getPosition (), nuevo Vector3f (1,0,0));  @Override protegido void controlRender (RenderManager rm, ViewPort vp) 

Creando la Grilla

Ahora que tenemos las clases anidadas necesarias, estamos listos para crear la cuadrícula. Comenzamos creando PointMass Objetos en cada intersección de la cuadrícula. También creamos un ancla inamovible. PointMass Objetos para mantener la rejilla en su lugar. Luego unimos las masas con manantiales:

 public class Grid private Node gridNode; resortes privados [] manantiales; puntos de PointMass [] [] privados; Geometría privada defaultLine; Geometría privada thickLine; cuadrícula pública (tamaño del rectángulo, espaciado Vector2f, guiNode del nodo, AssetManager assetManager) gridNode = new Node (); guiNode.attachChild (gridNode); defaultLine = createLine (1f, assetManager); thickLine = createLine (3f, assetManager); ArrayList springList = new ArrayList (); rigidez del flotador = 0.28f; amortiguación del flotador = 0.06f; int numColumns = (int) (size.width / spacing.x) + 2; int numRows = (int) (size.height / spacing.y) + 2; points = new PointMass [numColumns] [numRows]; PointMass [] [] fixedPoints = new PointMass [numColumns] [numRows]; // crear las masas de puntos float xCoord = 0, yCoord = 0; para (int fila = 0; fila < numRows; row++)  for (int column = 0; column < numColumns; column++)  points[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),1); fixedPoints[column][row] = new PointMass(new Vector3f(xCoord,yCoord,0),0); xCoord += spacing.x;  yCoord += spacing.y; xCoord = 0;  // link the point masses with springs Geometry line; for (int y=0; y 0) si (y% 3 == 0) línea = thickLine;  else line = defaultLine;  springList.add (nuevo Spring (puntos [x-1] [y], puntos [x] [y], rigidez, amortiguamiento, gridNode, true, line.clone ()));  if (y> 0) if (x% 3 == 0) line = thickLine;  else line = defaultLine;  springList.add (nuevo Spring (puntos [x] [y-1], puntos [x] [y], rigidez, amortiguamiento, gridNode, true, line.clone ())); 

El primero para el bucle crea masas regulares e inmóviles en cada intersección de la cuadrícula. En realidad no usaremos todas las masas inamovibles, y las masas no utilizadas simplemente serán recolectadas en algún momento después de que el constructor termine. Podríamos optimizar evitando la creación de objetos innecesarios, pero como la cuadrícula normalmente solo se crea una vez, no habrá mucha diferencia..

Además de usar masas de puntos de anclaje alrededor del borde de la cuadrícula, también usaremos algunas masas de anclaje dentro de la cuadrícula. Se utilizarán para ayudar a que la rejilla vuelva a su posición original muy suavemente después de deformarse..

Como los puntos de anclaje nunca se mueven, no es necesario actualizarlos en cada fotograma. Simplemente podemos engancharlos a los resortes y olvidarlos. Por lo tanto, no tenemos una variable miembro en el Cuadrícula clase para estas masas.

Hay una serie de valores que puede modificar en la creación de la cuadrícula. Los más importantes son la rigidez y la amortiguación de los resortes. La rigidez y la amortiguación de los anclajes de borde y los anclajes interiores se establecen independientemente de los resortes principales. Los valores más altos de rigidez harán que los resortes oscilen más rápidamente, y los valores más altos de amortiguación harán que los resortes se desaceleren más rápido.

Hay una última cosa a mencionar: la createLine () método.

 geometría privada createLine (grosor de flotación, AssetManager assetManager) Vector3f [] vértices = Vector3f nuevo (0,0,0), vector3f nuevo (0,0,1); Índices int [] = 0,1; Mesh lineMesh = new Mesh (); lineMesh.setMode (Mesh.Mode.Lines); lineMesh.setLineWidth (espesor); lineMesh.setBuffer (VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer (vértices)); lineMesh.setBuffer (VertexBuffer.Type.Index, 1, BufferUtils.createIntBuffer (índices)); lineMesh.updateBound (); Geometry lineGeom = new Geometry ("lineMesh", lineMesh); Material matWireframe = nuevo Material (assetManager, "Common / MatDefs / Misc / Unshaded.j3md"); matWireframe.getAdditionalRenderState (). setFaceCullMode (RenderState.FaceCullMode.Off); matWireframe.setColor ("Color", nuevo ColorRGBA (0.118f, 0.118f, 0.545f, 0.25f)); matWireframe.getAdditionalRenderState (). setBlendMode (BlendMode.AlphaAdditive); lineGeom.setMaterial (matWireframe); línea de retornoGeom; 

Aquí, básicamente creamos una línea especificando los vértices de la línea y el orden de los vértices, creando una malla, agregando un material azul, y así sucesivamente. Si desea comprender exactamente el proceso de creación de la línea, siempre puede consultar los tutoriales de jME..

¿Por qué la creación de la línea tiene que ser tan complicada? ¿No es 'simplemente' una línea simple? Sí, lo es, pero hay que mirar lo que jME pretende ser. Por lo general, en los juegos 3D, no tienes líneas o triángulos en el juego, sino modelos con texturas y animaciones. Entonces, si bien es posible generar una sola línea en jME, el enfoque principal es importar modelos que se hayan generado con otro software, como Blender.

Manipulando la rejilla

Para que la cuadrícula se mueva, debemos actualizarla cada cuadro. Esto es muy simple, como ya hicimos todo el trabajo duro en el PointMass y Primavera clases.

 actualización pública vacía (float tpf) para (int i = 0; i 

Ahora, agregaremos algunos métodos que manipulan la cuadrícula. Puedes agregar métodos para cualquier tipo de manipulación que puedas imaginar. Aquí implementaremos tres tipos de manipulaciones: empujar parte de la cuadrícula en una dirección determinada, empujar la cuadrícula hacia afuera desde algún punto y tirar de la cuadrícula hacia algún punto. Los tres afectarán a la cuadrícula dentro de un radio dado desde algún punto objetivo.

A continuación se muestran algunas imágenes de estas manipulaciones en acción:


Ola creada empujando la cuadrícula a lo largo del eje z.
Balas que repelen la rejilla hacia afuera..
Chupando la rejilla hacia adentro..

Y aquí están los métodos para los efectos:

 public void applyDirectedForce (Vector3f force, Vector3f position, float radius) para (int x = 0; x 

Usando la cuadrícula en Shape Blaster

Ahora es el momento de usar la cuadrícula en nuestro juego. Comenzamos declarando una Cuadrícula variable en MonkeyBlasterMain e inicializándolo en simpleInitApp ():

 Tamaño del rectángulo = nuevo Rectángulo (0, 0, settings.getWidth (), settings.getHeight ()); Espacio Vector2f = nuevo Vector2f (25,25); grid = new Grid (tamaño, espaciado, guiNode, assetManager);

Entonces, tenemos que llamar grid.update (float tpf) desde el actualización simple método:

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

A continuación, debemos llamar a los métodos de efecto desde los lugares correctos en nuestro juego..

El primero, crear una ola cuando el jugador genera, es bastante fácil: simplemente extendemos el lugar donde generamos al jugador simpleUpdate (float tpf) Con la siguiente línea:

 grid.applyDirectedForce (nuevo Vector3f (0,0,5000), player.getLocalTranslation (), 100);

Tenga en cuenta que aplicamos una fuerza en el dirección z. Es posible que tengamos un juego en 2D pero, dado que jME es un motor en 3D, también podemos usar fácilmente los efectos en 3D. Si tuviéramos que rotar la cámara, veríamos que la cuadrícula rebota hacia adentro y hacia afuera..

El segundo y tercer efecto deben manejarse en los controles. Cuando las balas vuelan por el juego, llaman a este método en controlUpdate (float tpf):

 grid.applyExplosiveForce (direction.length () * (18f), spatial.getLocalTranslation (), 80);

Esto hará que las balas repelan la cuadrícula proporcionalmente a su velocidad. Eso fue bastante facil.

Es similar con los agujeros negros:

 grid.applyImplosiveForce (FastMath.sin (sprayAngle / 2) * 10 +20, spatial.getLocalTranslation (), 250);

Esto hace que el agujero negro aspire la rejilla con una cantidad variable de fuerza. Reutilizé el sprayAngle variable, lo que hará que la fuerza en la cuadrícula vibre en sincronía con el ángulo en que rocía las partículas (aunque a la mitad de la frecuencia debida a la división en dos). La fuerza pasada variará sinusoidalmente entre 10 y 30.

Para que este trabajo funcione, no debes olvidar pasar. cuadrícula a BulletControl y BlackHoleControl.


Interpolación

Podemos optimizar la cuadrícula mejorando la calidad visual para un número dado de resortes sin aumentar significativamente el costo de rendimiento.

Haremos que la cuadrícula sea más densa agregando segmentos de línea dentro de las celdas de cuadrícula existentes. Lo hacemos dibujando líneas desde el punto medio de un lado de la celda hasta el punto medio del lado opuesto. La imagen de abajo muestra las nuevas líneas interpoladas en rojo:

Crearemos esas líneas adicionales en el constructor de nuestra Cuadrícula clase. Si le echas un vistazo, verás dos para Bucles donde unimos las masas puntuales con los resortes. Solo inserta este bloque de código allí:

 if (x> 0 && y> 0) Geometry additionalLine = defaultLine.clone (); additionalLine.addControl (nuevo AdditionalLineControl (puntos [x-1] [y], puntos [x] [y], puntos [x-1] [y-1], puntos [x] [y-1])); gridNode.attachChild (additionalLine); Geometría adicionalLine2 = defaultLine.clone (); additionalLine2.addControl (nuevo AdditionalLineControl (puntos [x] [y-1], puntos [x] [y], puntos [x-1] [y-1], puntos [x-1] [y])); gridNode.attachChild (additionalLine2); 

Pero, como saben, crear objetos no es lo único que debemos hacer; También necesitamos agregarles un control para que se comporten correctamente. Como se puede ver arriba, el Control adicional de línea se pasan cuatro masas de puntos para que pueda calcular su posición, rotación y escala:

 la clase pública AdditionalLineControl extiende AbstractControl private PointMass end11, end12, end21, end22; control AdditionalLine público (PointMass end11, PointMass end12, PointMass end21, PointMass end22) this.end11 = end11; this.end12 = end12; this.end21 = end21; this.end22 = end22;  @ Anular el control void ControlUpdate (float tpf) // movement spatial.setLocalTranslation (position1 ()); // escala Vector3f dif = position2 (). restar (position1 ()); spatial.setLocalScale (dif.length ()); // rotación spatial.lookAt (position2 (), nuevo Vector3f (1,0,0));  private Vector3f position1 () devolver nuevo Vector3f (). interpolate (end11.getPosition (), end12.getPosition (), 0.5f);  private Vector3f position2 () devolver nuevo Vector3f (). interpolate (end21.getPosition (), end22.getPosition (), 0.5f);  @Override protegido void controlRender (RenderManager rm, ViewPort vp) 

Que sigue?

Tenemos el juego básico y los efectos implementados. Depende de usted convertirlo en un juego completo y pulido con su propio sabor. Intente agregar algunas nuevas mecánicas interesantes, algunos efectos nuevos geniales o una historia única. En caso de que no esté seguro de por dónde empezar, aquí hay algunas sugerencias:

  • Crea nuevos tipos de enemigos como serpientes o enemigos explosivos..
  • Crea nuevos tipos de armas, como buscar misiles o una pistola de rayos..
  • Añadir una pantalla de título y menú principal..
  • Añadir una tabla de puntuación más alta.
  • Agrega algunos power-ups, como un escudo o bombas. Para obtener puntos de bonificación, se creativo con tus power-ups. Puedes hacer power-ups que manipulen la gravedad, alteren el tiempo o crezcan como organismos. Puedes unir una bola de demolición gigante basada en la física a la nave para aplastar a los enemigos. Experimenta para encontrar potenciadores que sean divertidos y que ayuden a que tu juego destaque..
  • Crea múltiples niveles. Los niveles más difíciles pueden introducir enemigos más duros y armas y potenciadores más avanzados..
  • Permitir que un segundo jugador se una con un gamepad.
  • Permite que la arena se desplace para que sea más grande que la ventana del juego.
  • Añadir peligros ambientales como los láseres..
  • Agrega una tienda o un sistema de nivelación y permite que el jugador gane actualizaciones.

Gracias por leer!