Un tutorial completo de Flame (o Cómo hacer juegos con Flutter)

Introducción

¡Hola a todos! Soy Luan, y bienvenido a este primer tutorial completo de Flame.

Flame es un motor de juego Flutter minimalista que proporciona algunos módulos para crear un juego basado en Canvas.

En este tutorial, vamos a crear un juego muy simple, donde los cuadros caerán y el objetivo es destruirlos antes de que toquen la parte inferior de la pantalla.

Así es como se verá el juego

Puede verificar el juego usted mismo para ver qué estamos haciendo para instalar este APK o instalarlo desde Play Store.

Esto nos permitirá cubrir todas las características proporcionadas por el marco y demostrar cómo realizar las operaciones más básicas: renderizado, sprites, audio, texto, animaciones y más.

¿Por qué seleccionamos este juego? Además de ser muy simple pero completo para explorar el marco, un buen factor es que tenemos los recursos para hacerlo.

Para este juego, vamos a utilizar los siguientes recursos: un sprite de caja, un sprite de explosión (con animación), un sonido de explosión, un sonido de 'señorita' (para cuando no se golpea la caja), una música de fondo y también un bonita fuente para representar la partitura.

Es difícil encontrar buenos recursos disponibles comercialmente en Internet, pero encontramos todo (excepto la fuente) en este increíble sitio. Son realmente geniales, absolutamente recomendables.

Por ahora, las hojas de sprites no son compatibles, por lo que, primero, necesitamos convertir todos los recursos a formatos apropiados. Además, necesitamos convertir todo el audio para MP3 (también se admite OGG; WAV no). Una vez hecho todo esto, puede descargar un paquete con todo lo que necesitará en cuanto a recursos aquí.

También vale la pena mencionar, todo lo descrito aquí está comprometido, como una versión completa totalmente funcional, en GitHub. Siempre puedes echar un vistazo cuando tengas dudas. Además, el ejemplo se creó siguiendo estos pasos exactos y tomando confirmaciones frecuentes como instantáneas. A lo largo del tutorial, vincularé los commits específicos que hacen avanzar el repositorio a la etapa en cuestión. Esto le permitirá establecer puntos de control y examinar el código anterior para ver cualquier cosa que no esté clara.

Una última cosa; Puede consultar la documentación completa de Flame aquí. Si tiene alguna pregunta, sugerencia, error, no dude en abrir un problema o contácteme.

Aparte de eso, también necesitarás Flutter y Dart instalados. Si le conviene, también puede tener IntelliJ IDEA, que es un muy buen lugar para escribir su código. Para instalar estas cosas, puede consultar una gran cantidad de tutoriales, como este.

Lo esencial

Entonces, por ahora, supondré que tienes todo listo. Entonces, solo ejecuta lo siguiente y ábrelo.

Lo primero que debe hacer es agregar la dependencia de la llama. Vaya a su archivo pubspec.yaml y asegúrese de que la clave de dependencias enumere la llama:

Aquí estamos utilizando la última versión, 0.5.0, pero también puede elegir una nueva si está disponible.

Ahora, su archivo main.dart ya tiene muchas cosas: un método "principal" que debe mantenerse; y una llamada posterior al método runApp. Este método toma Widgets y otros componentes de Flutter que se usan para crear pantallas de aplicaciones. Como estamos creando un juego, vamos a dibujar todo en el lienzo y no usaremos estos componentes; así que quita todo eso.

Nuestro método principal ahora está vacío, y vamos a agregar dos cosas; primero, alguna configuración:

La importación flame.dart da acceso a la clase estática Flame, que es solo un soporte para varias otras clases útiles. Usaremos más de esto más adelante. Por ahora, estamos llamando a dos métodos en esta clase Flame.

El último se explica por sí mismo, deshabilita parte del registro desde el complemento de reproductores de audio. Ahora, en breve agregaremos audio al juego, y si no funciona, ahí es donde debes comentar para solucionar el problema. Pero llegaremos allí eventualmente.

La primera línea es más compleja. Básicamente, algunas funciones cruciales de Flutter están ausentes porque no estamos usando el método runApp. Esta llamada enableEvents hace una pequeña solución para obtener lo que es esencial para cada aplicación, sin la necesidad de usar Widgets.

Finalmente, necesitamos comenzar nuestro juego. Para hacer eso, vamos a agregar una clase más a la lista de importación, la clase Juego. Esta clase proporciona la abstracción necesaria para crear cualquier juego: un bucle de juego. Debe subclasificarse para que se requiera implementar la base de cualquier juego: un método de actualización, que se llama siempre que sea conveniente y toma la cantidad de tiempo transcurrido desde la última actualización, y un método de representación, que debe saber cómo dibujar el estado actual del juego. El funcionamiento interno del bucle se deja para que lo resuelva la clase de juego (puede echar un vistazo, por supuesto, es muy simple), y solo necesita llamar a start, bueno, start.

Punto de control: 599f809

En este momento, el render no hace nada, por lo tanto, si lo inicia, debería ejecutarse, pero le dará una pantalla en negro. ¡Dulce! Entonces obtuvimos una aplicación funcional sin widgets y demás, y un lienzo en blanco para comenzar a dibujar nuestra aplicación.

Formas de renderizado

¿Y cómo se hace el dibujo? Dibujemos un rectángulo simple para verlo en acción. Agregue lo siguiente a su método de renderizado:

Aquí, como puede ver, definimos un rectángulo, basado en las posiciones de la pantalla. La siguiente imagen muestra cómo se orientan las reglas. Básicamente, el origen está en la esquina superior izquierda, y el eje aumenta a la derecha y hacia abajo.

Además, tenga en cuenta que la mayoría de los métodos de dibujo toman una pintura. Una pintura no es solo un solo color, sino que podría ser un Degradè u otras texturas. Normalmente, querrás un color sólido o ir directamente a un Sprite. Así que solo establecemos el color dentro de la pintura en una instancia de Color.

El color representa un solo color ARGB; lo crea con un número entero que puede escribir en hexadecimal para facilitar la lectura; está en el formato A (alfa, transparencia, normalmente 0xFF), y luego dos dígitos para R, G y B en ese orden.

También hay una colección de colores con nombre; Sin embargo, está dentro del paquete de material. Solo tenga cuidado de importar solo el módulo Colors, para no usar accidentalmente otra cosa del paquete de material.

Entonces, genial, ¡ahora tenemos un cuadrado!

Punto de control: 4eff3bf

¡También sabemos cómo funcionan las reglas, pero no sabemos las dimensiones de la pantalla! ¿Cómo vamos a dibujar algo en las otras tres esquinas sin esta información? No temas, ya que Flame tiene un método para obtener la dimensión real de la pantalla (eso es porque hay un problema documentado al respecto.

Básicamente, el método asíncrono

Tenga en cuenta que la palabra clave en espera, al igual que en JavaScript, solo puede usarse en una función asíncrona, así que asegúrese de hacer su asíncrona principal (a Flutter no le importará).

El siguiente punto de control buscará las dimensiones una vez en el método principal y las almacenará dentro de nuestra clase de Juego, porque las necesitaremos repetidamente.

Punto de control: a1f9df3

Renderizando Sprites

Finalmente, sabemos cómo dibujar cualquier forma, en cualquier lugar de la pantalla. ¡Pero queremos sprites! El siguiente punto de control agrega algunos de los activos que vamos a utilizar en la carpeta de activos apropiada:

Punto de control: 92ebfd9

Y el siguiente hace una cosa crucial que no puedes olvidar: agrega todo a tu archivo pubsepc.yaml. Cuando se está creando su código, Dart solo agrupará los recursos que especifique allí.

Punto de control cf5975f

Finalmente, estamos listos para dibujar nuestro sprite. La forma básica en que Flame le permite hacer eso es exponer un método Flame.images.load ('ruta desde dentro de la carpeta de imágenes') que devuelve una promesa para la imagen cargada, que luego se puede dibujar con el método canvas.drawImage.

Sin embargo, en el caso de dibujar una caja, es muy simple de hacer, porque podemos usar la clase SpriteComponent, de esta manera:

La clase abstracta Component es una interfaz con dos métodos, render y actualización, al igual que nuestro juego. La idea es que el Juego pueda estar compuesto por Componentes que tengan sus métodos de renderizado y actualización llamados dentro de los métodos del Juego. SpriteComponent es una implementación que representa un sprite, dado su nombre y tamaño (cuadrado o rectangular), la posición (x, y) y el ángulo de rotación. Reducirá o expandirá adecuadamente la imagen para que se ajuste al tamaño deseado.

En este caso, cargamos el archivo "crate.png", que debe estar en la carpeta assets / images, y tener una clase Crate que dibuja cuadros de 128x128 píxeles, con ángulo de rotación 0.

Luego agregamos una propiedad Crate al juego, la instanciamos en la parte superior de la pantalla, centrada horizontalmente, y la procesamos en nuestro bucle de juego:

¡Esto hará que nuestra caja! ¡Increíble! El código es bastante sucinto y fácil de leer también.

Punto de control 7603ca4

Estado de actualización en el bucle del juego

Nuestra caja está casi detenida en el aire. ¡Queremos moverlo! Cada caja va a caer con velocidad constante, hacia abajo. Necesitamos hacer eso en nuestro método de actualización; solo cambia la posición Y de la caja individual que tenemos:

Este método lleva el tiempo (en segundos) que tomó desde la última actualización. Normalmente esto será muy pequeño (orden de 10 ms). Entonces la VELOCIDAD es una constante en esas unidades; en nuestro caso, VELOCIDAD = 100 píxeles / segundo.

Punto de control: 452dc40

Manejo de entrada

¡Hurra! Las cajas se caen y desaparecen, pero no puedes interactuar con ellas. Agreguemos un método para destruir las cajas que tocamos. Para eso, vamos a utilizar un evento de ventana. El objeto de ventana está disponible en todos los proyectos de Flutter a nivel mundial y tiene algunas propiedades útiles. Vamos a registrar en el método principal un evento onPointerDataPacket, es decir, cuando el usuario toca la pantalla:

Simplemente extraemos la coordenada (x, y) del clic y la pasamos directamente a nuestro Juego; de esa manera, el juego puede manejar el clic sin preocuparse por los detalles de los eventos.

Para hacer las cosas más interesantes, refactoricemos también la clase Juego para tener una Lista de cajas, en lugar de una sola. Después de todo, eso es lo que queremos. Reemplazamos los métodos de representación y actualización con forEach sobre las cajas, y el nuevo método de entrada se convierte en:

Punto de control: 364a6c2

Renderizando Sprites Múltiples

Hay un punto crucial que mencionar aquí, y es sobre el método de renderizado. Cuando renderizamos un cajón, el estado del lienzo se traduce y gira arbitrariamente para permitir el dibujo. Como vamos a dibujar varias cajas, debemos restablecer el lienzo entre cada dibujo. Eso se hace con los métodos guardar, que guarda el estado actual y restaurar, que restaura el estado guardado anteriormente, eliminándolo.

Este es un comentario importante, ya que es la fuente de muchos errores extraños. ¿Quizás deberíamos hacer eso automáticamente en cada render? No sé, ¿qué te parece?

¡Ahora queremos más cajas! ¿Como hacer eso? Bueno, el método de actualización puede ser nuestro temporizador. Por lo tanto, queremos que se agregue una nueva caja a la lista (generada) cada segundo. Entonces creamos otra variable en la clase Juego, para acumular los tiempos delta (t) de cada llamada de actualización. Cuando supera 1, se restablece y se genera una nueva caja:

No olvides mantener la actualización anterior, para que las cajas no dejen de caerse. Además, cambiamos la velocidad a 250 píxeles / segundo, para hacer las cosas un poco más interesantes.

Punto de control: 3932372

Renderizar animaciones

Esto debería ser un GIF, ¿verdad? ¡Estamos trabajando en una configuración para mejores capturas de pantalla y GIF para este tutorial!

Ahora conocemos los conceptos básicos de Sprite Handling and Rendering. Pasemos al siguiente paso: ¡Explosiones! ¿Qué juego es bueno sin ellos? La explosión es una bestia de un tipo diferente, porque presenta una animación. Las animaciones en Flame se realizan simplemente renderizando diferentes cosas en el render de acuerdo con la marca actual. De la misma manera que agregamos un temporizador hecho a mano para generar cuadros, agregaremos una propiedad lifeTime para cada Explosión. Además, Explosion no se heredará de SpriteComponent, ya que para este último solo puede tener un Sprite. Vamos a extender la superclase, PositionComponent e implementar el renderizado con Flame.image.load.

Como cada explosión tiene muchos cuadros, y deben dibujarse de manera receptiva, vamos a precargar cada cuadro una vez y guardar en una variable estática dentro de la clase Explosión; al igual que:

Tenga en cuenta que cargamos cada uno de nuestros 7 cuadros de animación, en orden. Luego, en el método de renderizado, hacemos una lógica simple para decidir qué marco dibujar:

Tenga en cuenta que estamos dibujando "a mano", usando drawImageRect, como se explicó anteriormente. Este código es similar a lo que hace SpriteComponent debajo del capó. También tenga en cuenta que, si la imagen no está en la matriz, no se dibuja nada, por lo que después de TIME segundos (lo configuramos en 0,75 o 750 ms), no se muestra nada.

Está bien, pero no queremos seguir contaminando nuestro conjunto de explosiones con explosiones explotadas, por lo que también agregamos un método destroy () que devuelve, en función del tiempo de vida, si debemos destruir el objeto de la explosión.

Finalmente, actualizamos nuestro Juego, agregando una Lista de Explosiones, presentándolos en el método de representación y actualizándolos luego en el método de actualización. Deben actualizarse para aumentar su vida útil. También nos tomamos este tiempo para refactorizar lo que estaba anteriormente en el método Game.update, es decir, hace que las cajas caigan, para estar dentro del método Crate.update, ya que es responsabilidad del Crate. Ahora la actualización del juego solo delega a otros. Finalmente, en la actualización, necesitamos eliminar de la lista lo que ha sido destruido. Para eso, List proporciona un método muy útil, removeWhere:

Ya lo usamos en el método de entrada, para eliminar los cuadros que se tocaron de la matriz. También es donde crearemos una explosión.

Echa un vistazo al punto de control para más detalles.

Punto de control: d8c30ad

Reproducción de audio

¡En el próximo commit, finalmente vamos a reproducir algo de audio! Para hacerlo, debe agregar el archivo a la carpeta de activos, dentro de assets / audio /. Debe ser un archivo MP3 o un archivo OGG. Luego, en cualquier parte de su código, ejecute:

Donde filename.mp3 es el nombre del archivo dentro. En nuestro caso, reproduciremos el sonido explosion.mp3 cuando hagamos clic en un cuadro.

Además, comencemos a otorgar puntuación. Agregamos una variable de puntos para mantener el número actual de puntos. Comienza con cero; obtenemos 10 puntos por casilla en la que hacemos clic y perdemos 20 cuando la casilla toca el suelo.

Ahora tenemos la obligación de tratar con cajas fuera de control. Según lo que le hicimos a la clase Explosión, agregamos un método de destrucción para la caja, que devolverá si están fuera de la pantalla. ¡Esto está empezando a convertirse en un patrón! Si se destruye, eliminamos de la matriz y ajustamos los puntos.

Por ahora, la puntuación funciona, pero no se muestra en ningún lado; eso vendrá pronto

El audio no funcionará en este próximo punto de control porque olvidé agregar los archivos y ponerlos en pubspec.yaml; eso se hace en la siguiente confirmación.

Punto de control: 43a7570

¡Ahora queremos más sonidos! Los reproductores de audio (tenga en cuenta lo que usa la Llama) le permiten reproducir múltiples sonidos a la vez, como ya habrá notado si hizo clic en el frenesí, pero ahora usemos eso para nuestra ventaja, reproduciendo un sonido de falta, cuando la caja toca el suelo (método de destrucción de la caja), y una música de fondo.

Para reproducir la música de fondo en un bucle, use el método de bucle, que funciona igual que antes:

En esta confirmación, también corregimos la condición de destrucción de los Cajones, que perdimos en la confirmación anterior (porque no había forma de saberlo, ahora hay sonido).

Punto de control: f575150

Renderizando texto

Ahora que tenemos todo el audio que queremos (fondo, música, efectos de sonido, MP3, OGG, loop, simultáneamente), vamos a ver el renderizado de texto. Necesitamos ver esa puntuación, después de todo. La forma en que esto se hace "a mano" es crear un objeto Paragraph y usar el drawParagraph del Canvas. Requiere mucha configuración y es una API bastante confusa, pero se puede lograr así:

Punto de control: e09221e

Esto se dibuja en la fuente predeterminada, y puede usar la propiedad fontFamily para especificar una fuente de sistema común diferente; aunque, probablemente, en tu juego, querrás agregar uno personalizado.

Así que me dirigí a 1001fonts.com y obtuve esta bonita fuente comercial gratuita de Halo como TTF. Nuevamente, simplemente suelte el archivo en assets / fonts, pero ahora debe importarse de manera diferente en el archivo pubspec.yaml. En lugar de agregar un activo más, hay una etiqueta de fuente dedicada, que se comenta de manera predeterminada con instrucciones completas sobre cómo agregar las fuentes. Entonces, asígnele un nombre y haga algo como:

Esta capa de abstracción adicional es de Flutter y le permite agregar varios archivos a la misma fuente (para definir negrita, tamaños más grandes, etc.). Ahora, volviendo a nuestro párrafo, simplemente agregamos la propiedad fontFamily: 'Halo' al constructor TextStyle.

¡Corre y verás la bonita fuente Halo!

Punto de control: 3155bda

Este método descrito le dará más control, por ejemplo, si desea varios estilos en el mismo párrafo. Pero si desea, como en este caso, un párrafo simple con un solo estilo, simplemente use el ayudante Flame.util.text para crearlo:

Esta línea única reemplaza a las 4 anteriores y expone las características más importantes. El texto (primer argumento) es obligatorio, y el resto son opcionales, con valores predeterminados razonables.

Para el color, nuevamente estamos usando el asistente Colors.white, pero también podemos usar el nuevo Color (0xFFFFFFFF) si desea un color específico.

Punto de control: 952a9df

¡Y ahí lo tienes! Un juego completo con representación de sprites, representación de texto, audio, bucle de juego, eventos y gestión de estado.

Lanzamiento

¿Está listo tu juego para lanzar?

Simplemente siga estos sencillos pasos del tutorial de Flutter.

Todos son bastante sencillos, como puede ver en este último punto de control, a excepción de la parte de Iconos, que puede causar un poco de dolor de cabeza. Mi recomendación es hacer una versión grande (512 o 1024 px) de su icono, y usar el sitio web Make App Icon para generar un zip con todo lo que necesita (iOS y Android).

Punto de control: 2974f29

¿Qué más?

¿Disfrutaste Flame? Si tiene alguna sugerencia, error, pregunta, solicitud de funciones o cualquier otra cosa, ¡no dude en ponerse en contacto conmigo!

¿Quieres mejorar tu juego y aprender más? ¿Qué tal si agrega un servidor con Firebase y Google Sign In? ¿Qué hay de poner anuncios? ¿Qué tal configurar un menú principal y varias pantallas?

Hay mucho que mejorar, por supuesto, este es solo un juego de ejemplo. Pero debería haber dado una idea básica de los conceptos básicos del desarrollo del juego con Flutter (con o sin Flame).

¡Espero que todos lo hayan disfrutado!

Publicado originalmente en GitHub.