paint-brush
Enseñando a tu personaje a correr en llamaspor@eugene-kleshnin
3,824 lecturas
3,824 lecturas

Enseñando a tu personaje a correr en llamas

por Eugene Kleshnin11m2023/02/28
Read on Terminal Reader

Demasiado Largo; Para Leer

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 esté sumergiendo sus dedos en Flame o el desarrollador de juegos en general. 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 correr.
featured image - Enseñando a tu personaje a correr en llamas
Eugene Kleshnin HackerNoon profile picture
0-item

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


juego completo


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 del juego Bare Flame hace un gran trabajo al describir todos los pasos para hacerlo, así que solo síguelo.

Una cosa para agregar: cuando está configurando el archivo pubspec.yaml , 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. ( sintaxis de intercalación )

Si siguió todos los pasos, su archivo main.dart debería verse así:

 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.


El nivel está construido de mosaicos repetidos.


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é el paquete Arcade Platformer Assets de 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.



Un solo sprite que representa el suelo.


Hoja de sprites con 6 sprites para las animaciones del jugador.


Ejecutar animación


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 el empaquetador de texturas) 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.


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 Nearest Neighbour (hard edges) en Photoshop (o Interpolación configurada en Ninguno en Gimp). Pero si su activo es más detallado, probablemente no funcionará.


Con las explicaciones fuera del camino, descargue los recursos que preparé o prepare los suyos propios y agréguelos a la carpeta assets/images de su proyecto.


Cada vez que agregue nuevos activos, debe registrarlos en el archivo pubspec.yaml de esta manera:

 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); } }


PlatformerGame es la clase principal que representa nuestro juego, extiende FlameGame , la clase de juego base utilizada en el motor Flame. Lo que a su vez amplía Component , 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 Component tiene un método asíncrono onLoad , que se llama en la inicialización del componente. Por lo general, toda la lógica de configuración de componentes va allí.


Finalmente, importamos nuestro archivo assets.dart que creamos anteriormente y lo agregamos as Assets para declarar explícitamente de dónde provienen nuestras constantes de activos. Y usó el método images.loadAll para cargar todos los activos enumerados en la lista de SPRITES en el caché de imágenes del juego.


Luego, necesitamos crear nuestro nuevo PlatformerGame desde main.dart . Modifique el archivo de la siguiente manera:

 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 lib/actors/ y un nuevo archivo theboy.dart dentro de ella. Este será el componente que representará al personaje del jugador: El Niño.

 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 SpriteAnimationComponent que es un componente que se usa para sprites animados y tiene un HasGameRef 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.


En nuestro método onLoad , creamos una nueva SpriteAnimation a partir de la hoja de sprites THE_BOY que declaramos en el archivo assets.dart .


¡Ahora agreguemos nuestro jugador al juego! Regrese al archivo game.dart y agregue lo siguiente al final del método 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!


Conoce al chico

Movimiento del jugador

Primero, debemos agregar la capacidad de controlar a The Boy desde el teclado. Agreguemos la mezcla HasKeyboardHandlerComponents al archivo game.dart .

 class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents


A continuación, volvamos a la mezcla theboy.dart y 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 onKeyEvent que permite escuchar las entradas del teclado:

 @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 _horizontalDirection 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 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 render Component's y luego se calcula un nuevo estado en la update del método. El parámetro dt 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 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:


El niño cambia su posición a lo largo del eje X.


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 onLoad de esta manera:

 @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 update y ejecute el juego.

Ejecutar animación y voltear el sprite.


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í.