Hasta ahora, en esta serie sobre la creación de un juego inspirado en Geometry Wars en jMonkeyEngine, hemos implementado la mayor parte del juego y el audio. En esta parte, terminaremos el juego agregando agujeros negros, y agregaremos algo de interfaz de usuario para mostrar la puntuación de los jugadores.
Esto es para lo que estamos trabajando en toda la serie:
... y esto es lo que tendremos al final de esta parte:
Además de modificar las clases existentes, agregaremos dos nuevas:
BlackHoleControl
: No hace falta decir que esto manejará el comportamiento de nuestros agujeros negros..Hud
: Aquí almacenaremos y mostraremos la puntuación de los jugadores, las vidas y otros elementos de la interfaz de usuario.Empecemos por los agujeros negros..
El agujero negro es uno de los enemigos más interesantes en Geometry Wars. En MonkeyBlaster, nuestro clon, es especialmente bueno una vez que agregamos efectos de partículas y la cuadrícula de deformación en los próximos dos capítulos..
Los agujeros negros tirarán de la nave del jugador, los enemigos cercanos y (después del siguiente tutorial) las partículas, pero repelerán las balas..
Hay muchas funciones posibles que podemos usar para la atracción o la repulsión. Lo más simple es usar una fuerza constante, de modo que el agujero negro se tire con la misma fuerza independientemente de la distancia del objeto. Otra opción es hacer que la fuerza aumente linealmente desde cero, a cierta distancia máxima, hasta fuerza total, para objetos directamente sobre el agujero negro. Y si quisiéramos modelar la gravedad de manera más realista, podemos usar el cuadrado inverso de la distancia, lo que significa que la fuerza de la gravedad es proporcional a 1 / (distancia * distancia)
.
En realidad, usaremos cada una de estas tres funciones para manejar diferentes objetos. Las balas serán repelidas con una fuerza constante, los enemigos y la nave del jugador serán atraídos con una fuerza lineal, y las partículas usarán una función de cuadrado inverso..
Comenzaremos por engendrar nuestros agujeros negros. Para lograrlo necesitamos otra varibale en MonkeyBlasterMain
:
privado spawnCooldownBlackHole;
Luego necesitamos declarar un nodo para los agujeros negros; llamémoslo blackHoleNode
. Puedes declararlo e inicializarlo igual que hicimos nodo enemigo
en el tutorial anterior.
También crearemos un nuevo método., spawnBlackHoles
, que llamamos justo después engendro enemigos
en simpleUpdate (float tpf)
. El desove real es bastante similar al de los enemigos desovadores:
vacío vacío spawnBlackHoles () si (blackHoleNode.getQuantity () < 2) if (System.currentTimeMillis() - spawnCooldownBlackHole > 10f) spawnCooldownBlackHole = System.currentTimeMillis (); if (new Random (). nextInt (1000) == 0) createBlackHole ();
La creación del agujero negro sigue nuestro procedimiento estándar también:
private void createBlackHole () Spatial blackHole = getSpatial ("Black Hole"); blackHole.setLocalTranslation (getSpawnPosition ()); blackHole.addControl (nuevo BlackHoleControl ()); blackHole.setUserData ("activo", falso); blackHoleNode.attachChild (blackHole);
Una vez más, cargamos el espacio, establecemos su posición, agregamos un control, lo configuramos como no activo y finalmente lo adjuntamos al nodo apropiado. Cuando echas un vistazo a BlackHoleControl
, Notarás que tampoco es muy diferente.
Implementaremos la atracción y repulsión más adelante, en MonkeyBlasterMain
, Pero hay una cosa que debemos abordar ahora. Dado que el agujero negro es un enemigo fuerte, no queremos que caiga fácilmente. Por lo tanto, añadimos una variable., puntos de golpe
, al BlackHoleControl
, y establecer su valor inicial a 10
para que muera despues de diez golpes.
la clase pública BlackHoleControl extiende AbstractControl private long spawnTime; puntos de acceso int privados; público BlackHoleControl () spawnTime = System.currentTimeMillis (); puntos de golpe = 10; @ Anular el control void protegidoUpdate (float tpf) if ((Boolean) spatial.getUserData ("active")) // usaremos este punto más adelante ... else else // // manejamos el estado "activo" = 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 ("Black Hole"); pic.getMaterial (). setColor ("Color", color); @ Anular la protección de void controladoRender (RenderManager rm, ViewPort vp) public void wasShot () hitpoints--; public boolean isDead () return hitpoints <= 0;
Ya casi terminamos con el código básico para los agujeros negros. Antes de poner en práctica la gravedad, tenemos que cuidar las colisiones..
Cuando el jugador o un enemigo se acerca demasiado al agujero negro, morirá. Pero cuando una bala logra golpearlo, el agujero negro perderá un punto de golpe..
Echa un vistazo al siguiente código. Pertenece a manejarColisiones ()
. Es básicamente el mismo que para todas las otras colisiones:
// ¿Algo choca con un agujero negro? para (i = 0; iBueno, ahora puedes matar el agujero negro, pero esa no es la única vez que debería desaparecer. Cada vez que el jugador muere, todos los enemigos desaparecen y el agujero negro debería desaparecer. Para manejar esto, simplemente agregue la siguiente línea a nuestro
killPlayer ()
método:blackHoleNode.detachAllChildren ();Ahora es el momento de implementar las cosas interesantes. Crearemos otro método.,
handleGravity (float tpf)
. Solo llámalo con los otros métodos ensimplueUpdate (float tpf)
.En este método, verificamos todas las entidades (jugadores, balas y enemigos) para ver si están cerca de un agujero negro, digamos dentro de 250 píxeles, y, si lo están, aplicamos el efecto apropiado:
privado void handleGravity (float tpf) for (int i = 0; iPara verificar si dos entidades están a una cierta distancia entre sí, creamos un método llamado
está cerca()
que compara las ubicaciones de los dos spatials:booleano privado esNearby (Spatial a, Spatial b, float distance) Vector3f pos1 = a.getLocalTranslation (); Vector3f pos2 = b.getLocalTranslation (); devolver pos1.distanceSquared (pos2) <= distance * distance;Ahora que hemos comprobado cada entidad, si está activa y dentro de la distancia especificada de un agujero negro, finalmente podemos aplicar el efecto de la gravedad. Para hacer eso, haremos uso de los controles: creamos un método en cada control, llamado
applyGravity (Vector3f gravity)
.Echemos un vistazo a cada uno de ellos:
Control de jugador
:public void applyGravity (Vector3f gravity) spatial.move (gravity);
BulletControl
:public void applyGravity (Vector3f gravity) direction.addLocal (gravity);
SeekerControl
yWandererControl
:public void applyGravity (Vector3f gravity) velocity.addLocal (gravedad);Y ahora volvemos a la clase principal.,
MonkeyBlasterMain
. Primero te daré el método y te explicaré los pasos a continuación:private void applyGravity (BlackHole espacial, objetivo espacial, tpf flotante) Vector3f diferencia = blackHole.getLocalTranslation (). restar (target.getLocalTranslation ()); Vector3f gravedad = diferencia.normalizar (). MultLocal (tpf); distancia de flotación = diferencia.longitud (); if (target.getName (). es igual a ("Player")) gravity.multLocal (250f / distance); target.getControl (PlayerControl.class) .applyGravity (gravity.mult (80f)); else if (target.getName (). es igual a ("Bullet")) gravity.multLocal (250f / distance); target.getControl (BulletControl.class) .applyGravity (gravity.mult (-0.8f)); else if (target.getName (). es igual a ("Seeker")) target.getControl (SeekerControl.class) .applyGravity (gravity.mult (150000)); else if (target.getName (). es igual a ("Wanderer")) target.getControl (WandererControl.class) .applyGravity (gravity.mult (150000));Lo primero que hacemos es calcular el
Vector
entre el agujero negro y el objetivo. A continuación, calculamos la fuerza gravitacional. Lo importante a tener en cuenta es que, una vez más, multiplicamos la fuerza por el tiempo transcurrido desde la última actualización.,tpf
, con el fin de lograr el mismo efecto con cada velocidad de fotogramas. Finalmente, calculamos la distancia entre el objetivo y el agujero negro..Para cada tipo de objetivo, debemos aplicar la fuerza de una manera ligeramente diferente. Para el jugador y para las balas, la fuerza se hace más fuerte cuanto más cerca están del agujero negro:
gravity.multLocal (250f / distance);Las balas necesitan ser repelidas; por eso multiplicamos su fuerza gravitatoria por un número negativo.
Los buscadores y los vagabundos simplemente obtienen una fuerza aplicada que es siempre la misma, independientemente de su distancia del agujero negro.
Ahora hemos terminado con la implementación de los agujeros negros. Agregaremos algunos efectos geniales en los próximos capítulos, pero por ahora puedes probarlo.!
Propina: Tenga en cuenta que esto es tu juego; ¡Siéntase libre de modificar cualquier parámetro que desee! Puedes cambiar el área de efecto para el agujero negro, la velocidad de los enemigos o el jugador ... Estas cosas tienen un efecto tremendo en el juego. A veces vale la pena jugar un poco con los valores..
La pantalla frontal
Hay cierta información que debe ser rastreada y mostrada al jugador. Para eso está el HUD (Head-Up Display). Queremos rastrear las vidas de los jugadores, el multiplicador de puntuación actual y, por supuesto, la puntuación en sí misma, y mostrarle todo esto al jugador..
Cuando el jugador obtenga 2,000 puntos (o 4,000, o 6,000, o ...) el jugador obtendrá otra vida. Además, queremos guardar la puntuación después de cada juego y compararla con la puntuación más alta actual. El multiplicador aumenta cada vez que el jugador mata a un enemigo y salta a uno cuando el jugador no mata nada en algún momento..
Vamos a crear una nueva clase para todo eso, llamada
Hud
. EnHud
Tenemos algunas cosas para inicializar desde el principio:clase pública Hud AssetManager privado assetManager; nodo privado guiNode; private int screenWidth, screenHeight; final privado int fontSize = 30; private final int multiplierExpiryTime = 2000; final privado int maxMultiplier = 25; vida pública int puntaje de int público; multiplicador de int público; privado multiplicadorActivationTime; private int scoreForExtraLife; privado BitmapFont guiFont; privado BitmapText livesText; privado BitmapText scoreText; BitmapText privado multiplicadorText; Nodo privado gameOverNode; public Hud (AssetManager assetManager, Node guiNode, int screenWidth, int screenHeight) this.assetManager = assetManager; this.guiNode = guiNode; this.screenWidth = screenWidth; this.screenHeight = screenHeight; setupText ();Esas son muchas variables, pero la mayoría de ellas son bastante autoexplicativas. Necesitamos tener una referencia a la
Gestor de activos
para cargar texto, a laguiNodo
Para añadirlo a la escena, y así sucesivamente..A continuación, hay algunas variables que debemos seguir continuamente, como la
multiplicador
, Su tiempo de expiración, el máximo multiplicador posible y la vida del jugador..Y finalmente tenemos algunos.
BitmapText
Objetos, que almacenan el texto real y lo muestran en la pantalla. Este texto está configurado en el método.setupText ()
, que se llama al final del constructor.private void setupText () guiFont = assetManager.loadFont ("Interface / Fonts / Default.fnt"); livesText = new BitmapText (guiFont, false); livesText.setLocalTranslation (30, screenHeight-30,0); livesText.setSize (fontSize); livesText.setText ("Vidas:" + vidas); guiNode.attachChild (livesText); scoreText = new BitmapText (guiFont, true); scoreText.setLocalTranslation (screenWidth - 200, screenHeight-30,0); scoreText.setSize (fontSize); scoreText.setText ("Score:" + score); guiNode.attachChild (scoreText); multiplierText = new BitmapText (guiFont, true); multipliqueText.setLocalTranslation (screenWidth-200, screenHeight-100,0); multiplierText.setSize (fontSize); multiplierText.setText ("Multiplicador:" + vidas); guiNode.attachChild (multiplierText);Para cargar texto, primero debemos cargar la fuente. En nuestro ejemplo, usamos una fuente predeterminada que viene con jMonkeyEngine.
Propina: Por supuesto, puede crear sus propias fuentes, colocarlas en algún lugar de la bienes directorio-preferiblemente activos / interfaz-y cargarlos. Si desea obtener más información, consulte este tutorial sobre cómo cargar fuentes en jME.A continuación, necesitaremos un método para restablecer todos los valores para que podamos comenzar de nuevo si el jugador muere muchas veces:
restablecimiento de vacío público () puntuación = 0; multiplicador = 1; vidas = 4; multiplierActivationTime = System.currentTimeMillis (); scoreForExtraLife = 2000; updateHUD ();Restablecer los valores es simple, pero también debemos aplicar los cambios de las variables al HUD. Lo hacemos en un método separado:
private void updateHUD () livesText.setText ("Lives:" + lives); scoreText.setText ("Score:" + score); multiplierText.setText ("Multiplicador:" + multiplicador);Durante la batalla, el jugador gana puntos y pierde vidas. Llamaremos a esos métodos desde
MonkeyBlasterMain
:public void addPoints (int basePoints) score + = basePoints * multiplicador; if (score> = scoreForExtraLife) scoreForExtraLife + = 2000; vidas ++; IncreaseMultiplier (); updateHUD (); private void IncreaseMultiplier () multiplierActivationTime = System.currentTimeMillis (); si (multiplicador < maxMultiplier) multiplier++; public boolean removeLife() if (lives == 0) return false; lives--; updateHUD(); return true;Los conceptos notables en esos métodos son:
- Cada vez que agregamos puntos, verificamos si ya hemos alcanzado la puntuación necesaria para obtener una vida extra.
- Cuando agregamos puntos, también necesitamos aumentar el multiplicador llamando a un método separado.
- Cada vez que aumentamos el multiplicador, debemos ser conscientes del máximo multiplicador posible y no ir más allá de eso..
- Cada vez que el jugador golpea a un enemigo, necesitamos restablecer el
multiplierActivationTime
.- Cuando al jugador no le queda vida para ser eliminado, volvemos.
falso
Para que la clase principal pueda actuar en consecuencia..Hay dos cosas que nos quedan por manejar.
Primero, necesitamos reiniciar el multiplicador si el jugador no mata nada por un tiempo. Implementaremos un
actualizar()
Método que comprueba si es hora de hacer esto:public void update () if (multiplier> 1) if (System.currentTimeMillis () - multiplierActivationTime> multiplierExpiryTime) multiplicador = 1; multiplierActivationTime = System.currentTimeMillis (); updateHUD ();Lo último que tenemos que cuidar es terminar el juego. Cuando el jugador ha agotado todas sus vidas, el juego ha terminado y la puntuación final debe mostrarse en el centro de la pantalla. También debemos verificar si la puntuación más alta actual es más baja que la puntuación actual del jugador y, de ser así, guardar la puntuación actual como la nueva puntuación más alta. (Tenga en cuenta que necesita crear un archivo puntuación más alta.txt primero, o no podrás cargar una puntuación.)
Así es como terminamos el juego en
Hud
:public void endGame () // init gameOverNode gameOverNode = new Node (); gameOverNode.setLocalTranslation (screenWidth / 2 - 180, screenHeight / 2 + 100,0); guiNode.attachChild (gameOverNode); // check highscore int highscore = loadHighscore (); if (score> highscore) saveHighscore (); // init y muestra el texto BitmapText gameOverText = new BitmapText (guiFont, false); gameOverText.setLocalTranslation (0,0,0); gameOverText.setSize (fontSize); gameOverText.setText ("Game Over"); gameOverNode.attachChild (gameOverText); BitmapText yourScoreText = new BitmapText (guiFont, false); yourScoreText.setLocalTranslation (0, -50,0); yourScoreText.setSize (fontSize); yourScoreText.setText ("Your Score:" + score); gameOverNode.attachChild (yourScoreText); BitmapText highscoreText = new BitmapText (guiFont, false); highscoreText.setLocalTranslation (0, -100,0); highscoreText.setSize (fontSize); highscoreText.setText ("Highscore:" + highscore); gameOverNode.attachChild (highscoreText);Finalmente, necesitamos dos últimos métodos:
loadHighscore ()
yguardarPuntuación más alta ()
:private int loadHighscore () try FileReader fileReader = new FileReader (nuevo archivo ("highscore.txt")); BufferedReader reader = new BufferedReader (fileReader); String line = reader.readLine (); devuelve Integer.valueOf (línea); catch (FileNotFoundException e) e.printStackTrace (); catch (IOException e) e.printStackTrace (); return 0; private void saveHighscore () try FileWriter writer = new FileWriter (nuevo archivo ("highscore.txt"), false); writer.write (score + System.getProperty ("line.separator")); writer.close (); catch (IOException e) e.printStackTrace ();Propina: Como habrán notado, no utilicé elgestor de activos
Para cargar y guardar el texto. Lo usamos para cargar todos los sonidos y gráficos, y el apropiado La forma jME de cargar y guardar textos en realidad está usando elgestor de activos
para ello, pero como no admite la carga de archivos de texto por su cuenta, deberíamos registrar unTextLoader
con elgestor de activos
. Puede hacerlo si lo desea, pero en este tutorial me limito a la forma predeterminada de Java de cargar y guardar texto, por simplicidad.Ahora tenemos una gran clase que manejará todos nuestros problemas relacionados con HUD. Lo único que tenemos que hacer ahora es agregarlo al juego..
Necesitamos declarar el objeto al inicio:
Hud Hud privado;... inicializarlo en
simpleInitApp ()
:hud = new Hud (assetManager, guiNode, settings.getWidth (), settings.getHeight ()); hud.reset ();... actualizar el HUD en
simpleUpdate (float tpf)
(independientemente de si el jugador está vivo):hud.update ();… Agrega puntos cuando el jugador golpea a los enemigos (en
checkCollisions ()
):// agregar puntos dependiendo del tipo de enemigo if (enemyNode.getChild (i) .getName (). es igual a ("Buscador")) hud.addPoints (2); else if (enemyNode.getChild (i) .getName (). es igual a ("Wanderer")) hud.addPoints (1);Cuidado! Necesitas sumar los puntos. antes de si separa a los enemigos de la escena, o tendrá problemas conenemyNode.getChild (i)
.... y quitar vidas cuando el jugador muere (en
killPlayer ()
):if (! hud.removeLife ()) hud.endGame (); gameOver = true;Es posible que hayas notado que también hemos introducido una nueva variable.,
juego terminado
. Lo pondremos afalso
al principio:privado booleano gameOver = falso;El jugador no debería aparecer más una vez que finalice el juego, así que agregamos esta condición a
simpleUpdate (float tpf)
else if (System.currentTimeMillis () - (Long) player.getUserData ("dieTime")> 4000f &&! gameOver)¡Ahora puedes iniciar el juego y comprobar si te has perdido algo! Y tu juego tiene un nuevo objetivo: superar la puntuación más alta. Les deseo buena suerte!
Cursor personalizado
Ya que tenemos un juego en 2D, hay una cosa más que agregar para perfeccionar nuestro HUD: un cursor de mouse personalizado.
No es nada especial; solo inserta esta linea ensimpleInitApp ()
:inputManager.setMouseCursor ((JmeCursor) assetManager.loadAsset ("Textures / Pointer.ico"));
Conclusión
El juego ahora está completamente terminado. En las dos partes restantes de esta serie, agregaremos algunos efectos gráficos geniales. Esto hará que el juego sea un poco más difícil, ya que los enemigos pueden no ser tan fáciles de detectar.!