Flutter Flame Tutorial para Desarrollar tu Primer Juego en 2D
Índice de contenido
- Crear un proyecto en Flutter y agregar Flame
- Por qué elegir Flutter para juegos casuales
- Clase Game y componentes en Flutter Flame
- Clases tipo Game/FlameGame
- Componentes
- Conclusiones
- Dibujar un sprite/imagen
- ¿Qué es un Sprite?
- Conclusiones
- Dibujar un círculo en Flame con Flutter
- Game loop: Bucle de juego
- Función render
- Función update
- Función update (actualizar)
- Dibujar un rectángulo
- Clase Game en Flame Flutter juegos en 2D
- Tipos de clases
- Función update - Actualizar posición de un Sprite
- Evento de entradas de teclado en Flutter Flame
- Clase tipo Game
- Nivel de componente
- Caso práctico
- Reto: Mover sprite al pulsar flechas de direcciones
- Sprites o imágenes para Flame - SpriteComponent
- Sprites
- Referenciar un Sprite
- SpriteComponent: Componentes para renderizar imágenes
- Crear un Sprite Sheet para generar imágenes animadas para juegos 2D con TexturePacker
- Como utilizar un Sprite Sheet en Flutter con Flame 13 - Juegos 2D animados
- Caso práctico
- Cargar un Sprite
- Como utilizar un Sprite Sheet Componente Flutter con Flame
- Animación de sprite con SpriteAnimationComponent
- SpriteAnimationComponent: Pruebas
- Entradas de Teclado FlameGame, nivel de clase tipo Game
- Nivel de la clase Game
- Caso práctico
- Evento tap o click a nivel de los componentes
- Tap a nivel de componente
- Cambiar animaciones de un Sprite por Tap Flutter
- Resolución reto
- Crear un fondo parallax en Flame
- Variar velocidad en las capas
- Crear una clase componente para el parallax
- CameraComponent en Flame para seguir componentes en Flutter y uso de World
- World
- CameraComponent
- Implementar el componente de cámara
- SpriteAnimationTicker en Flame para controlar una animación en Flutter
- Implementar un Joystick
- GamePad en Flutter y Flame - Dualsense - Xbox
- Primeros pasos con el plugin gamepads
- Definir un listeners para detectar teclas presionadas
En esta guía, darás los primeros pasos en la creación de tus primeros juegos en 2D con Flutter y Flame; la tabla de contenido la tienes arriba pero en esencia verás desde la instalación, el entorno y crear tus primeros juegos.
Para llegar a este punto, recuerda que hemos cubierto varios posts en los cuales te enseño como crear una app en Flutter, lo último, son buenas prácticas mediante cual emplear, si una clase de tipo Stateful/Stateless Widget o un método/función en Flutter.
Crear un proyecto en Flutter y agregar Flame
El desarrollo de juegos en 2D con Flutter y Flame puede ser muy emocionante y gratificante, la verdad es toda una experiencia poder desarrollar juegos móviles tan fácilmente, si eres nuevo en el desarrollo de juegos o quieres iniciar en el desarrollo de juegos y por supuesto, conoces como desarrollar en Flutter, estos tutoriales son para ti!
Flutter es un framework de desarrollo de aplicaciones móviles multiplataforma que utiliza el lenguaje de programación Dart, y Flame es una biblioteca de juegos 2D de alto rendimiento construida encima de Flutter; como veremos en esta serie de tutoriales, ambos, son los mejores amigos y se complementan realmente bien.
Poder utilizar los widgets de Flutter para construir todos esos elementos gráficos necesarios en cualquier juego como textos, botones de acciones y encadenarlos con un proceso en Flame, es realmente sencillo de realizar.
Con Flutter, podemos realizar todo tipo de aplicaciones sobre todo enfocadas al ámbito móvil, pero, también podemos utilizar el mismo proyecto para desarrollar no solamente en Android e iOS, si no, de escritorio para Linux, MacOS y Windows, y también para desarrollo web realizando cambios sutiles en el proyecto a nivel de código. Aunque, Flutter va mucho más allá con Flame.
Flame, es un motor para crear juegos en 2D con Flutter que lo podemos utilizar en un proyecto en Flutter instalando un sencillo paquete mediante un pub; por lo tanto, con esto, podemos crear juegos en 2D con Flame para móvil, escritorio y web.
En esta publicación, vamos a aprender a crear un proyecto en Flutter y agregar la librería base necesaria para desarrollar nuestros juegos en 2D empleando Flutter; por lo tanto, es un capítulo que debes de tomar de referencia cada vez que creamos un nuevo proyecto en posteriores entradas.
Comencemos creando el proyecto que vamos a utilizar para crear la aplicación:
$ flutter create <NameProyect>Una vez creado el proyecto en Flutter, cambiamos a la carpeta del proyecto:
$ cd <NameProyect>Y agregamos la librería de flame:
$ flutter pub add flameFinalmente, abrimos VSC o el editor que utilices para desarrollar en Flutter; para esto, puedes hacer el proceso manual (abrir el proyecto que creamos anteriormente desde VSC), o usar el comando de VSC (en caso de que lo tengas configurado):
$ code .Recuerda que este material forma parte de mi curso completo y libro sobre Flame y Flutter los cuales puedes adquirir por un módico precio en caso de que quieres profundizar más en el desarrollo de juegos en 2D.
Por qué elegir Flutter para juegos casuales
Flutter es una excelente opción para los desarrolladores de juegos. En primer lugar, es gratuito y de código abierto, lo que te brinda un control detallado sobre la lógica de procesamiento y manejo de entradas de tu juego. Esto le permite depurar problemas en su esencia y personalizar el motor según sus necesidades. La apertura de Flutter también se extiende a nuestro ecosistema. Todos los complementos y paquetes de Flutter también están disponibles para su integración sin costo alguno.
En segundo lugar, desarrollar en Flutter es muy productivo. Flutter introdujo una capacidad revolucionaria llamada recarga en caliente que permite a los desarrolladores ver actualizaciones instantáneas de la interfaz de usuario después de realizar cambios en el código, lo que hace que el proceso de desarrollo sea más iterativo y eficiente. Además, Flutter admite el desarrollo de juegos multiplataforma, por lo que puedes crear tu juego para iOS y Android, web y escritorio, todo desde una única base de código compartida. Esto te ahorra tiempo y esfuerzo y permite que tu juego llegue a un público más amplio desde el primer día.
Finalmente, los juegos de Flutter se cargan rápido y, en general, tienen un gran rendimiento, incluso en dispositivos o navegadores de gama baja. Los tamaños de los paquetes pueden ser más pequeños porque el motor Flutter solo agrega unos pocos megabytes a tu juego.
Clase Game y componentes en Flutter Flame
Conoceremos los elementos claves de Flame, su organización, componentes y estructuras claves; este es un capítulo netamente referencia, no te preocupes si no entendiste todos los términos explicados en este apartado, los siguientes capítulos ofrecen un esquema más práctico en la cual, construimos una aplicación paso a paso; cuando se presenten una de estas clases y funciones, puedes volver a este capítulo para repasar lo explicado en el mismo.
Puedes crear un proyecto llamado "pruebasflame" como se muestra en el capítulo anterior.
Un proyecto en Flame, lo podemos dividir dos partes:
- La clase principal, que es la que permite comunicar todos los módulos de la aplicación al igual que, emplear procesos propios de Flame como el sistema de colisiones y entrada (teclado, gestos...).
- Los componentes, que son los elementos de nuestro juego, un fondo, jugador, enemigo, etc.
Para que la idea quede más más fácil de entender, puedes ver la clase tipo Game de Flame como el MaterialApp de Flutter y los componentes de Flame, como cada una de las páginas que conforman la aplicación en Flutter:
Vamos a conocer cada uno de estos elementos de Flame más en detalle.
Clases tipo Game/FlameGame
La clase Game en Flame es un componente esencial para la creación de juegos en 2D con Flutter. Esta clase es la principal de todo el juego y se encarga de la configuración, renderizado y actualización de los elementos del juego; en pocas palabras, es la clase central y la que gobierna toda la aplicación; esta clase viene siendo el equivalente a la de MaterialApp en Flutter; a partir de esta clase podemos usar todo tipo de funcionalidades que forman parte del API de Flame como los tap, drag and drop, colisiones y un largo etc.
En Flame, tenemos dos tipos de clases principales, las clases Game que nos ofrece un esquema más simplificado que la clase FlameGame; por ejemplo, la clase Game solamente permite definir componentes locales a su misma clase, cosa que es impensable cuando tenemos una aplicación de gran tamaño.
En la clase Game, se define el tamaño de la pantalla, se crean los sprites, se establece la lógica del juego y se cargan los recursos necesarios, como imágenes y sonidos. Además, la clase Game también maneja el ciclo de actualización y renderizado del juego.
Al heredar de la clase Game, se puede personalizar la lógica y la apariencia del juego. Con la ayuda de la clase Game en Flame, se puede crear todo tipo de juegos 2D. Es importante mencionar que Flame también ofrece otros componentes, como Sprite y Animation, que ayudan a construir juegos más complejos y dinámicos.
De los componentes, hablaremos a continuación.
Componentes
Una de las características grandiosas que tiene Flame, es que, podemos usar las distintas características de Flame en componentes en las cuales, un componente pueden ser varias cosas como un jugador (player), un enemigo, un fondo, efectos de partículas, textos, joystick, entre otros; los cuales, podemos incorporar más características como el uso de colisiones, actualizaciones, interacción con teclas, drag and drop, tap, etc; como puedes darte cuenta, tiene un enfoque muy similar al de los widgets de Flutter, pero, en esta caso con componentes y basado en juegos.
En esencia, una entidad del juego, como el jugador, está representada por un componente, que es una clase, y gracias a la clase tipo Game de nuestra aplicación que es la entidad global, podemos comunicar los componentes entre sí; por ejemplo, con las entradas o las colisiones, los que nos da, un entorno muy modular y escalable a la aplicación.
Tenemos muchos tipos de componentes, en este libro, veremos algunos como:
- SpriteComponent
- SpriteAnimationComponent
- PositionComponent
- TextComponent
Puedes ver la lista completa en:
https://docs.flame-engine.org/1.5.0/flame/components.html
Conclusiones
En Flame, los componentes son objetos interactivos que forman parte de un juego 2D y pueden ser de diferentes tipos, como sprites, animaciones, efectos visuales, sonidos, entre otros; estos componentes interactivos pueden tener distintos propósitos como un jugador, un enemigo, un consumible, algún objeto que puede hacer daño a algún jugador y en esencia, cualquier cosa en el juego incluyendo el background del juego; los componentes son la pieza central en Flame y son gobernados por otros componentes o por las clases tipo Game que presentamos anteriormente.
Los componentes en Flame se utilizan para representar elementos visuales y funcionales en el juego, como personajes, obstáculos, objetos y elementos del entorno.
Lo estupendo de los componentes es que, cada componente tiene su propia estructura y comportamiento, y se pueden combinar y personalizar para crear juegos únicos. Al agregar componentes a una escena en Flame, se pueden definir cómo interactúan entre sí y cómo se comportan en el juego. Por ejemplo, se pueden definir las propiedades de un sprite, como su velocidad y su dirección, y cómo debe reaccionar al colisionar con otro objeto.
Los componentes en Flame proporcionan una organización clara del código y una gestión eficiente de recursos, lo que facilita la creación y el mantenimiento de juegos complejos; los componentes al ser clases, pueden heredad de otras clases, o implementar y poder reutilizadlas fácilmente. En resumen, los componentes en Flame son elementos esenciales en la creación de juegos 2D en Flutter, ya que permiten la creación de objetos interactivos y personalizados que componen el juego en sí.
Recuerda que este material forma parte de mi libro y curso completo sobre Flutter Flame.
Dibujar un sprite/imagen
¿Qué es un Sprite?
Antes de comenzar a desarrollar en Flame, vamos a introducir el concepto de sprite, que es fundamental en el desarrollo de cualquier juego en 2D; los juegos en 2D tiene la característica de que muchos de ellos son basados en imágenes, a diferencia de los juegos en 3D, en los cuales vemos modelos completos creados con programas como Blender, en el desarrollo 2D esto no necesariamente es así; los juegos tipo Plantas vs Zombies es un buen ejemplo de juego en 2D basado en imágenes:
Y es aquí en donde entra el concepto de Sprite; un sprite se refiere a una imagen o conjunto de imágenes que se utilizan para crear personajes y todo tipo de objetos o elementos en nuestro juego; es decir, cada elemento visual de nuestro juego en Flame es un sprite; en Flame, tenemos un conjunto enorme de funciones listas para usar para variar los sprite, crear sprites animados, ocultarlos, realizar operaciones geométricas entre otras características.
Los sprites se usan para establecer la forma, tamaño y posición de los elementos en la pantalla, es la representación visual que tiene un usuario al momento de interactuar con cualquier elemento, por ejemplo, el player con un consumible, con los tiles, una casa, puerta y un largo etc.
Como mencionamos antes, los Sprites también se usan para las animaciones. Los desarrolladores de juegos 2D a menudo crean hojas de sprites que contienen múltiples imágenes de sprites, aunque a veces las generan por separado y las podemos combinar; en Flame, lo ideal es mantener todos los estados en una sola imagen y a partir de la misma, se cargan y definen las animaciones, pero, esto es otro tema.
Antes de comenzar a usar sprites más completos, vamos a crear un sencillo, dibujar una image.
En definitiva, un Sprite no es más que una imagen que puede tener una organización para -por ejemplo- poder crear una animación a partir de la misma.
En el siguiente código, puedes ver la estructura básica de una aplicación en Flame, en la cual, tenemos la entidad global FlameGame y la definición y posterior agregado de un componente dentro de la clase FlameGame:
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
class MySprite extends SpriteComponent {
MySprite() : super(size: Vector2.all(16));
@override
Future<void> onLoad() async {
sprite = await Sprite.load('image.png');
}
}
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
await add(MySprite());
}
}
main() {
runApp(GameWidget(game: MyGame()));
}Para la implementación anterior, estamos cargando una imagen:
sprite = await Sprite.load('image.png');Que debe de existir en el siguiente path:
assets/images/image.png
Y registrar la imagen en la app:
pubspec.yaml
assets:
- assets/images/image.pngY el sprite tendrá un tamaño de 16 píxeles según lo especificado en el constructor de la clase:
size: Vector2.all(16)Luego, la agregamos desde la instancia global:
add(MySprite());Conclusiones
Con esto, logramos pintar una imagen o sprite en Flame, este es el primer paso que debemos de realizar para hacer cualquier cosa, como mencionamos antes, las imágenes o sprite son el corazón en este tipo de juegos y por ende debemos de saber como cargar las mismas, y no solamente cargarlas, si no, definir otras características como el tamaño y posición son fundamentales; ya con esto, vimos otro ejemplo de como usar componentes en Flame, aunque, en esta oportunidad no es un PositionComponent en Flame, si no, un SpriteComponent.
Recuerda que este artículo es una pequeña parte de mi curso y libro completo sobre Flutter y Flame.
Dibujar un círculo en Flame con Flutter
Para empezar a desarrollar juegos 2D con Flutter y Flame, puedes seguir estos tutoriales que traigo a tu disposición en los cuales conoceremos los conceptos básicos para crear juegos en 2D con Flutter.
Antes de entrar en materia de la gestión de imágenes o sprites, la detección de colisiones, eventos taps… Debemos de comenzar de a poco e inicialmente, vamos a querer dibujar algo por pantalla, alguna primitiva como un circulo, cosa que es muy sencilla de hacer; estos ejercicios son fundamentales para entender como desarrollar componentes más completos como sprites, animaciones y por supuesto, como interacturar con todos estas imágenes según alguna acción del usuario.
Dibujar un circulo
Para dibujar un círculo en Flame con Flutter, se puede utilizar la clase Circle del paquete Flame/components.dart. Esta clase permite dibujar un círculo en el canvas en base a coordenadas que serían para definir su centro y tamaño:
Offset(10, 10)Y también el color
BasicPalette.red.paint()En este ejemplo, veremos cómo dibujar un círculo con la clase FlameGame:
lib/main.dart
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
class MyCircle extends PositionComponent {
MyCircle() : super();
@override
Future<void> onLoad() async {}
@override
void render(Canvas canvas) {
canvas.drawCircle(const Offset(10, 10), 10, BasicPalette.red.paint());
// canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20),
// BasicPalette.red.paint());
}
}
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
await add(MyCircle());
}
}
main() {
runApp(GameWidget(game: MyGame()));
}En este otro ejemplo, tenemos la misma aplicación, pero usando la clase Game en su lugar:
lib/main.dart
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
import 'package:flame/game.dart';
void main() async {
runApp(GameWidget(game: MyCircle()));
}
class MyCircle with Game {
@override
Future<void> onLoad() async {
super.onLoad();
// init
}
@override
void render(Canvas canvas) {
canvas.drawCircle(const Offset(10, 10), 10, BasicPalette.red.paint());
// canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20),
// BasicPalette.red.paint());
}
@override
void update(double deltaTime) {}
}Ya sea, el programa con la clase de Game o FlameGame, si ejecutas el script anterior, verás un resultado como el siguiente:
Como puedes concluir, aquellos enfoques que nos permita organizar el código en clases, es el enfoque que permite reutilizar más fácilmente los componentes, al igual que extender los mismos o crear otros, por lo tanto, estos ejemplos refuerzan el motivo por el cual se emplea la clase FlameGame en vez de la de Game; aparte de que, con la clase FlameGame tenemos acceso a otras funcionalidades de la API de Flame.
Como vistes antes, usamos los métodos fundamentales del GameLoop en Flame, que son. la función de render y la de update.
La clase FlameGame mantiene una lista de todos los componentes del juego, y estos pueden agregarse dinámicamente al juego según sea necesario; por ejemplo, podríamos agregar varios componentes SpriteComponent enemigos al juego y luego eliminarlos del juego a medida que el jugador mata a los enemigos. Luego, la clase FlameGame iterará sobre estos componentes diciéndole a cada componente que se actualice y se represente a sí mismo.
La clase de GameWidget representa el constructor que usualmente empleamos para crear la instancia del juego en Flame y establecerla en el árbol de widget en Flutter.
Como recomendación, presiona sobre la clase FlameGame, Game, PositionComponent, SpriteComponent y el resto, para ver el detalle de las mismas, ver que propiedades implementan, como en el caso, de las clases tipo Game:
class PositionComponent extends Component
implements
AnchorProvider,
AngleProvider,
PositionProvider,
ScaleProvider,
CoordinateTransform {
PositionComponent({
Vector2? position,
Vector2? size,
Vector2? scale,
double? angle,
this.nativeAngle = 0,
Anchor? anchor,
super.children,
***O sobre las funciones que puedes implementar y que parámetros puedes enviar y el tipo, como en el caso del Canvas y haz pruebas con las mismas y evalúa el resultado...
Por ejemplo, para dibujar un rectángulo, podemos hacerlo definiendo dos puntos en la pantalla:
canvas.drawRect(Rect.fromPoints(const Offset(10,10), const Offset(500,500)), BasicPalette.purple.paint());O mediante un círculo, pero, en vez de dibujar un círculo con canvas.drawCircle(), dibujamos un rectángulo:
canvas.drawRect(Rect.fromCircle(center: const Offset(100, 100), radius: 50.0),BasicPalette.brown.paint());En resumen, con estos ejemplos, van quedando más claramente como funciona Flame, una estructura en base a componentes, que al igual que los widgets, son un elemento de varios de nuestros juegos, vemos como usar los PositionComponent los cuales son widgets básicos y que se heredan de otros tipos de widgets que presentaremos más adelante; elementos como la posición, tamaño, escala entre otras operaciones geométricas, son fundamentales en cualquier motor de videojuegos y en Flame no es la excepción.
Game loop: Bucle de juego
El módulo de Flame llamado GameLoop o bucle de juego no es más que una simple abstracción del concepto de bucle de juego. Básicamente, la mayoría de los juegos se basan en dos métodos:
- El método de renderizado toma el canvas/lienzo para dibujar el estado actual del juego.
- El método de actualización, que recibe el tiempo (delta) desde la última actualización y permite pasar al siguiente estado, es decir, actualizar el estado del juego.
El Game Loop se puede considerar como el proceso más importantes en el desarrollo de videojuegos; ya que, a partir del mismo es que nosotros podemos implementar nuestra aplicación.
El GameLoop tal cual puedes suponer por su nombre, es un bucle infinito que se encarga de actualizar el estado del juego; esto va desde agregar elementos en pantalla, mover al player o a los enemigos y en pocas palabras, cualquier cambio en pantalla, se realiza desde el GameLoop; en Flame, es básicamente la función de update() la que se ejecuta de manera infinita, y es aquí, en donde se colocan verificaciones de diversos tipos como a escuchadores, etc. Este bucle es el encargado de que el juego se ejecute de la forma adecuada; usando primitivas podemos controlar la velocidad de ejecución del juego para que se ejecute de manera correcta, y en el caso de Flame, para que se ejecute a la misma velocidad en todos los dispositivos; en otras palabras, cuantas actualizaciones por segundo van a ocurrir; esto es importante ya que, el número de veces por segundo que se ejecute esta función, depende de la velocidad de procesamiento, y al tener la aplicación ejecutándose en diversos entornos, es importante realizar este tipo de configuraciones.
En general, el Game Loop se encarga de procesar todo lo que sucede en el juego y actualizar la interfaz gráfica del usuario en consecuencia.
En Flame tenemos un método para inicializar el juego y otro para realizar actualizaciones; Flame sigue estos principios y tenemos un par de funciones que permiten realizar dichas operaciones.
Función render
La función render() recibe un parámetro tipo objeto que hace referencia al lienzo o canvas:
@override
void render(Canvas canvas) {
canvas.drawRect(squarePos, squarePaint);
}Que es al igual que ocurre con otras tecnologías como HTML5, no es más que un lienzo en blanco que dibujar sobre el mismo; aquí, podemos dibujar cualquier cosa, por ejemplo, un círculo:
@override
void render(Canvas canvas) {
canvas.drawCircle(const Offset(10, 10), 10, BasicPalette.red.paint());
}Función update
Los elementos que estén en el juego, necesitan ser redibujados constantemente según el estado actual del juego; para entender esto más claramente, veamos un ejemplo:
Supongamos que un elemento del juego representado por un sprite (una imagen), que en este ejemplo llamaremos cómo "player"; cuando el usuario da un click sobre un botón, entonces el player debe de actualizar su posición; esta actualización, se aplica en el juego mediante una función llamada update().
Al igual que vimos en el ejemplo del círculo en movimiento, es esta función la que se encarga de actualizar la posición del círculo en pantalla.
La función de update() es tal cual indica su nombre, una función de actualización, la cual, recibe un parámetro, llamado "deltatime" (dt) que nos dice la hora que ha transcurrido desde que se dibujó el cuadro anterior. Esta variable debe usarla para hacer que su componente se mueva a la misma velocidad en todos los dispositivos.
Los dispositivos funcionan a diferentes velocidades, dependiendo de la potencia de procesamiento (es decir, según el procesador que tenga el dispositivo, específicamente, la frecuencia en la cual trabaja el procesador), por lo que si ignoramos el valor delta y simplemente ejecutamos todo a la velocidad máxima que puede ejecutar el procesador, es decir, el juego podría tener problemas en la velocidad para controlar su carácter correctamente ya que sería demasiado rápido o lento. Usando el parámetro deltaTime en nuestro cálculo de movimiento, podemos garantizar que nuestros sprites se moverán a la velocidad que nosotros deseemos en dispositivos con diferentes velocidades de procesador.
Al actualizar cualquier aspecto del juego mediante la función de update() es reflejado automáticamente mediante la función de render() y con esto, la actualización del juego a nivel gráfico.
El Game loop es utilizado por todas las implementaciones de las clases tipo Game y sus componentes:
https://docs.flame-engine.org/1.6.0/flame/game.html
Función update (actualizar)
Los elementos que estén en el juego, necesitan ser redibujados constantemente según el estado actual del juego; para entender esto más claramente, veamos un ejemplo:
Supongamos que un elemento del juego representado por un sprite (una imagen), que en este ejemplo llamaremos cómo "player"; cuando el usuario da un click sobre un botón, entonces el player debe de actualizar su posición; esta actualización, se aplica en el juego mediante una función llamada update().
Al igual que vimos en el ejemplo del círculo en movimiento, es esta función la que se encarga de actualizar la posición del círculo en pantalla.
La función de update() es tal cual indica su nombre, una función de actualización, la cual, recibe un parámetro, llamado "deltatime" (dt) que nos dice
la hora que ha transcurrido desde que se dibujó el cuadro anterior. Esta variable debe usarla para hacer que su componente se mueva a la misma velocidad en todos los dispositivos.
Al actualizar cualquier aspecto del juego mediante la función de update() es reflejado automáticamente mediante la función de render() y con esto, la actualización del juego a nivel gráfico.
En Flame, el método update() se utiliza para actualizar el estado del juego en cada interacción; como explicamos antes en la publicación del GameLoop, es fundamental para realizar cualquier actualización por pantalla. Este método se llama automáticamente por el motor de juegos de Flame en un tiempo relativamente constante que como mencionamos antes, cada cuanto se llame a esta función, depende de la velocidad de procesamiento del dispositivo usado
La función de update, recibe un parámetro llamado como DeltaTime el cual es el tiempo pasado desde la ultima invocación de dicha función.
Dentro del método update(), se pueden realizar tareas como actualizar la posición, escalado… y en escencia, cualquier operación que quieras realizar en el juego; una validación común es la de verificar colisiones, entre otras operaciones.
A continuación se muestra un ejemplo básico de cómo se puede implementar el método update() en una clase de juego en Flame:
import 'package:flame/game.dart';
class MyGame extends FlameGame{
@override
void update(double dt) {
super.update(dt);
}
}En este ejemplo, se define una clase MyGame que extiende FlameGame. El método update() se sobrescribe para realizar las operaciones mencionadas antes; es importante acotar que, esta función también esta disponible en los componentes en Flame y no solamente a nivel de las clases tipo Game.
Es importante llamar al método super.update(dt) al finalizar el método update(), ya que esto se encarga de actualizar los componentes del juego y renderizarlos en la pantalla.
Espero que esta información te sea útil para entender la función del método update() en Flame.
El Game loop es utilizado por todas las implementaciones de las clases tipo Game y sus componentes:
https://docs.flame-engine.org/1.6.0/flame/game.html
Dibujar un rectángulo
Anteriormente, vimos cómo dibujar un circulo en Flame usando los Canvas, siguiendo con las figuras básicas, vamos a dibujar un rectangulo o cuadrado en Flame.
Recapitulando un poco; Flame es un motor de juego 2D que se ejecuta sobre Flutter; muy sencillo de utilizar y con el cual podemos empezar en el mundo del desarrollo de juegos en 2D; con un proyecto en Flutter, podemos tener el mismo juego para diferentes plataformas como Windows, Linux, MacOS y por supuesto, dispositvos móviles.
Con Flame, podemos crear juegos 2D multiplataforma para iOS y Android.
Flame ofrece muchas funcionalidades listas para usar como sprites, manejar las colisiones, eventos de entrada de datos, etc. También ofrece soporte para la integración de audio entre muchas cosas más.
Para dibujar un rectángulo podemos emplear exactamente el mismo código presentado en dibujar un circulo en Flame pero, cambiando la función del canvas con:
canvas.drawRect(Rect.fromCircle(center: const Offset(0, 0), radius: 20), BasicPalette.red.paint());Clase Game en Flame Flutter juegos en 2D
Como comentamos en entradas anteriores, una aplicación creada en Flame consta de dos partes, los componentes y una única instancia de una clase Game (clase tipo Game) de las cuales, tenemos varias aunque, la más versátil viene siendo la de FlameGame.
Esta clase principal se utiliza para crear el juego, estas clases pueden ser la de Game, BaseGame y FlameGame, que proporciona un completo conjunto de herramientas para el desarrollo de juegos en 2D como hemos visto hasta este punto y como veremos en futuras entregas.
Mediante una clase tipo Game, podemos inicializar y controlar toda la aplicación, desde su ciclo de vida, actualizaciones mediante el GameLoop, comunicación con otros componentes, interactuar con el usuario, habilitar ciertas funcionalidades a los componentes, entre otros. En resumen, podemos usar la clase Game en Flame para crear y controlar TODO el juego, así como para agregar y administrar todos los componentes de diversas formas según la lógica de negocio de nuestro juego.
Tipos de clases
La clase BaseGame en Flame es una clase abstracta que proporciona un conjunto de herramientas y métodos útiles para el desarrollo de juegos en 2D.
La clase Game, es una subclase de BaseGame, se utiliza para implementar el juego real. Podemos usar la clase BaseGame para personalizar y extender la funcionalidad del ciclo de vida del juego, manejar eventos de entrada, usar cámaras, procesar imágenes, entradas de datos mediante teclados, tap entre otros...
Función update - Actualizar posición de un Sprite
Como mencionamis en entradas anteriores, para cualquier actualización que queremos se realice en el juego, se debe de hacer mediante el método de update(); con esto, podemos actualizar distintos aspectos sobre algún componente de nuestro juego como el tamaño, la posición, etc. Durante la actualización, vamos a cambiar la posición del sprite estableciendo el valor de x y y sobre la propiedad de position; en este ejemplo, vemos una implememtación minina de como actualizar la posición de un sprite:
import 'package:flame/game.dart';
import 'package:flame/components/sprite.dart';
class MyGame extends Game {
late SpriteComponent sprite;
@override
Future<void> onLoad() async {
sprite = SpriteComponent.fromImage(
await images.load('sprite.png'),
x: 20,
y: 100,
);
}
@override
void update(double dt) {
// actualizar la posición del sprite aquí
sprite.x += 10 * dt;
}
@override
void render(Canvas canvas) {
sprite.render(canvas);
}
}En este ejemplo, el sprite empezará en la posición (20, 100) y se moverá horizontalmente hacia la derecha en cada interacción de la función update().
La posición se actualiza en el método update mediante el cálculo de sprite.x += 10 * dt, que agrega 10 píxeles por segundo; recuerda que el parámetro de dt es el tiempo pasado desde la anterior llamada; por lo tanto, al pasar un segundo, se desplazará 10 pixeles; esto es muy importante ya que el movimiento será constante independientemente de la velocidad del juego y es justamente por multiplicar la posición por el factor de dt, que es el tiempo transcurrido desde el último fotograma en segundos.
Aquí te dejo un ejemplo de cómo actualizar la posición de un sprite en la clase Game de Flame:
Para mover un PositionComponent o un Sprite en general, tenemos la propiedad de position, la cual recibe un vector2; podemos usar la función de update() para actualizar la posición de la imagen y desplazarlo automáticamente:
class PlayerImageSpriteComponent extends SpriteComponent{
***
@override
void update(double dt) {
position = Vector2(centerX++, centerY++);
super.update(dt);
}
}En este otro ejemplo, no se usa el factor de dt y por lo tanto, la actualización será bastante rápida y distinta para cada dispositivo; para que sean constante la velocidad independientemente del la velocidad de procesamiento, tenemos:
class PlayerImageSpriteComponent extends SpriteComponent{
***
@override
void update(double dt) {
position = Vector2(centerX++, centerY++) * dt;
super.update(dt);
}
}Evento de entradas de teclado en Flutter Flame
En Flame, tenemos acceso al teclado como fuente de datos, es decir, un mecanismo con el cual el usuario puede interactuar con la aplicación; al igual que ocurre con otros componentes en Flame, tenemos acceso a la entrada de datos por el teclado tanto a nivel de la clase tipo Game y de componentes; aunque, su implementación es prácticamente la misma; veamos ambas implementaciones.
Con la clase KeyboardEvents en las clases tipo Game o la clase KeyboardHandler en componentes, tenemos funciones que permiten detectar y responder a eventos del teclado realizados por el usuario y tambien combinación de teclas. combinaciones de teclas y eventos relacionados; tenemos constantes como LogicalKeyboardKey.arrowUp para determinar cada tecla; veamos una implementación practica.
Clase tipo Game
Siguiendo con nuestra aplicación, vamos a colocar a nivel de la clase FlameGame, el escuchador de las entradas de teclado:
class MyGame extends FlameGame with KeyboardEvents {
***
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
//print(keysPressed);
print(event);
return KeyEventResult.handled;
}
}Al presionar distintas teclas, con cualquiera de los eventos, veremos una salida como la siguiente:
{LogicalKeyboardKey#00301(keyId: "0x100000301", keyLabel: "Arrow Down", debugName: "Arrow Down")}
RawKeyUpEvent#881c1(logicalKey: LogicalKeyboardKey#00304(keyId: "0x100000304", keyLabel: "Arrow Up", debugName: "Arrow Up"), physicalKey: PhysicalKeyboardKey#70052(usbHidUsage: "0x00070052", debugName: "Arrow Up"))En el caso anterior, se presionó la tecla de flecha arriba o arrow up.
Para este experimento, no realizaremos ninguna adaptación más en la aplicación ya que, la lógica del juego va a estar implementada en los componentes y no a nivel de juego.
Puedes crear condiciones como:
keysPressed.contains(LogicalKeyboardKey.arrowUp)Para preguntar por la tecla presionada.
Nivel de componente
Para poder utilizar los eventos localmente en los componentes, debemos de utilizar el mixin HasKeyboardHandlerComponents a nivel de la clase FlameGame:
lib/main.dart
class <LevelGame> extends FlameGame
with
HasKeyboardHandlerComponents {}Y ahora, a nivel del componente al cual vamos a colocar el escucha del evento de teclado, usamos el mixin de KeyboardHandler:
class <Component> extends <TypeComponent> with KeyboardHandler {}Caso práctico
Con las configuraciones anteriores realizadas tanto a nivel de la clase Game como en el componente, vamos a realizar algunas modificaciones sobre la clase componente llamada "PlayerImageSpriteComponent", vamos a agregar el evento para mover el sprite:
lib/components/player_image_sprite_component.dart
import 'package:flame/components.dart';
class PlayerImageSpriteComponent extends SpriteComponent with KeyboardHandler {
***
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {
position = Vector2(centerX, centerY--);
} else if (keysPressed.contains(LogicalKeyboardKey.arrowDown)) {
position = Vector2(centerX, centerY++);
} else if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
position = Vector2(centerX++, centerY);
} else if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
position = Vector2(centerX--, centerY);
}
return true;
}
}Como puedes apreciar en el código anterior, podemos mover fácilmente el sprite utilizando las flechas del teclado y modificando el vector position provisto por la clase SpriteComponent/PositionComponent; aunque esta no es la forma obtima de mover al player, ya que, no estamos usando el deltatime, es un primer acercamiento.
Reto: Mover sprite al pulsar flechas de direcciones
Ya sabemos como usar los eventos de teclado en Flame a nivel de clase y componente, ahora, como reto, adapta el código anterior y además de las flechas, que se puedan utilizar las teclas de w (arriba), d (derecha), s (abajo) y a (izquierda).
Resolución reto
Para agregar las teclas típicas en de WASD en nuestro script, podemos hacer un sencillo condicional con un OR:
@override
bool onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (keysPressed.contains(LogicalKeyboardKey.arrowUp) ||
keysPressed.contains(LogicalKeyboardKey.keyW)) {
position = Vector2(centerX, centerY--);
} else if (keysPressed.contains(LogicalKeyboardKey.arrowDown) ||
keysPressed.contains(LogicalKeyboardKey.keyS)) {
position = Vector2(centerX, centerY++);
} else if (keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
keysPressed.contains(LogicalKeyboardKey.keyD)) {
position = Vector2(centerX++, centerY);
} else if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
keysPressed.contains(LogicalKeyboardKey.keyA)) {
position = Vector2(centerX--, centerY);
}
return true;
}Como puedes ver, la lógica es sencilla, basta con detectar las teclas típicas de WASD y de direcciones y desplazar el player en la posición acorde; recordemos que para:
- X si es positivo, se mueve a la derecha y negativo a la izquierda.
- Y si es positivo hacia abajo y negativo hacia arriba.
Sprites o imágenes para Flame - SpriteComponent
Vamos a conocer los elementos principales que debemos de conocer para trabajar con Flame, veremos cómo crear los componentes, que son la pieza clave para crear cada uno de los elementos de nuestro juego, la estructura básica de un juego con Flame y el uso de las entradas (teclado, joystick, etc) que es la manera en la cual, el usuario puede interactuar con la aplicación.
Sprites
Los sprites son un termino común en el desarrollo de juegos en 2D y no son más que imágenes que representan los objetos y personajes del juego . Los sprites pueden ser animados o estáticos; esto queire decir que en el caso de los sprites animados, en la misma imagen se encuentran disponibles cada uno de los frames para una acción (como puedes ver en las imágenes de esta publicación) estados como morir, caminar, correr o saltar son los títpicos que podemos usar.
Los sprites se pueden combinar en hojas de sprites (sprite sheets) para reducir la cantidad de recursos de memoria necesarios para cargarlos; ya que, si cargaramos cada frame en una imagen distinta, se tendría que acceder de manera individual a cada una de ellas, operación que no sería optima; tambien podemos combinar múltiples frames de diversos tipos e inclusive de otros jugadores u objetos, todo en una sola imagen.
Referenciar un Sprite
Cada sprite tiene una posición en el espacio 2D del juego y puede cambiar su posición en cada fotograma, lo que permite la creación de animaciones.
En el desarrollo de juegos en 2D, el uso de sprites es muy común debido a su simplicidad y eficiencia en el uso de memoria; en Internet, puedes encontrar todo tipo de Sprites de objetos y personajes tanto de manera gratuita como de pago; en webs como la HumbleStore o Fanatical, siempre tienen bundles de Sprites que podemos usar en nuestros proyectos a un precio muy reducido.
Primero, comencemos definiendo que es un Sprite
SpriteComponent: Componentes para renderizar imágenes
Los SpriteComponent son un tipo de componente en el desarrollo de videojuegos en 2D que se utiliza para renderizar imágenes, es decir, sprites. Los sprites se utilizan para representar objetos y personajes del juego en 2D y pueden ser animados o estáticos; en el curso y libro, usamos el componente de SpriteComponent para dibujar personanes estáticos, como componente de fondo, para tiles, para consumibles y en general, lo puedes utilizar para representar cualquier objeto con el cual se pueda interactuar en el juego de alguna manera.
El SpriteComponent se encarga de cargar y mostrar una imagen en el juego. Este componente puede recibir una hoja de sprites, que es una imagen que contiene varios sprites diferentes que se utilizan en el juego. El SpriteComponent permite seleccionar uno de los sprites de la hoja de sprites para su visualización en el juego.
En resumen, los SpriteComponent son componentes importantes en el desarrollo de juegos en 2D, ya que permiten representar objetos y personajes del juego mediante el uso de sprites.
Como mencionamos antes, un sprite a una serie de imágenes unidas en un mismo archivo una al lado de otra como la siguiente:
https://www.pngwing.com/es/free-png-izmqx
O como esta:
https://www.gameart2d.com/free-dino-sprites.html
Un sprite no es más que una colección de imágenes puestas en una sola imagen y son muy utilizados en cualquier motor de videojuegos como Unity; en el desarrollo de videojuegos, un sprite es una imagen donde están incluidos todos los movimientos (estados) de un personaje u objeto; por lo tanto, es común que para un juego 2D existan múltiples imágenes para cada objeto animable.
Crear un Sprite Sheet para generar imágenes animadas para juegos 2D con TexturePacker
Un sprite animado es un conjunto de imágenes 2D combinadas para generar una animación. En los videojuegos y en general en cualquier animación en 2D, se utilizan sprites para generar los personajes, objetos y otros elementos que se mueven; estos sprites pueden ser un sprite sheet como el mostrado en la imagen de abajo.
Los sprites animados se crean generalmente como una serie de fotogramas secuenciales (un sprite sheet), que se reproducen en rápida sucesión para crear la ilusión de movimiento.
Los sprites animados pueden ser creados a mano o utilizando software especializado, y se pueden guardar en diferentes formatos de archivo, como PNG o GIF. En Flame, podemos generar animaciones basadas en sprite muy facilmente empleando una seria de funciones disponibles en Flame
Ahora, nos interesa no solamente procesar una sola imagen, sino, varias de ellas, para ello, utilizaremos una imagen que conste de varias imágenes, como la presentada anteriormente:
En la cual, simulaba una caminata de nuestro player.
Para este ejemplo, tenemos son imágenes individuales para la caminata:
Para evitar cargar una imagen una a una en el proyecto, y manejarlas de manera independiente y con esto, elevar el consumo de recursos (ya que, a nivel del proyecto se debería de cargar de manera individual cada imagen), vamos a funcionar todos estos pasos en uno solo; este formato se conoce como "sprite sheet"; para esta labor, puedes utilizar cualquier programa de edición de imágenes, pero, para facilitar el proceso, se recomienda utilizar programas como el siguiente:
https://www.codeandweb.com/texturepacker
El programa anterior, nos permite crear un sprite sheet para las imágenes; puedes instalar el programa anterior llamado TexturePacker en Linux, Windows y MacOS; para este proceso, puedes descargar cualquier kit de imágenes que quieras; aunque, en este web:
https://www.gameart2d.com/freebies.html
Encontrarás varios sprite que puedes utilizar de manera gratuita.
Una vez instalado el programa anterior y descargado tu kit de imágenes, vamos a crear el sprite sheet; para ello, arrastramos y soltamos las imágenes con las cuales queremos trabajar hacia esta área; en el caso del libro, son las imágenes mostradas anteriormente:
Y tendremos:
Con la opción de "Zoom" ubicado en la esquina inferior izquierda, puedes ajustar el zoom para visualizar completamente el sprite sheet:
Puedes previsualizar la escena:
Indicando los FPS (velocidad) de la animación (recuerda seleccionar todos los sprites cargados):
Y finalmente, exportar el proyecto para nuestro sprite sheet:
Y tendremos, nuestro sprite sheet que llamaremos como dino.png:
El cual, luego copiaremos en el proyecto en Flutter
Como utilizar un Sprite Sheet en Flutter con Flame 13 - Juegos 2D animados
Un sprite no es más que un objeto o personaje y son muy utilizados en cualquier motor de videojuegos como Unity; en el desarrollo de videojuegos, los sprites es donde están incluidos todos los movimientos (estados) de un personaje u objeto; por lo tanto, es común que para un juego 2D existan múltiples imágenes para cada objeto animable.
nos interesa no solamente procesar una sola imagen, sino, varias de ellas, para ello, utilizaremos una imagen que conste de varias imágenes, como la generada anteriormente.
Un sprite sheet es una imagen que contiene varios sprites o imágenes más pequeñas utilizadas en el desarrollo de juegos en 2D. Los sprites shhets suelen representar objetos , personajes y elementos del juego, y pueden ser estáticos o animados. La hoja de sprites permite que el juego cargue varias imágenes necesarias para el juego de una sola vez, lo que puede mejorar la eficiencia en la carga y el rendimiento general del juego. Al tener todas las imágenes en una sola imagen, facilita la gestión y organización de los recursos gráficos en el desarrollo de juegos en 2D.
Caso práctico
Finalmente, a nivel de nuestro código, crearemos la siguiente clase:
lib/components/player_sprite_sheet_component.dart
import 'package:flame/sprite.dart';
import 'package:flutter/material.dart';
import 'package:flame/flame.dart';
import 'package:flame/components.dart';
import 'dart:ui';
class PlayerSpriteSheetComponent extends SpriteComponent {
late double screenWidth, screenHeight, centerX, centerY;
final double spriteSheetWidth = 680, spriteSheetHeight = 472;
@override
Future<void>? onLoad() async {
//sprite = await Sprite.load('tiger.png');
final spriteImage = await Flame.images.load('dino.png');
final spriteSheet = SpriteSheet(image: spriteImage, srcSize: Vector2(spriteSheetWidth, spriteSheetHeight));
sprite = spriteSheet.getSprite(2, 1);
screenWidth = MediaQueryData.fromWindow(window).size.width;
screenHeight = MediaQueryData.fromWindow(window).size.height;
size = Vector2(spriteSheetWidth, spriteSheetHeight);
centerX = (screenWidth / 2) - (spriteSheetWidth / 2);
centerY = (screenHeight / 2) - (spriteSheetHeight / 2);
position = Vector2(centerX, centerY);
return super.onLoad();
}
}Explicación del código anterior
Como puedes apreciar en el código anterior, definimos el tamaño para cada sprite de nuestro sprite sheet; para la imagen que seleccionamos, sería de 680 píxeles x 472 píxeles:
Para ello, usamos un par de propiedades:
late double spriteSheetWidth = 680.0, spriteSheetHeight = 472.0;Es importante notar que, debes de especificar el tamaño acorde al sprite que estés utilizando.
Cargamos el sprite sheet:
var spriteImages = await Flame.images.load('dino.png');Y definimos el spriteSheet para que pueda ser manipulado mediante una propiedad; es en este paso que, utilizamos las dimensiones individuales de cada estado:
final spriteSheet = SpriteSheet(image: spriteImages, srcSize: Vector2(spriteSheetWidth, spriteSheetHeight));Finalmente, ya es posible consultar de manera individual cada posición de uno de los pasos del sprite sheet:
sprite = spriteSheet.getSprite(1, 1);Existen sprite sheet con tamaños distintos para cada frame o sprite, por lo tanto, debes de estar seguro que el sprite sheet que estas usando tienen los mismos tamaños para cada uno de los sprite, en este ejemplo es de 680 px 472 px.
Cargar un Sprite
Tambien es posible cargar sprite usando la función de load; se usa la función loadSprite de la clase Flame para cargar el sprite: sprite = await Flame.images.load('imagen.png');
Una vez cargado el sprite, podrías utilizar el componente SpriteComponent para renderizarlo en el juego como si fuera un componente; esto puede parecer poco pero, al usar los componentes de Flame, podemos usar características como el sistema de colisiones, entrada de datos y en general, cualquier funcionalidad disponible en una clase PositionComponent.
Aquí te dejo un ejemplo de cómo cargar un sprite en Flame:
import 'package:flame/components/sprite_component.dart';
import 'package:flame/flame.dart';
class MyGame extends BaseGame {
SpriteComponent _spriteComponent;
MyGame() {
_loadSprite();
}
void _loadSprite() async {
var image = await Flame.images.load('ruta/a/la/imagen.png');
_spriteComponent = SpriteComponent.fromImage(0, 0, image);
add(_spriteComponent);
}
}Este código creará un nuevo juego (MyGame) y cargará un sprite utilizando la función load de Flame, y lo renderizará en el componente SpriteComponent.
Recuerda que este material es parte de mi curso y libro en Flame para la creación de juegos en 2D.
Como utilizar un Sprite Sheet Componente Flutter con Flame
Para generar un Sprite Sheet animado, tenemos que realizar varias operaciones que van, desde cargar una imagen sprite, hasta referenciarla en el proyecto; cada estado del sprite, tiene que tener un tamaño definido; para ello, usamos un par de propiedades:
late double spriteSheetWidth = 680.0, spriteSheetHeight = 472.0;Es importante notar que, debes de especificar el tamaño acorde al sprite que estés utilizando.
Cargamos el sprite sheet:
var spriteImages = await Flame.images.load('dino.png');Y definimos el SpriteSheet para que pueda ser manipulado mediante una propiedad; es en este paso que, utilizamos las dimensiones individuales de cada estado:
final spriteSheet = SpriteSheet(image: spriteImages, srcSize: Vector2(spriteSheetWidth, spriteSheetHeight));Finalmente, ya es posible consultar de manera individual cada posición de uno de los pasos del sprite sheet:
sprite = spriteSheet.getSprite(1, 1);Una animación, no es mas que una lista de Sprite, por lo tanto, si formas un sprite list, puedes utilizarlas en el componente correspondiente de Flame para manejar los sprite animados; por ejemplo:
class PlayeSpriteSheetComponent extends SpriteAnimationComponent {
late double screenWidth, screenHeight, centerX, centerY;
final double spriteWidth = 512.0, spriteHeight = 512.0;
late double spriteSheetWidth = 680.0, spriteSheetHeight = 472.0;
late SpriteAnimation dinoAnimation;
@override
Future<void> onLoad() async {
super.onLoad();
screenWidth = MediaQueryData.fromWindow(window).size.width;
screenHeight = MediaQueryData.fromWindow(window).size.height;
centerX = (screenWidth / 2) - (spriteSheetWidth / 2);
centerY = (screenHeight / 2) - (spriteSheetHeight / 2);
var spriteImages = await Flame.images.load('dino.png');
final spriteSheet = SpriteSheet(
image: spriteImages,
srcSize: Vector2(spriteSheetWidth, spriteSheetHeight));
//sprite = spriteSheet.getSprite(1, 1);
position = Vector2(centerX, centerY);
size = Vector2(spriteSheetWidth, spriteSheetHeight);
//sprite = await Sprite.load('Sprite.png');
animation = [spriteSheet.getSprite(1, 1),spriteSheet.getSprite(1, 2)];
}
}Animación de sprite con SpriteAnimationComponent
En Flame, tenemos un componente para generar las animaciones en base a un Sprite Sheet como:
En el curso y libro de Flame para crear juegos en 2D, creamos una función, con la cual, podemos generar fácilmente un listado animado listo para usar en un SpriteAnimation; simplemente vemos el sprite sheet como una matriz y definimos la posición inicial y final, además del tamaño del sprite sheet; entre otras opciones:
extension CreateAnimationByLimit on SpriteSheet {
SpriteAnimation createAnimationByLimit({
required int xInit,
required int yInit,
required int step,
required int sizeX,
required double stepTime,
bool loop = true,
}) {
final List<Sprite> spriteList = [];
int x = xInit;
int y = yInit - 1;
for (var i = 0; i < step; i++) {
if (y >= sizeX) {
y = 0;
x++;
} else {
y++;
}
spriteList.add(getSprite(x, y));
// print(x.toString() + ' ' + y.toString());
}
return SpriteAnimation.spriteList(spriteList,
stepTime: stepTime, loop: loop);
}
}SpriteAnimationComponent: Pruebas
Para esta función, necesitaremos los siguientes parámetros:
- Paso inicial en X (por ejemplo (3,0)).
- Paso inicial en Y (por ejemplo (3,0)).
- Cantidad de pasos (por ejemplo el valor de 6 corresponde a qué queremos 6 sprites).
- El ancho de la matriz (en la imagen de dino.png la matriz de sprite sería de 3x3 por lo tanto, el ancho sería de 3).
- Velocidad de la animación.
- Si se va a ejecutar en bucle.
Teniendo esto en cuenta, crearemos la siguiente función de extensión que extienda a la clase SpriteSheet:
lib/components/player_sprite_sheet_component.dart
extension CreateAnimationByLimit on SpriteSheet {
SpriteAnimation createAnimationByLimit({
required int xInit,
required int yInit,
required int step,
required int sizeX,
required double stepTime,
bool loop = true,
}) {
final List<Sprite> spriteList = [];
int x = xInit;
int y = yInit - 1;
for (var i = 0; i < step; i++) {
if (y >= sizeX) {
y = 0;
x++;
} else {
y++;
}
spriteList.add(getSprite(x, y));
// print(x.toString() + ' ' + y.toString());
}
return SpriteAnimation.spriteList(spriteList,
stepTime: stepTime, loop: loop);
}
}Lo más importante es notar el bloque del for en la cual, recorremos la matriz de los sprite justamente como en los ejemplos explicados anteriormente; con esta lista de sprite, generamos la animación utilizando SpriteAnimation.spriteList().
Con el código anterior definido en la clase PlayeSpriteSheetComponent definimos la propiedad de animation, usando la función llamada createAnimationByLimit() que creamos anteriormente, el resto de propiedades, son las mismas empleadas en cualquier otro componente de tipo SpriteComponent o PositionComponent que presentamos en anteriores entregas; propiedades como size, position son necesarias para indicar el tamaño y posición del sprite.
Entradas de Teclado FlameGame, nivel de clase tipo Game
Dotar al usuario para que pueda interactuar con el juego mediante:
- Teclado
- Drag and Drop
- Gestos
- Tap/toque (en la mayoría de los casos, equivalente al evento click)
- Joystick virtual
Entre otros; las entradas, son una funcionalidad fundamental en cualquier juego hoy en día; en Flame, podemos implementar este tipo de funcionalidades mediante eventos los cuales son escuchados mediante una función escuchadora.
En este apartado, vamos a trabajar con las entradas de teclado; Flame ofrece dos formas diferentes de tomar entradas de teclado:
- A nivel de la clase Game.
- A nivel de componentes.
En cualquiera de los escenarios, su uso es muy sencillo y es similar a otros enfoques, por ejemplo, el de JavaScript, en el cual, tenemos una función listener llamada onKeyEvent que se ejecuta cada vez que presiona una tecla, en dicha función, recibimos el evento con la tecla presionada a la cual, podemos aplicar cualquier lógica.
Nivel de la clase Game
Para poder dotar a la aplicación para que reconozca los eventos de teclado, es decir, que, al presionar una tecla, poder asignar alguna función, podemos emplear la clase de KeyboardEvents sobre la clase de tipo Game.
Esta viene siendo la manera más global ya que, los eventos se ejecutan a nivel de la clase Game, que recordemos que es global a toda la aplicación y no de los componentes que es donde la mayoría de las veces nos interesa interactuar; es decir, como vimos en el ejemplo anterior, en el cual dibujamos un sprite en un componente, si quisiéramos mover ese sprite, que puede simular nuestro jugador o player, nos interesa es que dicho componente reciba los eventos de teclado (o entrada en general); aun así, es importante conocer que a nivel de la clase Game podemos agregar este tipo de interactividad ya que, muchas veces es necesario que varios componentes necesitan realizar alguna funcionalidad cuando ocurre (por ejemplo) una entrada de teclado; por ejemplo, tenemos dos componentes:
- Un jugador
- El escenario
Que tal cual vimos, son dos clases apartes, dependiendo de la lógica de tu juego, puede ser que, al presionar (por ejemplo) la tecla de movimiento (flecha arriba o la tecla W) esto aplique el movimiento en el jugador y un cambio en el escenario, y en estos casos, hay que comunicar dos componentes y no solamente uno.
A nivel de la clase FlameGame, debemos de agregar el mixin de KeyboardEvents y con esto, sobrescribir la función onKeyEvent; este método recibe dos parámetros los cuales devuelven las teclas presionadas:
Caso práctico
Siguiendo con nuestra aplicación, vamos a colocar a nivel de la clase FlameGame, el escuchador de las entradas de teclado:
class MyGame extends FlameGame with KeyboardEvents {
***
@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
//print(keysPressed);
print(event);
return KeyEventResult.handled;
}
}Al presionar distintas teclas, con cualquiera de los eventos, veremos una salida como la siguiente:
{LogicalKeyboardKey#00301(keyId: "0x100000301", keyLabel: "Arrow Down", debugName: "Arrow Down")}
RawKeyUpEvent#881c1(logicalKey: LogicalKeyboardKey#00304(keyId: "0x100000304", keyLabel: "Arrow Up", debugName: "Arrow Up"), physicalKey: PhysicalKeyboardKey#70052(usbHidUsage: "0x00070052", debugName: "Arrow Up"))En el caso anterior, se presionó la tecla de flecha arriba o arrow up.
Para este experimento, no realizaremos ninguna adaptación más en la aplicación ya que, la lógica del juego va a estar implementada en los componentes y no a nivel de juego.
Puedes crear condiciones como:
keysPressed.contains(LogicalKeyboardKey.arrowUp)Para preguntar por la tecla presionada.
Evento tap o click a nivel de los componentes
El evento "tap" se refiere a una de las muchas acciones que tenemos disponibles como entrada de datos por parte del usuario (jugador) en la que tocan o hacen click en la ventana de nuestro juego sobre una pantalla con la cual puede interactuar el usuario, como sería un teléfono inteligente con Android, iOS, una tableta, iPAD, etc
El tap puede ser la acción de tocar el elemento con un dedo, mientras que en una computadora, un tap puede ser la acción de hacer clic con un mouse en el elemento; y en Flame con Flutter esto es fundamental;
El evento "tap" se utiliza comúnmente en el desarrollo de aplicaciones móviles y de escritorio para permitir que los usuarios interactúen con la interfaz de usuario; es el evento por excelencia junto con la entrada de teclado que lo tratamos en otra entrada.
Con el evento tap podemos hacer toda clase te juegos como el tipico planta vs zombies para seleccionar y colocar una planta, para indicar que un caracer tiene que saltar, atacar…
Em definitiva, como toda entrada de datos, lo que queiras hacer depende de ti.
El tap es ora entrada de datos que podemos utilizar en Flame, es el de tap sobre la pantalla que a la final viene siendo un evento click; al igual que con la entrada de teclado, podemos utilizar el evento tap a nivel de la clase como a nivel de componentes.
Tap a nivel de componente
Para poder utilizar el evento de tap a nivel del componente, primero, debemos de habilitarlo a nivel del componente en la clase tipo Game:
lib/main.dart
class <LevelGame> extends FlameGame
with
HasTappables {}Y desde el componente, implementamos el mixin de Tappable:
class <Component> extends <TypeComponent> with Tappable {}Con esto, tenemos acceso a las mismas funciones que las usadas a nivel de la clase Game que mostramos antes.
Por ejemplo, una posible implementación queda como:
lib\components\player_sprite_sheet_component.dart
class PlayeSpriteSheetComponent extends SpriteAnimationComponent with Tappable {
***
@override
bool onTapDown(TapDownInfo info) {
print("Player tap down on ${info.eventPosition.game}");
return true;
}
@override
bool onTapUp(TapUpInfo info) {
print("Player tap up on ${info.eventPosition.game}");
return true;
}
}Con el par de funciones anteriores, podejos determinar si ejecutar el evento al tocar la pantalla o al dejar de precionar la pantalla respetivamente; usualmente se usa es la de onTapDown, que es cuando el usurio da un click sobre la pantalla, pero, también puedes que quieras colocar alguna lógica cuando el usuario levanta el dedo o libera el click… en ese caso, puedes usar el de onTapUp en su lugar.
Nuevamente, lo que implementes en estas funciones depende de tu juego, en nuestro curso y libro sobre Flame para el desarrollo de juegos en 2D, hacemos toda clase de juegos con las mismas, ya sea para seleccionar las plantas y colcoarlar, en nuestro juego de plantas vs zombies, para seleccionar items, entre otros.
Cambiar animaciones de un Sprite por Tap Flutter
Como un pequeño reto, debes de implementar la siguiente lógica en el componente de los sprite sheet animados, y tienes que implementar una lógica muy sencilla para variar la animación cada vez que se da un tap sobre la pantalla; para ello, puedes utilizar una propiedad numérica la cual indicará la animación a ejecutarse:
- animationIndex con el valor de cero, se muestra la animación de la muerte.
- animationIndex con el valor de uno, se muestra la animación de inactiva.
- animationIndex con el valor de dos, se muestra la animación de salto.
- animationIndex con el valor de tres, se muestra la animación de caminando.
- animationIndex con el valor de cuatro, se muestra la animación de corriendo.
Resolución reto
Como resolución, podemos utilizar un switch en la cual, realicemos el cambio de la animación según el valor de animationIndex que incrementaremos cada vez que ocurra el "tap" sobre la pantalla:
lib\components\player_sprite_sheet_component.dart
***
int animationIndex = 0;
***
@override
bool onTapDown(TapDownInfo info) {
super.onTapDown(info);
print(info);
animationIndex++;
if (animationIndex > 4) animationIndex = 0;
switch (animationIndex) {
case 1:
animation = dinoIdleAnimation;
break;
case 2:
animation = dinoJumpAnimation;
break;
case 3:
animation = dinoWalkAnimation;
break;
case 4:
animation = dinoRunAnimation;
break;
case 0:
default:
animation = dinoDeadAnimation;
break;
}
return true;
}Como puedes ver, podemos variar fácilmente entre una animación a otra mediante un valor numérico que corresponde a un indice de la animación que se quiere animar.
Este snippet forma parte del proyecto completo:
https://github.com/libredesarrollo/flame-curso-libro-dinojump03
En el cual, vemos como crear la animación paso a paso; lo importante es notar lo sencillo que resulta hacer este tipo de integración, cada tecla del teclado esta asociada a una acción distinta como caminar, saltar o correr que a su vez está asociada a una animación distinta que es fácilmente cambiada asignando a la propiedad animation un valor distinto.
Crear un fondo parallax en Flame
Los fondos tipos parallax son empleado en toda clase de aplicaciones, no solamente en juegos en 2D, si no también en páginas web para definir background, etc; en esta entrada veremos como definir un fondo parallax como fondo para nuestros juegos en 2D con Flame.
Comencemos cargando las imágenes para el fondo parallax:
En Flame, tenemos un componente llamado ParallaxComponent que nos facilita el proceso de crear un efecto parallax muy fácilmente; con definir las imágenes a usar como un listado:
lib/main.dart
class MyGame extends FlameGame {
@override
void onLoad() async {
super.onLoad();
add(await bgParallax());
}
Future<ParallaxComponent> bgParallax() async {
ParallaxComponent parallaxComponent = await loadParallaxComponent([
ParallaxImageData('layer06_sky.png'),
ParallaxImageData('layer05_rocks.png'),
ParallaxImageData('layer04_clouds.png'),
ParallaxImageData('layer03_trees.png'),
ParallaxImageData('layer02_cake.png'),
ParallaxImageData('layer01_ground.png'),
],
baseVelocity: Vector2(10, 0));
return parallaxComponent;
}
}Verás que ya casi conseguimos el efecto parallax, de momento, todas las capas se mueven a la misma velocidad; el argumento de baseVelocity permite definir el desplazamiento de los fondos, usualmente se desea que el desplazamiento ocurra en el eje de las X y no en el eje Y, así que:
baseVelocity: Vector2(10, 0)Como tarea, prueba distintos valores al momento de definir el vector y usar distintas imágenes (comenta y descontenta algunos de los ParallaxImageData definidos anteriormente, cambia las capas de orden) y evalúa su comportamiento; pero, en esencia, si usas valores altos, como por ejemplo:
baseVelocity: Vector2(100, 0)Veas un desplazamiento rápido en los fondos, si colocas valores más bajos:
baseVelocity: Vector2(5, 0)El desplazamiento será más lento, y si defines el desplazamiento en el Y:
baseVelocity: Vector2(5, 10)Las capas se moverán en el eje X y Y.
Al ejecutar la aplicación, deberías de ver una salida como la siguiente:
Variar velocidad en las capas
De momento conseguimos un fondo parallax algo aburrido ya que, aunque tenemos un fondo que se desplaza, todo se mueve a la misma velocidad; pero, para personalizar este aspecto, es decir, que los fondos se desplacen a diferentes velocidades, podemos usar el parámetro de velocityMultiplierDelta, el cual, permite mover las imágenes de fondo (capas) con una velocidad más rápida, cuanto más "cerca" esté la imagen:
lib/main.dart
Future<ParallaxComponent> bgParallax() async {
ParallaxComponent parallaxComponent = await loadParallaxComponent([
***
baseVelocity: Vector2(10, 0),
velocityMultiplierDelta: Vector2(1.1, 0));
}Con esto, tendremos el mismo efecto pero ahora las capas se desplazan a diferentes velocidades; para que entiendas el funcionamiento de este argumento, prueba colocar valores más altos y veras que mientras más cerca esté la capa, más rápida se moverá.
Crear una clase componente para el parallax
Podemos crear el equivalente del parallax con una clase componente quedando como:
lib\background\candy_background.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/parallax.dart';
import 'package:parallax06/main.dart';
class CandyBackground extends ParallaxComponent {
@override
FutureOr<void> onLoad() async {
parallax = await gameRef.loadParallax([
ParallaxImageData('layer06_sky.png'),
ParallaxImageData('layer05_rocks.png'),
ParallaxImageData('layer04_clouds.png'),
ParallaxImageData('layer03_trees.png'),
ParallaxImageData('layer02_cake.png'),
ParallaxImageData('layer01_ground.png'),
],
baseVelocity: Vector2(10, 0),
velocityMultiplierDelta: Vector2(1.1, 1.1));
return super.onLoad();
}
}El gameRef permite obtener una referencia del juego (la clase MyGame definida en el main); en cuanto al main:
lib/main.dart
import 'package:flutter/material.dart';
import 'package:flame/game.dart';
import 'package:parallax06/background/candy_background.dart';
class MyGame extends FlameGame {
@override
void onLoad() async {
super.onLoad();
add(CandyBackground());
}Y con esto tenemos el mismo resultado que el de antes.
CameraComponent en Flame para seguir componentes en Flutter y uso de World
Actualmente tenemos un fondo más grande que el que se puede mostrar en la ventana, dando como resultado de que existe una gran parte del mismo que no puede ser visible; para poder aprovechar este fondo no visible y que se muestre a medida que el player se desplaza, tenemos que colocar un seguimiento de la cámara al player.
El concepto de cámara se ha trabajado en cualquier motor de videojuegos como Unity o Unreal o software de creación de contenido en 3D/2D como Blender debe de ser conocido por ti; pero en esencia, la cámara no es más que un mecanismo por el cual podemos observar una parte del mundo, en donde el mundo no es más que todos los componentes que están dentro del juego; y mediante coordenadas, podemos desplazar este observador.
En este tipo de juegos, usualmente la cámara sigue al player y con esto, podemos visualizar partes del fondo que no son visibles a medida que el player se desplaza por el mundo.
Puedes obtener más información al respecto en:
https://docs.flame-engine.org/latest/flame/camera_component.html
En Flame tenemos un componente llamado CameraComponent con la cual, podemos indicar el área de visualización.
World
El componente World, tal cual indica su nombre, es un componente que puede ser usado para albergar todos los demás componentes que conforman el mundo de juego. En el caso del juego que estamos implementando sería:
- El background.
- Los tiles.
- El player.
- Los meteoritos.
Todos estos componentes, deben ser agregados en una instancia del componente World para que se encuentren en la misma capa y con esto, poder interactuar con el player.
Usar un componente World es tan sencillo como, crear la instancia:
final world = World();
Y agregar en Flame:
add(world);Y en este punto, todos los componentes que formen parte del juego, deben ser agregados a la instancia de world:
world.add(<COMPONENT>);El componente de World y CameraComponent están diseñados para trabajar en conjunto; una instancia de la clase CameraComponent "mira" el mundo/world y esta es la razón por la cual introducimos el componente world junto con el componente de cámara.
Usualmente no es obligatorio usar un componente World para nuestros juegos en Flame, ya que, podemos agregar los componentes de nuestro juego directamente en la instancia de Flame:
add(player);
O en los mismos componentes, pero, en este tipo de juegos que tenemos un mundo más grande del que puede entrar en la pantalla, es necesario usar una cámara que siga o vigile a nuestro player.
CameraComponent
CameraComponent como comentamos antes, es el componente empleado para observar al mundo, tenemos varias propiedades para establecer en este componente y que "mire" exactamente a donde queramos y que siga a un componente:
- El Viewport es una ventana a través de la cual se ve el mundo. Esa ventana tiene cierto tamaño, forma y posición en la pantalla.
- El Viewfinder es responsable de saber qué ubicación en el mundo del juego subyacente estamos mirando actualmente. El Viewfinder también controla el nivel de zoom y el ángulo de rotación de la vista; esta propiedad es clave en estos juegos ya que, es la que se actualiza para "mirar" al jugador.
Como cualquier otro juego, la cámara debe de observar al jugador que es el que se va desplazando por el mundo, aunque, por suerte esta actualización se realiza de manera automática usando la función de follow(), que recibe como parámetro, un componente a observar y actualizar la posición de la cámara mientras este se desplaza por la pantalla:
cameraComponent.follow(player);El Viewfinder, permite personalizar aspectos en la visualización como el anchor, con el cual, podemos especificar el centro de la cámara; es decir, nuestro mundo luce como el siguiente:
La cámara debe de estar centrada en la esquina inferior izquierda, es decir, en el bottomLeft:
Y para esto:
cameraComponent.viewfinder.anchor = Anchor.bottomLeftImplementar el componente de cámara
Aclarado el funcionamiento del componente de mundo y cámara, vamos a implementar la lógica en nuestra aplicación:
class MyGame extends FlameGame *** {
***
late PlayerComponent player;
final world = World();
late final CameraComponent cameraComponent;
@override
void onLoad() {
var background = Background();
add(world);
world.add(background);
background.loaded.then(
(value) {
player = PlayerComponent();
cameraComponent = CameraComponent(world: world);
cameraComponent.follow(player);
cameraComponent.setBounds(Rectangle.fromLTRB(0, 0, background.size.x, background.size.y)));
// cameraComponent.viewfinder.anchor = Anchor.bottomLeft;
cameraComponent.viewfinder.anchor = const Anchor(0.1, 0.9);
add(cameraComponent);
cameraComponent.world.add(player);
},
);
add(ScreenHitbox());
}
}Puntos claves
Definimos y agregamos a Flame el objeto world:
final world = World();
***
add(world);Al usar el componente de cámara, es necesario emplear un objeto world:
cameraComponent = CameraComponent(world: world);Con una instancia del componente anterior, podemos personalizar varios aspectos sobre la visualización como lo es, que la cámara siga a un componente mediante la función de follow() la cual recibe como parámetros al componente a seguir:
cameraComponent.follow(player);El centro de la cámara, se define en la esquina inferior izquierda, pero, si la dejamos de esta manera:
cameraComponent.viewfinder.anchor = Anchor.bottomLeft; // Anchor(0, 1);Al posicionar la cámara que está siguiendo al player, veremos que el player es visible en parte:
Esto se debe a que la cámara "sigue" al anchor del player, el cual está alienado en el centro:
PlayerComponent({required this.mapSize}) : super() {
anchor = Anchor.center;
debugMode = true;
}Para evitar este comportamiento, podemos especificar valores numéricos para el anchor de la cámara que no sean tan restringidos como el valor anterior.
cameraComponent.viewfinder.anchor = const Anchor(0.1, 0.9);
Y con esto tenemos una visualización completa del player:
Sumado a lo anterior, también podemos colocar restricciones sobre la visualización de la cámara (el área visible por la cámara, que en este caso, es justamente es el tamaño de la imagen o mapa); es importante mencionar, que el área de visualización corresponde a un rectángulo con el tamaño del fondo; por lo tanto:
cameraComponent.setBounds(Rectangle.fromLTRB(0, 0, background.size.x, background.size.y)));Y desde el main, pasamos la posición de la cámara:
lib\main.dart
@override
void update(double dt) {
if (elapsedTime > 1.0) {
Vector2 cp = cameraComponent.viewfinder.position;
cp.y = cameraComponent.viewfinder.position.y -
cameraComponent.viewport.size.y;
world.add(MeteorComponent(cameraPosition: cp));
elapsedTime = 0.0;
}
elapsedTime += dt;
super.update(dt);
}Como puedes apreciar, no tenemos de manera directa la posición de la cámara, por lo tanto, debemos de hacer un cálculo; como comentamos anteriormente, mediante el Viewfinder:
cameraComponent.viewfinder
Tenemos la posición de la cámara, la cual se va actualizando mediante el player se va moviendo por pantalla, y podemos obtener con:
cameraComponent.viewfinder.position
Pero, esto nos da la posición de la cámara en la esquina inferior, es decir, la parte de abajo, y necesitamos generar los meteoritos en la parte de arriba, por tal motivo, le restamos el alto de la cámara que podemos obtener con:
cameraComponent.viewport.size.yEste material forma parte de mi curso y libro completo sobre el desarrollo de juegos en 2D con Flutter y Flame.
SpriteAnimationTicker en Flame para controlar una animación en Flutter
Un AnimationTicker es una técnica utilizada en varias bibliotecas de animación y permiten controlar una animación; en el caso de Flame, también permiten escuchar los estados de la animación; por ejemplo, cuando se completa la animación:
animationTicker.onComplete = () {
***
}
};
Cuando se ejecuta un frame de la animación:
animationTicker.onFrame = (index) {
***
}
};Todo esto se hace mediante la clase de SpriteAnimationTicker de Flame; para el juego que estamos implementando, es necesario conocer cuando acaba la animación de "muriendo" para reiniciar la partida; para esto, podemos emplear cualquiera de los listeners mostrados anteriormente.
Primero, necesitamos inicializar el ticker:
lib/components/player_component.dart
class PlayerComponent extends Character {
***
late SpriteAnimationTicker deadAnimationTicker;
@override
void onLoad() async {
***
deadAnimationTicker = deadAnimation.createTicker();
}
@override
void update(double dt) {
***
deadAnimationTicker.update(dt);
super.update(dt);
}
}Con la función de createTicker() creamos un ticker (SpriteAnimationTicker) sobre la animación que vamos a controlador; en el caso del juego que estamos implementando, nos interesa es detectar cuando termina la animación de muriendo, que se va a ejecutar únicamente cuando el player se queda sin vidas y al terminar la animación, se reinicia el nivel. La razón de que es inicializado la propiedad de deadAnimationTicker en el onLoad() y no cuando se emplee la animación de deadAnimation (al quedarse el player sin vidas) es que es necesario actualizar el ticker en la función de upload() según el ciclo de vida del ticker:
deadAnimationTicker.update(dt)
Con el ticket, se crea un listener para detectar cuando termina la animación de ejecutarse; para ello, podemos ejecutar el listener de onComplete():
deadAnimationTicker.onComplete = () {
// TODO
};O el de onFrame(), que se ejecuta por cada Frame, pero, preguntando si el frame actual es el último:
deadAnimationTicker.onFrame = (index) {
if (deadAnimationTicker.isLastFrame) {
// TODO
}
};Finalmente, el código completo queda como:
lib/components/player_component.dart
class PlayerComponent extends Character {
void reset({bool dead = false}) async {
game.overlays.remove('Statistics');
game.overlays.add('Statistics');
velocity = Vector2.all(0);
game.paused = false;
blockPlayer = true;
inviciblePlayer = true;
movementType = MovementType.idle;
if (dead) {
animation = deadAnimation;
deadAnimationTicker = deadAnimation.createTicker();
deadAnimationTicker.onFrame = (index) {
// print("-----" + index.toString());
if (deadAnimationTicker.isLastFrame) {
animation = idleAnimation;
position =
Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
}
};
deadAnimationTicker.onComplete = () {
if (animation == deadAnimation) {
animation = idleAnimation;
position =
Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
}
};
} else {
animation = idleAnimation;
position = Vector2(spriteSheetWidth / 4, mapSize.y - spriteSheetHeight);
size = Vector2(spriteSheetWidth / 4, spriteSheetHeight / 4);
}
game.colisionMeteors = 0;
game.addConsumibles();
//position = Vector2(spriteSheetWidth / 4, 0);
}
}Este material forma parte de mi curso y libro completo sobre el desarrollo de juegos en 2D con Flutter y Flame.
Implementar un Joystick
Hasta este momento, hemos usado el teclado como fuente principal para interactuar con el juego, pero, esto trae como inconveniente de que solo podríamos emplear dispositivos que cuenten con un teclado, como sería un PC o Mac, dejando a la aplicación fuera de otros dispositivos como teléfonos con Android e iOS; para que este tipo de aplicación, en la cual es necesario desplazar al player en todos los ejes (o solamente de izquierda a derecha y viceversa, como en el caso del juego del dinosaurio) podemos implementar un joystick virtual; Flame dispone de este tipo de componentes ya listos para usar en nuestra aplicación.
Un joystick virtual luce como el siguiente:
En definitiva, puedes ver que constan de dos componentes:
- El círculo translúcido que indica el área de movimiento de la palanca/mando.
- El circulo de color solido que se encuentra contenido en el anterior que indica el control para que el usuario pueda mover el joystick virtual, este corresponde a la palanca o mando.
Comencemos implementando una clase joystick como la siguiente:
lib\components\hud\joystick.dart
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Joystick extends JoystickComponent {
Joystick(
{required PositionComponent knob,
required PositionComponent background,
required EdgeInsets margin})
: super(knob: knob, background: background, margin: margin);
}La clase JoystickComponent existe en la API de Flame y para implementar la misma, tenemos que definir dos propiedades fundamentales:
- knob, que hace referencia al mando.
- background, que es el área en la cual se puede desplazar el mando.
Ambos son PositionComponent por lo tanto, pueden ser un Sprite o una figura geométrica como un círculo.
Existen otras propiedades para personalizar el joystick como puedes consultar en la documentación oficial, pero, estas son las principales o claves.
Finalmente, creamos otro archivo y clase, con el cual, implementaremos la clase Joystick personalizada definida anteriormente y que llamaremos como Hud de heads-up display que es un término común en los videojuegos para referirse al apartado de iconos, mapas, vida, etc sobre el personaje. En esta clase implementamos la propiedad de knob y background del joystick como un par de círculos con colores, además, de posicionar el mismo en la pantalla mediante el margen:
lib\components\hud\hud.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/palette.dart';
import 'package:flutter/widgets.dart';
import 'package:parallax06/hud/joystick.dart';
class HudComponent extends PositionComponent {
late Joystick joystick;
@override
void onLoad() {
final joystickKnobPaint = BasicPalette.red.withAlpha(200).paint();
final joystickBackgroundPaint = BasicPalette.black.withAlpha(100).paint();
joystick = Joystick(
knob: CircleComponent(radius: 30.0, paint: joystickKnobPaint),
background:
CircleComponent(radius: 100, paint: joystickBackgroundPaint),
margin: const EdgeInsets.only(left: 40, top: 100));
add(joystick);
}
}Finalmente, desde el main, agregamos una instancia de la clase anterior:
lib\main.dart
import 'package:parallax06/hud/hud_component.dart';
***
class MyGame extends FlameGame
with HasKeyboardHandlerComponents, HasCollisionDetection {
***
late HudComponent hudComponent;
@override
void onLoad() async {
***
hudComponent = HudComponent();
add(hudComponent);
}
***
}El componente de Joystick de Flame, tiene todo lo necesario para implementar la movilidad en nuestros componentes; mediante el tipo enumerado:
joystick.direction
Conocemos el estado del knob o mando:
enum JoystickDirection {
up,
upLeft,
upRight,
right,
down,
downRight,
downLeft,
left,
idle,
}También tenemos otras opciones que podemos usar para determinar el desplazamiento del knob; es decir, mientras más lejos el usuario lleve el mando:
Más alto será el vector:
[-1.0,-67.0] (joystick.delta)Y mientras más cerca esté el knob del centro (mientras menos se desplace):
Más bajo será el vector:
[-1.0,-21.0] (joystick.delta)Con este vector también tenemos el ángulo, es decir que si movemos el knob hacia arriba, tendremos un valor negativo en el Y:
[X,-72.0] (joystick.delta)Y positivo si va hacia abajo:
[X,72.0] (joystick.delta)Y la misma lógica se aplica para el eje de las X. Con esto, podemos usar de manera directa esta propiedad para desplazar al player sin necesidad de hacer comparaciones adicionales; finalmente, el desplazamiento del player mediante el joystick queda como:
lib\components\player_component.dart
class PlayerComponent extends Character with HasGameRef<MyGame> {
***
final double _maxVelocity = 5.0;
@override
void update(double dt) {
***
_movePlayerJoystick(dt);
}
void _movePlayerJoystick(double delta) {
if (gameRef.hudComponent.joystick.direction != JoystickDirection.idle) {
position.add(gameRef.hudComponent.joystick.delta * _maxVelocity * delta);
}
}
} Es importante mencionar que podemos tener habilitados (como es el caso para nuestra aplicación) dos o más funciones para el desplazamiento, en nuestro proyecto sería el desplazamiento mediante teclado y también mediante el joystick.
Finalmente, al ejecutar la aplicación, tendremos:
Claro está, que el joystick lo puedes implementar en los juegos anteriores para utilizar el juego en otros dispositivos que no cuenten con teclados.
Este material forma parte de mi curso y libro completo sobre el desarrollo de juegos en 2D con Flutter y Flame.
GamePad en Flutter y Flame - Dualsense - Xbox
El GamePad es el mecanismo por excelencia para jugar a videojuegos y uno de los primeros elementos que se nos vienen a la cabeza cuando escuchamos la palabra videojuego o consola de videojuegos; utilizar este tipo de controles para nuestros juegos no es imposible; sin embargo, en Flame, no tenemos muchas opciones; en la documentación oficial, veremos que al momento en que se escribieron estas palabras, existe un apartado muy corto que menciona el uso del gamepad en Flame:
https://docs.flame-engine.org/latest/flame/inputs/other_inputs.html
El problema con el paquete que hacen mención:
https://github.com/flame-engine/flame_gamepad
Es que al momento en el que se escribieron estas palabras, lleva más de 3 años sin actualizar y en el desarrollo de software, que incluye el desarrollo de videojuegos, es un tiempo considerablemente largo, por lo tanto, personalmente no recomendaría su uso ya que pueden existir problemas de versiones de paquete con el proyecto actual lo que traería problemas en la implementación.
En Flutter (No directamente Flame, si no el framework conocido como Flutter) existen unos pocos plugins que podemos usar; por ejemplo este:
https://pub.dev/packages/gamepads
Al momento en el cual se escribieron estas palabras, el paquete se encuentra en desarrollo, por lo tanto, su documentación es muy escasa y su implementación no es del todo amigable como veremos en el apartado de la implementación; sin embargo, es un paquete que tiene potencial y está en constante desarrollo, por lo tanto es el que vamos a emplear; es importante mencionar que este paquete no está diseñado para Flame, al igual que el paquete de Sharedpreferences, pero, podemos usarlo sin problemas en un proyecto con Flutter y Flame; actualmente este paquete no está disponible para web o móvil.
Primeros pasos con el plugin gamepads
Comencemos agregando la dependencia para el paquete anterior:
pubspec.yaml
dependencies:
flutter:
sdk: flutter
***
gamepads:El cual cuenta con 2 clases principales:
- GamepadController: Representa un solo gamepad actualmente conectado.
- GamepadEvent: Es un evento que se construye al momento de que ocurre una entrada desde un gamepad, en otras palabras, al momento de presionar una tecla, se construye un evento de este tipo.
La clase GamepadEvent contiene las siguientes propiedades:
class GamepadEvent {
// El id asignado al gamepad que disparó el evento.
final String gamepadId;
// La marca de tiempo en la que se disparó el evento, en milisegundos.
final int timestamp;
// El [KeyType] de la tecla que se pulso.
final KeyType type;
// Un identificador del boton/analog pulsado.
final String key;
// El valor actual del boton/analog pulsado.
final double value;
}Las más importantes serían la de key, que indica la tecla presionada y la de value, con la cual, dependiendo del tipo de botón presionado, podemos conocer el ángulo o si fue presionado o liberado el botón.
El tipo de entrada pueden ser de dos tipos que es representado mediante un tipo enumerado llamado KeyType:
- analog: Las entradas analógicas tienen un rango de valores posibles dependiendo de qué tan lejos/fuerte se presionen (representan palancas analógicas, disparadores traseros, algunos tipos de d-pads (crucetas), etc.)
- button: Los botones tienen solo dos estados, presionado (1.0) o no (0.0).
Definir un listeners para detectar teclas presionadas
Para este plugin, tenemos dos usos principales, conocer la cantidad de controles conectados; para ello:
gamepads = await Gamepads.list()O crear un listeners que permite escuchar las teclas presionadas sobre todos los gamepads conectados:
Gamepads.events.listen((GamepadEvent event) { });Como puedes apreciar, aqui tenemos el evento GamepadEvent que tiene la estructura mencionada antes; como puedes apreciar, el uso del paquete es extremadamente simple, ya que, no requiere de establecer un control conectado o algo similar, automáticamente tenemos un listeners de todos los controles conectados al dispositivo; desde el componente de player, colocamos el listener para el gamepad:
import 'package:gamepads/gamepads.dart';
***
@override
Future<void>? onLoad() async {
// final gamepads = await Gamepads.list();
// print('Gamepads' + gamepads.toString());
Gamepads.events.listen((GamepadEvent event) {
print("gamepadId" + event.gamepadId);
print("timestamp" + event.timestamp.toString());
print("type " + event.type.toString());
print("key " + event.key.toString());
print("value " + event.value.toString());
});
}Como recomendación conecta algún dispositivo a tu PC/Mac, mediante Bluetooth o su cable USB, inicia la aplicación y empieza a presionar algunos botones en el gamepad, incluyendo sus joystick y crucetas y analiza la respuesta, verás que algunos son propiamente "button" y otros son "analog" en base a la clasificación por tipos mencionada anteriormente, en el caso de los botones, verás que el listener anterior se ejecuta cuando se presiona y libera el botón.
En base a pruebas realizadas desde un dispositivo Windows 11 con un control de Xbox S|X, tenemos la siguiente implementación para detectar la tecla "A":
import 'package:gamepads/gamepads.dart';
***
@override
Future<void>? onLoad() async {
// final gamepads = await Gamepads.list();
// print('Gamepads' + gamepads.toString());
Gamepads.events.listen((GamepadEvent event) {
// print("gamepadId" + event.gamepadId);
// print("timestamp" + event.timestamp.toString());
// print("type " + event.type.toString());
// print("key " + event.key.toString());
// print("value " + event.value.toString());
if (event.key == 'button-0' && event.value == 1.0) {
print('rotar');
}
});
}Y para la cruceta, detectar el desplazamiento:
import 'package:gamepads/gamepads.dart';
***
@override
Future<void>? onLoad() async {
Gamepads.events.listen((GamepadEvent event) {
if (event.key == 'button-0' && event.value == 1.0) {
print('jump');
} else if (event.key == 'pov' && event.value == 0.0) {
// up
print('up');
} else if (event.key == 'pov' && event.value == 4500.0) {
// up - right
print('up - right');
} else if (event.key == 'pov' && event.value == 9000.0) {
// right
print('right');
} else if (event.key == 'pov' && event.value == 13500.0) {
// buttom right
print('buttom right');
} else if (event.key == 'pov' && event.value == 18000.0) {
// buttom
print('buttom');
} else if (event.key == 'pov' && event.value == 22500.0) {
// buttom left
print('buttom left');
} else if (event.key == 'pov' && event.value == 27000.0) {
// left
print('left');
} else if (event.key == 'pov' && event.value == 31500.0) {
// top left
print('top left');
}
});
}El caso de la cruceta es interesante ya que, según el plugin utilizado, corresponde a un mismo botón, por lo tanto, al presionar "flecha arriba" tenemos un valor de 0.0, al presionar "flecha abajo" tenemos un valor de 18000, como puedes ver, son valores que corresponden a un espacio de 360 grados; por lo tanto, podemos personalizar la experiencia como queramos; finalmente, la implementación usando la cruceta queda como:
import 'package:gamepads/gamepads.dart';
***
@override
FutureOr<void> onLoad() async {
// var gamepads = await Gamepads.list();
// print('*******');
// print(gamepads.toString());
Gamepads.events.listen((GamepadEvent event) {
if (event.key == 'button-0' && event.value == 1.0) {
// print('Rotate');
_rotate();
} else if (event.key == 'pov' && event.value == 0) {
// up
// print('up');
movementType = MovementType.up;
} else if (event.key == 'pov' && event.value == 4500) {
// up right
// print('up right');
} else if (event.key == 'pov' && event.value == 9000) {
// right
// print('right');
movementType = MovementType.right;
} else if (event.key == 'pov' && event.value == 13500) {
// buttom right
// print('buttom right');
} else if (event.key == 'pov' && event.value == 18000) {
//bottom
// print('bottom');
movementType = MovementType.down;
} else if (event.key == 'pov' && event.value == 22500) {
// buttom left
// print('buttom left');
} else if (event.key == 'pov' && event.value == 27000) {
// left
// print('left');
movementType = MovementType.left;
} else if (event.key == 'pov' && event.value == 31500) {
// top left
// print('top left');
} else {
movementType = MovementType.idle;
//
}
});
***
}Claro está, es posible que estos valores cambien si usas otro tipo de control o en versiones futuras del plugin por lo tanto, se recomienda al lector que adecue el script anterior a sus necesidades.
Con esta implementación, tenemos otras entradas para los juegos que hemos implementado en este libro y que por supuesto puedes adaptar a otros como el juego del dinosaurio.
Y que pasa si quieres crear un juego tipo Angry Birds, con motor de física con el de Box2D, aprende a desarrollar con Flutter, Flame y Forge 2d.
Acepto recibir anuncios de interes sobre este Blog.
Te doy las bases para que crees tu primer juego en 2D con Flutter y Flame, sobre el entorno, instalación, tus primeras pruebas, Hola Mundo, Trabajar con Sprite Sheets, uso de joystic virtuales, configurar el control de Xbox o Dualsense y mucho más.