paint-brush
Desarrollo de juegos MVP con Flutter y Flamepor@leobit
496 lecturas
496 lecturas

Desarrollo de juegos MVP con Flutter y Flame

por Leobit28m2024/06/06
Read on Terminal Reader

Demasiado Largo; Para Leer

Los MVP (productos mínimos viables) se están volviendo cada vez más populares en la industria del juego. Con MVP, puedes crear una aplicación con funcionalidad básica en cortos plazos y con un presupuesto limitado. Con la ayuda de Flame, un motor de juego robusto y de código abierto construido sobre Flutter, puedes crear impresionantes juegos 2D.
featured image - Desarrollo de juegos MVP con Flutter y Flame
Leobit HackerNoon profile picture
0-item

Según una encuesta reciente , sólo 2 de cada 5 startups son rentables. Un MVP (producto mínimo viable) aumenta significativamente las posibilidades de rentabilidad de una startup, ya que permite a dichas empresas recopilar comentarios tempranos de los usuarios sin gastar todo el presupuesto en una aplicación con funcionalidad completa.


Con MVP, puede crear una aplicación con funcionalidad básica en cortos plazos y con un presupuesto limitado, recopilar comentarios de los usuarios y continuar ampliando la solución con su equipo de desarrollo de acuerdo con estos comentarios.


Los MVP son cada vez más populares en la industria del juego. Hoy, exploraremos los entresijos del desarrollo rápido de MVP de juegos con Flutter y Flame, una combinación estelar para crear productos multiplataforma mínimamente viables.

¿Por qué elegir Flutter y Flame?

Flutter, una plataforma segura y repleta de funciones para el desarrollo multiplataforma , ha conquistado el mundo de las aplicaciones móviles y su alcance se extiende mucho más allá de la interfaz de usuario. Con la ayuda de Flame, un motor de juegos robusto y de código abierto creado sobre Flutter , puedes crear impresionantes juegos 2D que se ejecutan sin problemas en dispositivos Android, iOS, web y de escritorio.


Flutter también se ha convertido en una solución popular para crear MVP de juegos debido a sus características integrales que facilitan el rápido desarrollo de soluciones que presentan la funcionalidad básica en diferentes dispositivos. En particular, varios beneficios y funciones integrales de Flutter permiten:


  • Cree un producto con una base de código compartida para diferentes plataformas, incluidas Android e iOS, lo cual es mucho más rápido y rentable que crear aplicaciones nativas separadas para diferentes plataformas. También existen ciertas prácticas para crear aplicaciones web de Flutter con la misma base de código.


  • Cree interfaces de usuario flexibles con widgets prediseñados y animaciones predeterminadas, lo que aumenta la velocidad de desarrollo, uno de los factores más críticos en términos de desarrollo de MVP.


  • Flutter ofrece una funcionalidad de recarga en caliente que permite a los desarrolladores ver los cambios realizados en el código de la aplicación que aparecen en la pantalla simultáneamente, lo que garantiza una mayor flexibilidad en el desarrollo de MVP. Esta característica simplifica mucho la iteración y la experimentación, lo que permite a los desarrolladores probar rápidamente diferentes mecánicas y elementos visuales.


  • El desarrollo de un producto mínimo viable generalmente implica una cantidad mínima de recursos, y Flutter satisface completamente este requisito, ya que la integración predeterminada de Flutter con Firebase reduce significativamente la complejidad de la programación del lado del servidor.


Flutter no consume muchos recursos informáticos y facilita la configuración sencilla de aplicaciones multiplataforma.


La aplicación MVP basada en la combinación Flutter y Flame es una solución confiable pero relativamente simple de desarrollar. Se compila directamente en código nativo, lo que garantiza una jugabilidad fluida y capacidad de respuesta. Puedes desarrollar el MVP de tu juego una vez e implementarlo en diferentes plataformas, ahorrando tiempo y recursos. Flutter y Flame manejan las diferencias de plataforma debajo del capó.


Además, ambas tecnologías cuentan con comunidades vibrantes con documentación extensa, tutoriales y ejemplos de código. Esto significa que nunca te quedarás atrapado en busca de una respuesta o inspiración.

Entonces, ¿qué puede hacer la llama?

Flame proporciona un conjunto completo de herramientas para crear funciones de juegos MVP en plazos cortos y sin gastar demasiado recursos. Este marco de modelado multiplataforma ofrece herramientas para una amplia gama de casos de uso diferentes:


  • Sprites y animaciones: puedes crear rápidamente tus sprites o usarlos desde varias bibliotecas en línea. Flame también admite animación esquelética, lo que le permite realizar animaciones más complejas y realistas.


  • Detección de colisiones: Tiene un sistema de detección de colisiones incorporado que facilita la creación de juegos con tu propia física. Puedes utilizar la detección de colisiones para construir plataformas, paredes, objetos coleccionables y otras cosas con las que los personajes del juego puedan interactuar.


  • Simulaciones de física: Flame también admite simulaciones de física, que te permiten crear mecánicas de juego más dinámicas y atractivas. Puedes utilizar simulaciones físicas para crear cosas como gravedad, saltos y rebotes.


  • Audio y efectos de sonido: puedes usar audio para crear música de fondo, efectos de sonido (como golpes, saltos, etc.) e incluso actuación de voz.


  • Gestión de estado: Flame proporciona una serie de funciones para gestionar el estado de tu juego. Esto incluye cosas como el mantenimiento de puntuaciones, la gestión de niveles y los datos de los jugadores.


  • Dispositivos de entrada: Flame admite varios dispositivos de entrada, como pantallas táctiles, teclados y controladores de juegos. Esto lo convierte en una excelente opción para desarrollar juegos para una variedad de plataformas.


  • Desplazamiento de paralaje: admite desplazamiento de paralaje, que puede agregar profundidad e inmersión a tu mundo de juego. El desplazamiento de paralaje crea la ilusión de profundidad al mover diferentes capas del fondo a diferentes velocidades.


  • Sistemas de partículas: Flame también admite sistemas de partículas, que se pueden utilizar para crear una variedad de efectos visuales, como explosiones, humo y lluvia.


  • Juego multijugador: esto permite a los jugadores competir o colaborar entre sí en tiempo real.


La mayoría de las características mencionadas anteriormente son esenciales para muchos juegos y no deben pasarse por alto ni siquiera en la etapa de desarrollo de MVP. Lo realmente importante es que Flame aumenta significativamente la velocidad de desarrollo de la funcionalidad mencionada anteriormente, lo que le permite lanzar dichas funciones incluso en las primeras versiones del producto.

Vamos a intentarlo

Ahora, en lugar de hablar de Flame, creemos un MVP que contenga características básicas de nuestro propio juego con este marco. Antes de comenzar, debes tener instalado Flutter 3.13 o superior, tu IDE favorito y dispositivo para realizar pruebas.

Una idea

Este juego está inspirado en Chrome Dino. ¡Ah, el famoso Dino Run! Es más que un simple juego de Chrome. Es un querido huevo de Pascua escondido en el modo fuera de línea del navegador.


Nuestro proyecto tendrá la siguiente jugabilidad:

  • Juegas como Jack, un chico aventurero que corre sin cesar por un bosque oscuro.
  • Los controles son mínimos: toca la barra espaciadora o haz clic en la pantalla para saltar.
  • El juego comienza lento pero gradualmente aumenta la velocidad, manteniéndote alerta.
  • Tu objetivo es simple: evitar los obstáculos y correr lo más lejos posible, acumulando puntos en el camino.


Y se llamará “¡Forest Run!”

Prepárate

Crea un proyecto Flutter vacío como lo haces cada vez que inicias una nueva aplicación. Para comenzar, necesitamos establecer dependencias en pubspec.yaml para nuestro proyecto. Al escribir esta publicación, la última versión de Flame es 1.14.0. Además, definamos ahora todas las rutas de los activos, para que no sea necesario volver a este archivo más adelante. Y coloque las imágenes en el directorio activos/imágenes/. Necesitamos ponerlo aquí porque Flame escaneará exactamente esta ruta:


 environment: sdk: '>=3.2.3 <4.0.0' flutter: '>=3.13.0' dependencies: flutter: sdk: flutter flame: ^1.14.0 flutter: uses-material-design: true assets: - assets/images/ - assets/images/character/ - assets/images/background/ - assets/images/forest/ - assets/images/font/


Recuerde colocar todas las imágenes en activos/imágenes/ porque Flame no analizará otros directorios.


Necesitarás muchas imágenes para cualquier juego. ¿Pero qué pasa si no eres bueno diseñando? Afortunadamente, existen muchos recursos de código abierto que puedes utilizar para tus proyectos. Los recursos para este juego fueron tomados de itch.io. Usaremos estos recursos para nuestro proyecto:



Puede visitar esos enlaces o simplemente descargar recursos preparados (ENLACE AL ARCHIVO DE ACTIVOS) para este proyecto y copiar todo el contenido a su proyecto.


Flame tiene una filosofía similar a Flutter. En Flutter, todo es un widget; En Flame, todo es un Componente, incluso el Juego completo. Cada componente puede anular 2 métodos: onLoad() y update(). onLoad() se llama solo una vez cuando Component está montado en ComponentTree y update() se activa en cada fotograma. Muy similar a initState() y build() de StatefulWidget en Flutter.


Ahora, escribamos algo de código. Crea una clase que extienda FlameGame y cargue todos nuestros activos en el caché.


 class ForestRunGame extends FlameGame { @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); } }


A continuación, utilice ForestRunGame en main.dart. Además, puede utilizar métodos de Flame.device para configurar la orientación del dispositivo. Y está GameWidget, que sirve como puente entre widgets y componentes.


 Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.device.fullScreen(); await Flame.device.setLandscape(); runApp(GameWidget(game: ForestRunGame())); }


En este punto ya podemos iniciar el juego, pero solo quedará la pantalla en negro. Entonces, necesitamos agregar nuestros componentes.

Bosque oscuro

Dividiremos el bosque en dos componentes: fondo y primer plano. En primer lugar, nos ocuparemos del fondo. ¿Alguna vez te has desplazado por una página que te pareció dinámica? ¿Como si estuvieras desplazándote por más de una vista a la vez? Se trata de un efecto de paralaje y ocurre cuando los diferentes elementos de una página se mueven a diferentes velocidades, creando un efecto de profundidad 3D.


Como puedes imaginar, usaremos un paralaje como fondo. Extienda ParallaxComponent y configure una pila de imágenes usando ParallaxImageData. Además, existe baseVelocity para la velocidad de las capas iniciales y speedMultiplierDelta, que representa la diferencia relativa de velocidad entre capas. Y lo último, configurar el campo de prioridad (índice z) para moverlo detrás de otros componentes.


 class ForestBackground extends ParallaxComponent<ForestRunGame> { @override Future<void> onLoad() async { priority = -10; parallax = await game.loadParallax( [ ParallaxImageData('background/plx-1.png'), ParallaxImageData('background/plx-2.png'), ParallaxImageData('background/plx-3.png'), ParallaxImageData('background/plx-4.png'), ParallaxImageData('background/plx-5.png'), ], baseVelocity: Vector2.zero(), velocityMultiplierDelta: Vector2(1.4, 1.0), ); } }


El fondo está hecho; ahora es el momento de agregar el primer plano. Extienda el PositionComponent para que podamos alinear el suelo con la parte inferior de la pantalla. También necesitamos el mixin HasGameReference para acceder al caché del juego.


Para crear un terreno, solo necesita alinear la imagen del bloque de terreno varias veces. En Flame, los componentes de la imagen se denominan sprites. Un Sprite es una región de una imagen que se puede representar en el Canvas. Podría representar la imagen completa o ser una de las piezas que componen una hoja de sprites.


Además, recuerde que el eje X está orientado hacia la derecha y el eje Y está orientado hacia abajo. El centro de los ejes está ubicado en la esquina superior izquierda de la pantalla.



 class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { static final blockSize = Vector2(480, 96); late final Sprite groundBlock; late final Queue<SpriteComponent> ground; @override void onLoad() { super.onLoad(); groundBlock = Sprite(game.images.fromCache('forest/ground.png')); ground = Queue(); } @override void onGameResize(Vector2 size) { super.onGameResize(size); final newBlocks = _generateBlocks(); ground.addAll(newBlocks); addAll(newBlocks); y = size.y - blockSize.y; } List<SpriteComponent> _generateBlocks() { final number = 1 + (game.size.x / blockSize.x).ceil() - ground.length; final lastBlock = ground.lastOrNull; final lastX = lastBlock == null ? 0 : lastBlock.x + lastBlock.width; return List.generate( max(number, 0), (i) => SpriteComponent( sprite: groundBlock, size: blockSize, position: Vector2(lastX + blockSize.x * i, y), priority: -5, ), growable: false, ); } }


Y lo último, agregar estos componentes a nuestro ForestRunGame.


 class ForestRunGame extends FlameGame { late final foreground = ForestForeground(); late final background = ForestBackground(); @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); add(foreground); add(background); } }


Ahora, intenta iniciar el juego. En este punto ya tenemos nuestro bosque.


Un extraño en el bosque

El bosque se ve bien, pero por el momento es sólo una imagen. Entonces, vamos a crear a Jack, quien correrá por este bosque bajo la guía del jugador. A diferencia de los árboles y el suelo, el jugador necesita animaciones para sentirse vivo. Usamos Sprite para bloques de tierra, pero usaremos SpriteAnimation para Jack. ¿Cómo funciona esto? Bueno, todo es fácil, sólo necesitas hacer un bucle con una secuencia de sprites. Por ejemplo, nuestra animación de ejecución tiene 8 sprites, que se reemplazan entre sí con un pequeño intervalo de tiempo.



Jack puede correr, saltar y estar inactivo. Para representar sus estados, podemos agregar una enumeración PlayerState. Luego cree un reproductor que extienda SpriteAnimationGroupComponent y pase PlayerState como argumento genérico. Este componente tiene un campo de animaciones donde se almacenan las animaciones para cada PlayerState y un campo actual, que representa el estado actual del reproductor, que necesita ser animado.


 enum PlayerState { jumping, running, idle } class Player extends SpriteAnimationGroupComponent<PlayerState> { @override void onLoad() { super.onLoad(); animations = { PlayerState.running: SpriteAnimation.fromFrameData( game.images.fromCache('character/run.png'), SpriteAnimationData.sequenced( amount: 8, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(23, 34), ), ), PlayerState.idle: SpriteAnimation.fromFrameData( game.images.fromCache('character/idle.png'), SpriteAnimationData.sequenced( amount: 12, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(21, 35), ), ), PlayerState.jumping: SpriteAnimation.spriteList( [ Sprite(game.images.fromCache('character/jump.png')), Sprite(game.images.fromCache('character/land.png')), ], stepTime: 0.4, loop: false, ), }; current = PlayerState.idle; } }


Los estados del jugador están listos. Ahora, necesitamos darle al reproductor un tamaño y una posición en la pantalla. Voy a establecer su tamaño en 69x102 píxeles, pero siéntete libre de cambiarlo como quieras. Para la posición, debemos conocer las coordenadas del terreno. Al agregar el mixin HasGameReference, podemos acceder al campo de primer plano y obtener sus coordenadas. Ahora, anulemos el método onGameResize, que se llama cada vez que se cambia el tamaño de la aplicación, y establezcamos la posición de Jack allí.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { static const startXPosition = 80.0; Player() : super(size: Vector2(69, 102)); double get groundYPosition => game.foreground.y - height + 20; // onLoad() {...} with animation setup @override void onGameResize(Vector2 size) { super.onGameResize(size); x = startXPosition; y = groundYPosition; } }


Como se hizo antes, agrega el jugador a nuestro Juego.


 class ForestRunGame extends FlameGame { // Earlier written code here... late final player = Player(); @override Future<void> onLoad() async { // Earlier written code here... add(player); } }


¡Si comienzas el juego, verás que Jack ya está en el bosque!


¡Corre Jack, corre!

Nuestro juego tiene tres estados: introducción, juego y fin del juego. Entonces, agregaremos la enumeración GameState que los representa. Para hacer que Jack corra, necesitamos variables de velocidad y aceleración. Además, necesitamos calcular la distancia recorrida (se usará más adelante).


Como se mencionó anteriormente, el Componente tiene dos métodos principales: onLoad() y update(). Ya utilizamos el método onLoad varias veces. Ahora, hablemos de la actualización(). Este método tiene un parámetro llamado dt. Representa el tiempo que ha pasado desde la última vez que se llamó a update().


Para calcular la velocidad actual y la distancia recorrida, usaremos el método update() y algunas fórmulas cinemáticas básicas:

  • Distancia = velocidad * tiempo;
  • Velocidad = aceleración * tiempo;


 enum GameState { intro, playing, gameOver } class ForestRunGame extends FlameGame { static const acceleration = 10.0; static const maxSpeed = 2000.0; static const startSpeed = 400.0; GameState state = GameState.intro; double currentSpeed = 0; double traveledDistance = 0; // Earlier written code here... @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } }


En realidad, usaremos un truco para simplificar el desarrollo: Jack se mantendrá firme, pero el bosque se moverá hacia Jack. Entonces, necesitamos que nuestro bosque aplique la velocidad del juego.


Para el fondo de paralaje, solo necesitamos pasar la velocidad del juego. Y automáticamente se encargará del resto.


 class ForestBackground extends ParallaxComponent<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); parallax?.baseVelocity = Vector2(game.currentSpeed / 10, 0); } }


Para el primer plano, necesitamos cambiar cada bloque del terreno. Además, debemos verificar si el primer bloque de la cola salió de la pantalla. Si es así, elimínelo y colóquelo al final de la cola;


 class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); final shift = game.currentSpeed * dt; for (final block in ground) { block.x -= shift; } final firstBlock = ground.first; if (firstBlock.x <= -firstBlock.width) { firstBlock.x = ground.last.x + ground.last.width; ground.remove(firstBlock); ground.add(firstBlock); } } }


Todo está listo, menos un gatillo. Queremos empezar a ejecutar al hacer clic. Nuestros objetivos son tanto dispositivos móviles como de escritorio, por lo que queremos controlar los toques de pantalla y los eventos del teclado.


Por suerte, Flame tiene una manera de hacerlo. Simplemente agregue un mixin para su tipo de entrada. Para el teclado, son KeyboardEvents y TapCallbacks para tocar la pantalla. Esos mixins le brindan la posibilidad de anular métodos relacionados y proporcionar su lógica.


El juego debe comenzar si el usuario presiona la barra espaciadora o toca la pantalla.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { start(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { start(); } void start() { state = GameState.playing; player.current = PlayerState.running; currentSpeed = startSpeed; traveledDistance = 0; } }


Como resultado, Jack puede ejecutarse ahora después de hacer clic.


¡Oh no, un arbusto!

Ahora queremos tener obstáculos en el camino. En nuestro caso, estarán representados como arbustos venenosos. Bush no está animado, por lo que podemos usar SpriteComponent. Además, necesitamos una referencia del juego para acceder a su velocidad. Y una cosa más; No queremos generar arbustos uno por uno, porque este enfoque puede causar una situación en la que Jack simplemente no pueda pasar una línea de arbustos con un salto. Es un número aleatorio del rango, que depende de la velocidad actual del juego.


 class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { late double gap; Bush() : super(size: Vector2(200, 84)); bool get isVisible => x + width > 0; @override Future<void> onLoad() async { x = game.size.x + width; y = -height + 20; gap = _computeRandomGap(); sprite = Sprite(game.images.fromCache('forest/bush.png')); } double _computeRandomGap() { final minGap = width * game.currentSpeed * 100; final maxGap = minGap * 5; return (Random().nextDouble() * (maxGap - minGap + 1)).floor() + minGap; } @override void update(double dt) { super.update(dt); x -= game.currentSpeed * dt; if (!isVisible) { removeFromParent(); } } }


¿Quién planta arbustos? La naturaleza, por supuesto. Creemos una Naturaleza que gestione nuestra generación de arbustos.


 class Nature extends Component with HasGameReference<ForestRunGame> { @override void update(double dt) { super.update(dt); if (game.currentSpeed > 0) { final plant = children.query<Bush>().lastOrNull; if (plant == null || (plant.x + plant.width + plant.gap) < game.size.x) { add(Bush()); } } } }


Ahora, agreguemos Naturaleza a nuestro ForestForeground.


 class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... late final Nature nature; @override void onLoad() { // Earlier written code here... nature = Nature(); add(nature); }


Ahora nuestro bosque tiene arbustos. Pero espera, Jack simplemente está atravesándolos. ¿Por qué está pasando esto? Es porque aún no hemos implementado los golpes.


Aquí Hitbox nos ayudará. Hitbox es otro componente del zoológico de componentes de Flame. Encapsula la detección de colisiones y le brinda la posibilidad de manejarla con una lógica personalizada.


Añade uno para Jack. Recuerde que la posición del componente colocará su esquina izquierda-derecha, no el centro. Y con el tamaño, tú te encargas del resto.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(2, 2), size: Vector2(60, 100), ), ); } }


Y uno para el monte. Aquí, estableceremos el tipo de colisión en pasivo para realizar alguna optimización. De forma predeterminada, el tipo está activo, lo que significa que Flame comprobará si este hitbox colisiona con todos los demás hitbox. Sólo tenemos un jugador y arbustos. Como el jugador ya tiene un tipo de colisión activo y los arbustos no pueden chocar entre sí, podemos configurar el tipo en pasivo.


 class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(30, 30), size: Vector2(150, 54), collisionType: CollisionType.passive, ), ); } }


Está bien, pero no puedo ver si la posición del hitbox se ajustó correctamente. ¿Cómo puedo probarlo?


Bueno, puedes establecer el campo debugMode de Player y Bush en verdadero. Te permitirá ver cómo están posicionados tus hitboxes. El morado define el tamaño del componente y el amarillo indica el hitbox.


Ahora queremos detectar cuándo hay una colisión entre el jugador y el arbusto. Para esto, debe agregar el mixin HasCollisionDetection al juego y luego CollisionCallbacks para los componentes, que deben manejar la colisión.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... }


Por ahora, simplemente pausa el juego cuando se detecte la colisión.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.paused = true; } }


Saltar o morir

Si Jack quiere evitar esos arbustos, necesita saltar. Vamos a enseñarle. Para esta característica, necesitamos la constante de gravedad y la velocidad vertical inicial del salto de Jack. Esos valores fueron elegidos a ojo, así que siéntete libre de ajustarlos.


Entonces, ¿cómo funciona la gravedad? Básicamente, es la misma aceleración pero orientada al suelo. Entonces, podemos usar las mismas fórmulas para la posición vertical y la velocidad. Así, nuestro salto tendrá 3 pasos:

  1. Se activa el salto y la velocidad vertical de Jack cambia de cero al valor inicial.
  2. Se mueve hacia arriba y la gravedad cambia gradualmente su velocidad. En un momento, Jack dejará de subir y comenzará a bajar.
  3. Cuando Jack toca el suelo, debemos dejar de aplicarle gravedad y restablecer su estado para que vuelva a correr.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { static const gravity = 1400.0; static const initialJumpVelocity = -700.0; double jumpSpeed = 0; // Earlier written code here... void jump() { if (current != PlayerState.jumping) { current = PlayerState.jumping; jumpSpeed = initialJumpVelocity - (game.currentSpeed / 500); } } void reset() { y = groundYPos; jumpSpeed = 0; current = PlayerState.running; } @override void update(double dt) { super.update(dt); if (current == PlayerState.jumping) { y += jumpSpeed * dt; jumpSpeed += gravity * dt; if (y > groundYPos) { reset(); } } else { y = groundYPos; } } }


Y ahora activemos el salto mediante clic desde ForestRunGame.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { onAction(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { onAction(); } void onAction() { switch (state) { case GameState.intro: case GameState.gameOver: start(); break; case GameState.playing: player.jump(); break; } } }


Ahora Jack puede manejar los arbustos.

Juego terminado

Cuando termine el juego, queremos mostrar texto en la pantalla. Text in Flame funciona de manera diferente a Flutter. Primero debes crear una fuente. Debajo del capó, es solo un mapa, donde char es una clave y sprite es un valor. Casi siempre, la fuente del juego es una imagen donde se reúnen todos los símbolos necesarios.


Para este juego, solo necesitamos dígitos y letras en mayúscula. Entonces, creemos nuestra fuente. Para hacerlo, debes pasar la imagen fuente y los glifos. ¿Qué es un glifo? Glyph es una unión de información sobre char, su tamaño y posición en la imagen de origen.


 class StoneText extends TextBoxComponent { static const digits = '123456789'; static const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; StoneText({ required Image source, required super.position, super.text = '', }) : super( textRenderer: SpriteFontRenderer.fromFont( SpriteFont( source: source, size: 32, ascent: 32, glyphs: [ _buildGlyph(char: '0', left: 480, top: 0), for (var i = 0; i < digits.length; i++) _buildGlyph(char: digits[i], left: 32.0 * i, top: 32), for (var i = 0; i < letters.length; i++) _buildGlyph( char: letters[i], left: 32.0 * (i % 16), top: 64.0 + 32 * (i ~/ 16), ), ], ), letterSpacing: 2, ), ); static Glyph _buildGlyph({ required String char, required double left, required double top, }) => Glyph(char, left: left, top: top, height: 32, width: 32); }


Ahora podemos crear el panel Game Over y usarlo en el juego.


 class GameOverPanel extends PositionComponent with HasGameReference<ForestRunGame> { bool visible = false; @override Future<void> onLoad() async { final source = game.images.fromCache('font/keypound.png'); add(StoneText(text: 'GAME', source: source, position: Vector2(-144, -16))); add(StoneText(text: 'OVER', source: source, position: Vector2(16, -16))); } @override void renderTree(Canvas canvas) { if (visible) { super.renderTree(canvas); } } @override void onGameResize(Vector2 size) { super.onGameResize(size); x = size.x / 2; y = size.y / 2; } }


Ahora podemos mostrar nuestro panel, cuando Jack se va por las ramas. También modifiquemos el método start(), para que podamos reiniciar el juego al hacer clic. Además, debemos limpiar todos los arbustos del bosque.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... late final gameOverPanel = GameOverPanel(); @override Future<void> onLoad() async { // Earlier written code here... add(gameOverPanel); } void gameOver() { paused = true; gameOverPanel.visible = true; state = GameState.gameOver; currentSpeed = 0; } void start() { paused = false; state = GameState.playing; currentSpeed = startSpeed; traveledDistance = 0; player.reset(); foreground.nature.removeAll(foreground.nature.children); gameOverPanel.visible = false; } }


Y ahora necesitamos actualizar la devolución de llamada de colisión en el reproductor.


 class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.gameOver(); } }


Ahora puedes ver Game Over cuando Jack choca contra un arbusto. Y reinicia el juego con solo hacer clic nuevamente.


¿Qué pasa con mi puntuación?

Y el cálculo final de la puntuación táctil.


 class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { late final StoneText scoreText; late final StoneText highText; late final StoneText highScoreText; int _score = 0; int _highScore = 0; // Earlier written code here... @override Future<void> onLoad() async { // Earlier written code here... final font = images.fromCache('font/keypound.png'); scoreText = StoneText(source: font, position: Vector2(20, 20)); highText = StoneText(text: 'HI', source: font, position: Vector2(256, 20)); highScoreText = StoneText( text: '00000', source: font, position: Vector2(332, 20), ); add(scoreText); add(highScoreText); add(highText); setScore(0); } void start() { // Earlier written code here... if (_score > _highScore) { _highScore = _score; highScoreText.text = _highScore.toString().padLeft(5, '0'); } _score = 0; } @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; setScore(traveledDistance ~/ 50); if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } void setScore(int score) { _score = score; scoreText.text = _score.toString().padLeft(5, '0'); } }


¡Eso es todo amigos!


Ahora, pruébalo e intenta superar mi puntuación más alta. ¡Son 2537 puntos!

Conclusiones

Fue mucho, pero lo logramos. Hemos creado un producto mínimo viable para un juego móvil con física, animaciones, cálculo de puntuación y mucho más. Siempre hay margen de mejora y, como cualquier otro MVP, se puede esperar que nuestro producto incluya nuevas características, mecánicas y modos de juego en el futuro.


Además, hay un paquete flame_audio, que puedes usar para agregar música de fondo, sonidos de saltos o golpes, etc.


Por ahora, nuestro principal objetivo era crear la funcionalidad básica del producto en cortos plazos y con una asignación de recursos limitada. La combinación de Flutter y Flame demostró ser perfecta para crear un MVP del juego que se puede utilizar para recopilar comentarios de los usuarios y seguir actualizando la aplicación en el futuro.


Puedes consultar los resultados de nuestro esfuerzo aquí .


Con sus potentes funciones, facilidad de uso y una comunidad próspera, Flutter y Flame son una opción convincente para los aspirantes a desarrolladores de juegos. Ya seas un profesional experimentado o recién estés comenzando, esta combinación ofrece las herramientas y el potencial para hacer realidad tus ideas de juego. ¡Así que aprovecha tu creatividad, sumérgete en el mundo de Flutter y Flame y comienza a crear la próxima sensación de juego móvil!


Esperamos que este artículo le haya resultado agradable e informativo. Si desea obtener más información sobre el desarrollo de software o desea hablar sobre su propio proyecto MVP, ¡no dude en explorar Leobit o comunicarse con nuestro equipo técnico!