Esta es la última parte de mi serie de 4 partes donde aprendo cómo construir un juego de plataformas simple con el motor Flame. Ya sabemos cómo agregar un personaje de jugador animado al que llamé The Boy, cómo crear un nivel de juego desplazable usando el editor de mosaicos y cómo agregar gravedad y saltos con la ayuda de la detección de colisiones (partes 1 , 2 , 3 ).
En esta parte, agregaremos monedas, que el personaje podría recolectar, HUD para mostrar cuántas monedas tiene el jugador y una pantalla de victoria, que mostraremos una vez que se hayan recolectado todas las monedas.
Necesitamos decirle al juego dónde generar monedas (o cualquier otro objeto del juego). Como habrás adivinado, vamos a usar el editor de mosaicos para agregar otra capa de objetos, de manera similar a como agregamos plataformas, pero con dos diferencias:
Con las plataformas, incorporamos imágenes de sprites en los datos de nivel. Este método no es muy adecuado para las monedas, porque queremos eliminar el objeto una vez que el jugador lo recoge. Por lo tanto, solo agregaremos puntos de generación para saber dónde deben aparecer las monedas en el juego, y el renderizado se realizará utilizando los componentes de Flame.
Para las plataformas, usamos rectángulos de cualquier tamaño, pero para las monedas, agregaremos puntos de generación para que tengan el tamaño de 1 mosaico. Sin embargo, si desea agregar una línea de monedas una tras otra (hola, Mario), puede hacerlo fácilmente modificando el código del juego y considerando el tamaño de un punto de generación al agregar objetos de monedas. Pero para los propósitos de esta serie, asumimos que los puntos de generación de monedas son 1x1.
Abra nuestro nivel en el editor de mosaicos y cree una nueva capa de objetos llamada Monedas. Luego, usando la herramienta Rectangular, agregue varios puntos de generación en el mapa para que podamos agregar componentes de monedas usando el motor del juego. Mi nivel ahora se ve así:
Debo agregar que nuestro juego es bastante simple y sabemos que estos rectángulos vacíos se convertirán en monedas en el tiempo de ejecución del juego. Pero si queremos agregar más tipos de objetos, será difícil distinguirlos. Afortunadamente, Tiled tiene una herramienta para eso, llamada "Insertar mosaico", que puede agregar señales visuales para cada objeto, pero estas imágenes no se renderizarán en el juego.
Muy bien, guarde el nivel y regrese al IDE. Agreguemos nuestra clase Coin
a la carpeta /objects/
.
class Coin extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { late final SpriteAnimation spinAnimation; late final SpriteAnimation collectAnimation; Coin(Vector2 position) : super(position: position, size: Vector2.all(48)); @override Future<void> onLoad() async { spinAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.COIN), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(16), stepTime: 0.12, ), ); collectAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.COIN), SpriteAnimationData.range( start: 4, end: 7, amount: 8, textureSize: Vector2.all(16), stepTimes: List.filled(4, 0.12), loop: false ), ); animation = spinAnimation; final hitbox = RectangleHitbox() ..collisionType = CollisionType.passive; add(hitbox); return super.onLoad(); } }
Tenemos 2 animaciones diferentes para girar y recolectar y un RectangleHitbox
, para verificar las colisiones con el jugador más adelante.
Luego, regrese a game.dart
y modifique el método spawnObjects
para generar nuestras monedas:
final coins = tileMap.getLayer<ObjectGroup>("Coins"); for (final coin in coins!.objects) { add(Coin(Vector2(coin.x, coin.y))); }
Ejecute el juego y vea las monedas añadidas:
Regrese a coin.dart
y agregue el método collect
:
void collect() { animation = collectAnimation; collectAnimation.onComplete = () => { removeFromParent() }; }
Cuando se llame a este método, cambiaremos la animación giratoria a la de recolección y, una vez que haya terminado, eliminaremos este componente del juego.
Luego, vaya a la clase theboy.dart
y anule el método onCollisionStart
:
@override void onCollisionStart(Set<Vector2> intersectionPoints, PositionComponent other) { if (other is Coin) { other.collect(); } super.onCollisionStart(intersectionPoints, other); }
La razón por la que usamos onCollisionStart
en lugar de onCollision
es que queremos que la devolución de llamada de colisión se active solo una vez.
Las monedas ahora están desapareciendo al chocar con The Boy. Agreguemos la interfaz de usuario para rastrear la cantidad de monedas recolectadas.
HUD, o pantalla de visualización frontal, es simplemente una barra de estado que muestra cualquier información sobre el juego: puntos de golpe, munición, etc. Mostraremos un ícono de moneda para cada moneda recolectada.
En aras de la simplicidad, voy a almacenar el número de monedas en una variable, pero para interfaces más complejas, considere usar un paquete flame_bloc , que le permite actualizar y observar el estado del juego de manera conveniente.
Agregue una nueva clase que contendrá lógica HUD: lib/hud.dart
class Hud extends PositionComponent with HasGameRef<PlatformerGame> { Hud() { positionType = PositionType.viewport; } void onCoinsNumberUpdated(int total) { final coin = SpriteComponent.fromImage( game.images.fromCache(Assets.HUD), position: Vector2((50 * total).toDouble(), 50), size: Vector2.all(48)); add(coin); } }
Dos cosas interesantes aquí:
positionType
en PositionType.viewport
para pegar nuestro HUD en la esquina de la pantalla. Si no lo hacemos, debido al movimiento de la cámara, HUD se moverá con el nivel.onCoinsNumberUpdated
cada vez que el jugador recolecte la moneda. Utiliza el parámetro total
para calcular el desplazamiento del siguiente icono de moneda y luego agrega un nuevo objeto de moneda a la posición calculada.
Luego, regrese al archivo game.dart
y agregue nuevas variables de clase:
int _coins = 0; // Keeps track of collected coins late final Hud hud; // Reference to the HUD, to update it when the player collects a coin
Luego agregue el componente Hud
en la parte inferior del método onLoad
:
hud = Hud(); add(hud);
Y agregue un nuevo método:
void onCoinCollected() { _coins++; hud.onCoinsNumberUpdated(_coins); }
Finalmente, llámelo desde el método de collect
de Coin
:
void collect() { game.onCoinCollected(); animation = collectAnimation; collectAnimation.onComplete = () => { removeFromParent() }; }
¡Brillante, nuestro HUD ahora muestra cuántas monedas hemos recolectado!
Lo último que quiero agregar es la pantalla Win, que se mostrará una vez que el jugador recolecte todas las monedas.
Agregue una nueva const a la clase PlatformerGame
:
late int _totalCoins;
Y asígnale el número de monedas que tenemos en el nivel. Agregue esta línea al final del método spawnObjects
:
_totalCoins = coins.objects.length;
Y agregue esto al final del método onCoinCollected
.
Tenga en cuenta que es posible que deba agregar import 'package:flutter/material.dart';
a mano.
if (_coins == _totalCoins) { final text = TextComponent( text: 'U WIN!', textRenderer: TextPaint( style: TextStyle( fontSize: 200, fontWeight: FontWeight.bold, color: Colors.white, ), ), anchor: Anchor.center, position: camera.viewport.effectiveSize / 2, )..positionType = PositionType.viewport; add(text); Future.delayed(Duration(milliseconds: 200), () => { pauseEngine() }); }
Aquí, verifico si el contador de monedas es igual a la cantidad de monedas en el nivel, y si es así, agrego una etiqueta Ganar en la parte superior de la pantalla del juego. Luego pausa el juego para detener el movimiento del jugador. También agregué un retraso de 200 ms para que la etiqueta se renderice antes de hacer una pausa.
Debe tener un aspecto como este:
¡Y ese es el juego! Por supuesto, no parece un juego terminado ahora, pero con todo lo que expliqué, debería ser bastante fácil agregar más niveles, enemigos u otros elementos coleccionables.
Esta parte concluye la serie.
El motor de Flame tiene mucho más para ofrecer que no cubrí, incluido el motor de física Forge2D, partículas, efectos, menús de juego, audio, etc. Pero después de completar esta serie, comprendo cómo es el motor y tener una comprensión de cómo construir juegos más complejos.
Flame es una herramienta poderosa pero fácil de usar y aprender. Es modular, lo que permite traer otras cosas geniales como Box2D y se mantiene activamente. Pero una de las mayores ventajas de Flame es que está construido sobre Flutter, lo que significa que brinda soporte multiplataforma con un poco de trabajo adicional. Sin embargo, ser una extensión de Flutter significa que todos los problemas de Flutter también persisten en Flame. Por ejemplo, el error antialiasing de Flutter ha estado abierto durante varios años sin resolución, y es posible que también lo notes en el juego que construimos. Pero en general, es una gran herramienta para crear juegos que vale la pena probar.
El código completo para este tutorial, lo puedes encontrar en mi github
Al final de cada parte, agregaré una lista de increíbles creadores y recursos de los que aprendí.