Siempre he querido hacer videojuegos. Mi primera aplicación de Android que me ayudó a conseguir mi primer trabajo fue un juego simple, hecho con vistas de Android. Después de eso, hubo muchos intentos de crear un juego más elaborado utilizando un motor de juego, pero todos fracasaron debido a la falta de tiempo o la complejidad de un marco. Pero cuando escuché por primera vez sobre el motor Flame, basado en Flutter, me atrajo de inmediato su simplicidad y soporte multiplataforma, así que decidí intentar construir un juego con él. Quería comenzar con algo simple, pero desafiante, para tener una idea del motor. Esta serie de artículos es mi viaje de aprendizaje de Flame (y Flutter) y la creación de un juego de plataformas básico. Trataré de hacerlo bastante detallado, por lo que debería ser útil para cualquiera que se esté metiendo de lleno en Flame o en el desarrollo de juegos en general. En el transcurso de 4 artículos, voy a crear un juego de desplazamiento lateral en 2D que incluye: Un personaje que puede correr y saltar. Una cámara que sigue al jugador Mapa de nivel de desplazamiento, con suelo y plataformas. Fondo de paralaje Monedas que el jugador puede recolectar y HUD que muestra la cantidad de monedas Pantalla de victoria En la primera parte, vamos a crear un nuevo proyecto de Flame, cargar todos los activos, agregar un personaje de jugador y enseñarle cómo ejecutarlo. configuración del proyecto Primero, vamos a crear un nuevo proyecto. El tutorial oficial hace un gran trabajo al describir todos los pasos para hacerlo, así que solo síguelo. del juego Bare Flame Una cosa para agregar: cuando está configurando el archivo , puede actualizar las versiones de las bibliotecas a la última disponible, o dejarlo como está, porque el signo de intercalación (^) antes de una versión garantizará que su aplicación use la última no -versión de última hora. ( ) pubspec.yaml sintaxis de intercalación Si siguió todos los pasos, su archivo debería verse así: main.dart import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; void main() { final game = FlameGame(); runApp(GameWidget(game: game)); } Activos Antes de continuar, debemos preparar los activos que se utilizarán para el juego. Los activos son imágenes, animaciones, sonidos, etc. Para los propósitos de esta serie, usaremos solo imágenes que también se denominan sprites en el desarrollo de juegos. La forma más sencilla de construir un nivel de juego de plataformas es usar mapas de mosaicos y sprites de mosaicos. Significa que el nivel es básicamente una cuadrícula, donde cada celda indica qué objeto/suelo/plataforma representa. Más tarde, cuando el juego se está ejecutando, la información de cada celda se asigna al sprite de mosaico correspondiente. Los gráficos de los juegos creados con esta técnica pueden ser realmente elaborados o muy simples. Por ejemplo, en Super Mario Bros, ves que muchos elementos se repiten. Esto se debe a que, para cada mosaico de suelo en la cuadrícula del juego, solo hay una imagen de suelo que lo representa. Seguiremos el mismo enfoque y prepararemos una sola imagen para cada objeto estático que tengamos. También queremos que algunos de los objetos, como el personaje del jugador y las monedas, estén animados. La animación generalmente se almacena como una serie de imágenes fijas, cada una de las cuales representa un solo cuadro. Cuando se reproduce la animación, los fotogramas van uno tras otro, creando la ilusión de que el objeto se mueve. Ahora la pregunta más importante es dónde conseguir los activos. Por supuesto, puedes dibujarlos tú mismo o encargárselos a un artista. Además, hay muchos artistas increíbles que contribuyeron con recursos del juego al código abierto. Usaré de . el paquete Arcade Platformer Assets GrafxKid Por lo general, los activos de imagen vienen en dos formas: hojas de sprites y sprites individuales. La primera es una imagen grande que contiene todos los activos del juego en uno. Luego, los desarrolladores del juego especifican la posición exacta del sprite requerido y el motor del juego lo corta de la hoja. Para este juego, usaré sprites individuales (excepto animaciones, es más fácil mantenerlos como una sola imagen) porque no necesito todos los activos proporcionados en la hoja de sprites. Tanto si crea sprites usted mismo como si los obtiene de un artista, es posible que deba dividirlos para hacerlos más adecuados para el motor del juego. Puede usar herramientas creadas específicamente para ese propósito (como o cualquier editor gráfico. Usé Adobe Photoshop porque, en esta hoja de sprites, los sprites tienen un espacio desigual entre ellos, lo que dificultaba que las herramientas automáticas extrajeran imágenes, así que tuve que hacerlo manualmente. el empaquetador de texturas) También es posible que desee aumentar el tamaño de los activos, pero si no es una imagen vectorial, el sprite resultante podría volverse borroso. Una solución que encontré que funciona muy bien para el arte de píxeles es usar el método de cambio de en Photoshop (o Interpolación configurada en Ninguno en Gimp). Pero si su activo es más detallado, probablemente no funcionará. Nearest Neighbour (hard edges) Con las explicaciones fuera del camino, descargue o prepare los suyos propios y agréguelos a la carpeta de su proyecto. los recursos que preparé assets/images Cada vez que agregue nuevos activos, debe registrarlos en el archivo de esta manera: pubspec.yaml flutter: assets: - assets/images/ Y el consejo para el futuro: si está actualizando activos ya registrados, debe reiniciar el juego para ver los cambios. Ahora vamos a cargar los activos en el juego. Me gusta tener todos los nombres de los activos en un solo lugar, lo que funciona muy bien para un juego pequeño, ya que es más fácil realizar un seguimiento de todo y modificarlo si es necesario. Entonces, creemos un nuevo archivo en el directorio : lib assets.dart const String THE_BOY = "theboy.png"; const String GROUND = "ground.png"; const String PLATFORM = "platform.png"; const String MIST = "mist.png"; const String CLOUDS = "clouds.png"; const String HILLS = "hills.png"; const String COIN = "coin.png"; const String HUD = "hud.png"; const List<String> SPRITES = [THE_BOY, GROUND, PLATFORM, MIST, CLOUDS, HILLS, COIN, HUD]; Y luego cree otro archivo, que contendrá toda la lógica del juego en el futuro: game.dart import 'package:flame/game.dart'; import 'assets.dart' as Assets; class PlatformerGame extends FlameGame { @override Future<void> onLoad() async { await images.loadAll(Assets.SPRITES); } } es la clase principal que representa nuestro juego, extiende , la clase de juego base utilizada en el motor Flame. Lo que a su vez amplía , el bloque de construcción básico de Flame. Todo en su juego, incluidas las imágenes, la interfaz o los efectos, son Componentes. Cada tiene un método asíncrono , que se llama en la inicialización del componente. Por lo general, toda la lógica de configuración de componentes va allí. PlatformerGame FlameGame Component Component onLoad Finalmente, importamos nuestro archivo que creamos anteriormente y lo agregamos para declarar explícitamente de dónde provienen nuestras constantes de activos. Y usó el método para cargar todos los activos enumerados en la lista de en el caché de imágenes del juego. assets.dart as Assets images.loadAll SPRITES Luego, necesitamos crear nuestro nuevo desde . Modifique el archivo de la siguiente manera: PlatformerGame main.dart import 'package:flame/game.dart'; import 'package:flutter/widgets.dart'; import 'game.dart'; void main() { runApp( const GameWidget<PlatformerGame>.controlled( gameFactory: PlatformerGame.new, ), ); } Toda la preparación está lista y comienza la parte divertida. Agregar personaje de jugador Cree una nueva carpeta y un nuevo archivo dentro de ella. Este será el componente que representará al personaje del jugador: El Niño. lib/actors/ theboy.dart import '../game.dart'; import '../assets.dart' as Assets; import 'package:flame/components.dart'; class TheBoy extends SpriteAnimationComponent with HasGameRef<PlatformerGame> { TheBoy({ required super.position, // Position on the screen }) : super( size: Vector2.all(48), // Size of the component anchor: Anchor.bottomCenter // ); @override Future<void> onLoad() async { animation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, // For now we only need idle animation, so we load only 1 frame textureSize: Vector2.all(20), // Size of a single sprite in the sprite sheet stepTime: 0.12, // Time between frames, since it's a single frame not that important ), ); } } La clase extiende que es un componente que se usa para sprites animados y tiene un combinado que nos permite hacer referencia al objeto del juego para cargar imágenes del caché del juego u obtener variables globales más adelante. SpriteAnimationComponent HasGameRef En nuestro método , creamos una nueva a partir de la hoja de sprites que declaramos en el archivo . onLoad SpriteAnimation THE_BOY assets.dart ¡Ahora agreguemos nuestro jugador al juego! Regrese al archivo y agregue lo siguiente al final del método : game.dart onLoad final theBoy = TheBoy(position: Vector2(size.x / 2, size.y / 2)); add(theBoy); ¡Si ejecutas el juego ahora, deberíamos poder conocer a The Boy! Movimiento del jugador Primero, debemos agregar la capacidad de controlar a The Boy desde el teclado. Agreguemos la mezcla al archivo . HasKeyboardHandlerComponents game.dart class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents A continuación, volvamos a la mezcla y : theboy.dart KeyboardHandler class TheBoy extends SpriteAnimationComponent with KeyboardHandler, HasGameRef<PlatformerGame> Luego, agregue algunas variables de clase nuevas al componente : TheBoy final double _moveSpeed = 300; // Max player's move speed int _horizontalDirection = 0; // Current direction the player is facing final Vector2 _velocity = Vector2.zero(); // Current player's speed Finalmente, anulemos el método que permite escuchar las entradas del teclado: onKeyEvent @override bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) { _horizontalDirection = 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) || keysPressed.contains(LogicalKeyboardKey.arrowLeft)) ? -1 : 0; _horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) || keysPressed.contains(LogicalKeyboardKey.arrowRight)) ? 1 : 0; return true; } Ahora es igual a 1 si el jugador se mueve hacia la derecha, -1 si el jugador se mueve hacia la izquierda y 0 si el jugador no se mueve. Sin embargo, todavía no podemos verlo en la pantalla, porque la posición del jugador aún no ha cambiado. Arreglemos eso agregando el método . _horizontalDirection update Ahora necesito explicar qué es el bucle del juego. Básicamente, significa que el juego se ejecuta en un bucle sin fin. En cada iteración, el estado actual se representa en el método y luego se calcula un nuevo estado en la del método. El parámetro en la firma del método es el tiempo en milisegundos desde la última actualización de estado. Con eso en mente, agregue lo siguiente a : render Component's update dt theboy.dart @override void update(double dt) { super.update(dt); _velocity.x = _horizontalDirection * _moveSpeed; position += _velocity * dt; } Para cada ciclo de bucle de juego, actualizamos la velocidad horizontal, utilizando la dirección actual y la velocidad máxima. Luego cambiamos la posición del sprite con el valor actualizado multiplicado por . dt ¿Por qué necesitamos la última parte? Bueno, si actualizas la posición con solo la velocidad, entonces el sprite volará al espacio. Pero, ¿podemos usar el valor de velocidad más pequeño? Podemos, pero la forma en que el jugador se mueve será diferente con diferentes velocidades de fotogramas por segundo (FPS). La cantidad de fotogramas (o bucles de juego) por segundo depende del rendimiento del juego y del hardware en el que se ejecuta. Cuanto mejor sea el rendimiento del dispositivo, mayor será el FPS y más rápido se moverá el jugador. Para evitar eso, hacemos que la velocidad dependa del tiempo transcurrido desde el último cuadro. De esa manera, el sprite se moverá de manera similar en cualquier FPS. Bien, si ejecutamos el juego ahora, deberíamos ver esto: Impresionante, ahora hagamos que el niño dé la vuelta cuando vaya a la izquierda. Agregue esto al final del método : update if ((_horizontalDirection < 0 && scale.x > 0) || (_horizontalDirection > 0 && scale.x < 0)) { flipHorizontally(); } Lógica bastante fácil: verificamos si la dirección actual (la flecha que el usuario está presionando) es diferente de la dirección del sprite, luego volteamos el sprite a lo largo del eje horizontal. Ahora agreguemos también animación en ejecución. Primero defina dos nuevas variables de clase: late final SpriteAnimation _runAnimation; late final SpriteAnimation _idleAnimation; Luego actualice de esta manera: onLoad @override Future<void> onLoad() async { _idleAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 1, textureSize: Vector2.all(20), stepTime: 0.12, ), ); _runAnimation = SpriteAnimation.fromFrameData( game.images.fromCache(Assets.THE_BOY), SpriteAnimationData.sequenced( amount: 4, textureSize: Vector2.all(20), stepTime: 0.12, ), ); animation = _idleAnimation; } Aquí extrajimos la animación inactiva agregada previamente a la variable de clase y definimos una nueva variable de animación de ejecución. A continuación, agreguemos un nuevo método : updateAnimation void updateAnimation() { if (_horizontalDirection == 0) { animation = _idleAnimation; } else { animation = _runAnimation; } } Y finalmente, invoque este método en la parte inferior del método y ejecute el juego. update Conclusión Eso es todo por la primera parte. Aprendimos cómo configurar un juego Flame, dónde encontrar activos, cómo cargarlos en tu juego y cómo crear un personaje animado increíble y hacer que se mueva según las entradas del teclado. El código para esta parte se puede encontrar . en mi github En el próximo artículo, cubriré cómo crear un nivel de juego usando Tiled, cómo controlar la cámara Flame y agregar un fondo de paralaje. ¡Manténganse al tanto! Recursos Al final de cada parte, agregaré una lista de increíbles creadores y recursos de los que aprendí. Activos del juego de plataformas Arcade de GrafxKid https://opengameart.org/content/arcade-platformer-assets Serie de desarrollo de juegos DevKage Flame: https://youtu.be/mSPalRqZQS8 Canal de Craig Oda https://youtu.be/hwQpBuZoV9s Tutorial del juego Ember Quest https://github.com/flame-engine/flame/blob/main/doc/tutorials/platformer/platformer.md Documentación de Flame Engine https://docs.flame-engine.org/1.6.0/flame/flame.html