Conceptos claves en Forge 2D con Flame y Flutter

- Andrés Cruz

In english

En este apartado conoceremos los elementos básicos en Forge 2D y su combinación con Flame, que es el plugin principal. Este capítulo representa un intermedio para conocer las características básicas de Forge 2D con Flame, por lo tanto, es un capítulo netamente referencial y que puedes emplear de apoyo en cualquier momento en caso de que tengas alguna duda con alguna implementación real que veremos en los siguientes capítulos.

¿Qué es Forge 2D?

Comencemos hablando sobre la tecnología clave que es Forge2D; Forge2D no es más que una adaptación para Dart (y con esto, para Flutter y Flame) del motor de física Box2D; Box2D es una biblioteca de código abierto que implementa un motor de física (physics engine) en dos dimensiones. Por lo tanto con Forge2D podemos usar un motor de física en 2D para nuestras aplicaciones y/o juegos.

Como comentamos antes, puedes emplear Forge2D de forma independiente en Dart o en Flutter con el paquete:

$ dart pub add forge2d
$ flutter pub add forge2d

O con Flame con el paquete:

$ flutter pub add flame_forge2d

En este libro, vamos a usar Forge2D junto con Flame, por lo tanto, usaremos el paquete anterior en los proyectos en Flutter con Flame.

Clase Principal

En Flame, como clase principal o clase tipo Game, usábamos las clases de Game o FlameGame, en Forge 2D usamos es Forge2DGame:

class MyGame extends Forge2DGame { }

De esta manera, podemos interactuar con el mundo mediante el motor de física 2D.

Por ejemplo, para la clase principal, podemos especificar el uso de la gravedad:

MyGame() : super(gravity: Vector2(0, 15));

Esto significa que la gravedad afecta a todos los cuerpos dinámicos del mundo en y=15, que significa que los objetos serán empujados hacia abajo como en la vida real; por otra parte, x=0 significa que no hay gravedad horizontal.

En Forge2D, nuestros componentes son cuerpos o bodies y tienen cualidades físicas (a diferencia de los componentes de Flame) y son los objetos fundamentales del mundo en Forge2D y la principal diferencia o agregado que tenemos en Forge2D con respecto a Flame básico.

El mundo/world

Como todo juego creado en Flutter que emplea Flame, en Forge2D necesita un "mundo" para posicionar todos los componentes que conforman el juego; en otras palabras, los objetos o los llamados bodies o cuerpos que podemos crear en Forge2D; esto lo vamos a conocer un poco más en detalle cuando creemos un body, que viene siendo un componente de Flame, pero, con la capacidad de que a este componente se le puede interactuar física de manera nativa mediante Forge2D:

return world.createBody(<BODY>)

En definitiva, la clase world permite gestionar todas las entidades físicas.

Para definir al mundo, no podemos hacerlo empleando el World provisto en la clase de tipo Game de Flame como FlameGame ya que, no son compatibles; para acceder al world de Forge, debemos de hacerlo de la siguiente manera:

import 'package:flame/camera.dart' as camera;
***
final cameraWorld = camera.World();

Y agregar directamente en la clase de tipo Forge2DGame:

add(cameraWorld);

Es un poco extraño la manera de acceder al mundo en Forge con Flame y puede que esta implementación cambie a futuro por alguna más coherente.

Cámara

Al igual que ocurre en Flame, necesitamos utilizar una cámara para "ver el mundo", mediante la cámara se especifica que parte del mundo se está viendo en un momento dado.

Que al igual que en Flame básico, debemos de emplear el componente de cámara y agregar directamente en la clase de tipo Forge2DGame:

cameraComponent = CameraComponent(world: cameraWorld);
cameraComponent.viewfinder.anchor = Anchor.topLeft;

add(cameraComponent);

Cámara y mundo/world

Como ya conocemos de Flame sin ningún agregado o Flame básico, hay una relación estrecha entre el mundo y la cámara, ya que, mediante la cámara se define qué mundo se va a observar y el cómo.

La clase Forge2DGame, la cámara tiene un nivel de zoom establecido en 10 de forma predeterminada, por lo que los componentes serán mucho más grandes que en un juego normal de Flame. Esto se debe al límite de velocidad en el mundo de Forge2D, que alcanzarías muy rápido si lo usas con zoom = 1.0. El problema con esto es el posicionamiento, ya que, si queremos posicionar un elemento en:

10x10

Lo que está pasando realmente en Flame con Forge2D es que lo va a posicionar en:

100x100

Y es precisamente por el zoom establecido en 10.

Para evitar este comportamiento, podemos usar el método de screenToWorld() que convierte las coordenadas de pantalla a coordenadas mundiales; por ejemplo:

cameraComponent.viewport.size
>> Vector2 ([929,918])

Al pasar a coordenadas mundiales, tendremos:

screenToWorld(cameraComponent.viewport.size)
>> Vector2 ([92.9,91.8])

Puede cambiar fácilmente el nivel de zoom:

class MyGame extends Forge2DGame {
 MyGame() : super(zoom: 1);
}

Y si hacemos el mismo cálculo:

cameraComponent.viewport.size
>> Vector2 ([929,918])

Veremos:

screenToWorld(cameraComponent.viewport.size)
>> Vector2 ([929,918])

Por lo tanto, la función screenToWorld() devuelve las coordenadas en base al nivel de zoom; en el ejemplo anterior, al pasar de un zoom de 10 a 1, veremos que los objetos en nuestra ventana tienen un 1/10 del tamaño anterior.

Cuerpos/body

En Forge2D, las instancias de clases de tipo BodyComponent se conocen como cuerpos o bodies; estos cuerpos son los objetos fundamentales en el mundo de un proyecto en Forge 2D; a diferencia de los componentes en Flame, con Forge es posible emplear el motor de física en 2D, por lo tanto, los cuerpos son la pieza clave en nuestros juegos para interactuar con el motor de física en 2D; como veremos más adelante, podemos interactuar con estos cuerpos de diversas maneras y existen diferentes tipos de cuerpo:

  1. Estáticos: Define un cuerpo con masa cero, velocidad cero, se puede mover manualmente.
  2. Dinámicos: Define un cuerpo con masa positiva, velocidad distinta de cero determinada por fuerzas.
  3. Cinemáticos: Define un cuerpo con masa cero, velocidad distinta de cero establecida por el usuario.

En Forge, para crear un cuerpo, necesitamos crear una clase y extender de BodyComponent:

class MyBody extends BodyComponent {  
 @override  
 Body createBody() {  
   
 }  
}

Dentro del método createBody(), creamos un BodyDef que contendrá la definición del cuerpo:

final bodyDef = BodyDef(  
 position: Vector2(worldSize.x / 2, 0),  
 type: BodyType.dynamic,  
);

Podemos ver que este será un cuerpo dinámico, posicionado verticalmente en la parte superior de la pantalla y centrado horizontalmente.

El método de creadeBody() que se invoca automáticamente de manera interna según el ciclo de vida de un BodyComponent, al ejecutarse, se establece el cuerpo en una propiedad llamada body también interna del mencionado componentes; por lo tanto, al necesitar hacer alguna operación con el cuerpo, podemos referenciar a esta propiedad llamada body.

Puedes cambiar el color del componente mediante la propiedad paint; por ejemplo:

paint = BasicPalette.red.paint();

Características de los cuerpos y componentes de física

En este apartado veremos cómo podemos personalizar los cuerpos en Forge 2D y cómo podemos aplicar fuerzas al cuerpo para interactuar con el motor de física 2D; este apartado es algo abstracto y solo debes de tomarlo como referencia para entender cómo está formado el motor de física 2D; en los siguientes capítulos iremos viendo ejemplos reales de lo explicado en este apartado.

Shapes

En Forge2D, los cuerpos deben tener forma; puede ser circular, rectangular o cualquier tipo de polígono; puedes ver los cuerpos como los hitbox en Flame solo que estos hitbox tienen el componente de física; por ejemplo, para crear una forma circular para nuestro cuerpo:

final shape = CircleShape()..radius = .35;

Para crear una caja, tenemos:

final shape = PolygonShape()..setAsBoxXY(.15, 1.25);

Tenemos distintos tipos de Shapes que podemos emplear:

https://pub.dev/documentation/flame_forge2d/latest/flame_forge2d/CircleShape-class.html

Fixtures

Los cuerpos, aparte del tipo, pueden tener otras características; esto es algo sencillo de entender ya que, si llevamos los cuerpos al mundo real, una pelota sería un cuerpo, al igual que un automóvil sería un cuerpo, pero, entre ellos existen diferencias como el peso, la fricción o el rebote, y estas características las podemos simular con Forge2D mediante los fixture.

En Forge2D, un fixture tiene densidad, fricción y restitución asociadas:

  1. La densidad/Density, es la masa por metro cuadrado, es decir, el peso.
  2. La fricción/Friction [0,1], es la cantidad de fuerza opuesta cuando el objeto roza/se desliza a lo largo de otro cuerpo.
  3. La restitución/Restitution [0,1], es la cantidad de rebote del cuerpo.

Siguiendo el código anterior, vamos a crear un fixture de nuestro cuerpo:

final fixtureDef = FixtureDef(shape);

world.createBody(bodyDef)..createFixture(fixtureDef);

Es importante notar que, los fixtures se establecen en el shape y no de forma directa en el cuerpo; toda esta lógica debe de estar definida en el método de createBody() y una vez ejecutado el mismo (que se realiza de manera automática por el ciclo de vida de Forge 2D) podemos acceder al cuerpo y sus componentes mediante una propiedad llamada body.

Fuerzas, impulsos y velocidad

Sobre el cuerpo, podemos aplicar distintas magnitudes o fuerzas para mover el mismo mediante los siguientes métodos.

  1. Aplicar fuerza/applyForce: Aplicar una fuerza en un punto del mundo; las fuerzas cambian la velocidad de un cuerpo gradualmente con el tiempo. Por ejemplo, un carro que empieza a acelerar irá ganando velocidad de manera progresiva hasta alcanzar la velocidad deseada.
  2. Aplicar impulso lineal/applyLinearImpulse: Aplicar un impulso en un punto del mundo. Esto modifica inmediatamente la velocidad; los impulsos producen cambios inmediatos en la velocidad del cuerpo. Por ejemplo, un choque entre un cuerpo inerte con otro en movimiento hará que de manera inmediata el cuerpo inerte obtenga la velocidad del cuerpo en movimiento; aunque, el movimiento depende del peso inerte y de la fuerza aplicada.
  3. Velocidad lineal/linearVelocity: La velocidad lineal del centro de masa. Sin importar el tamaño y la masa del cuerpo se le establece la velocidad deseada.

Al final, estos métodos permiten mover un cuerpo, para emplear los métodos anteriores, se emplean vectores en 2 dimensiones indicando el movimiento hacia donde queremos mover el cuerpo; por ejemplo, si queremos movernos solamente en el horizontal, usamos un vector con un valor en X (por ejemplo Vector2(50,0)) o en el vertical para hacer un salto, usamos un vector con un valor en Y (por ejemplo Vector2(0,-20)).

Los primeros métodos son influenciables según el tipo de cuerpo, peso y gravedad, a diferencia del de linearVelocity cuyo movimiento se aplica automáticamente sobre el cuerpo sin importar el tipo de cuerpo, peso y gravedad.

Puedes obtener más información sobre los cuerpos, sus propiedades y métodos en:

https://pub.dev/documentation/forge2d/latest/forge2d_browser/Body-class.html

ContactCallbacks: Colisiones o contacto entre cuerpos

Al igual que ocurre en Flame en el cual tenemos una utilidad que permite manejar las colisiones entre componentes, en Forge tenemos un esquema prácticamente igual, pero, aplicado a manejar los contactos (colisiones) entre cuerpos y para emplearlo desde un BodyComponent, debemos de importar el mixin de ContactCallbacks y lo podemos emplear mediante los siguientes métodos:

  1. beginContact(Object other, Contact contact)  void, se llama cuando dos cuerpos empiezan a tocarse.
  2. endContact(Object other, Contact contact)  void, se llama cuando dos cuerpos dejan de tocarse.

En cuanto a los parámetros:

  1. Object other, corresponde al BodyComponent de contacto, en Flame, vienen siendo los componentes.
  2. Contact contact, información sobre el contacto.

Al igual que ocurre con las colisiones en Flame, puedes usar estos métodos para operaciones como reproducir sonidos, mostrar sprites o cualquier otra lógica de juego. Al momento de definir el cuerpo, es imprescindible que establezcas el userData a los cuerpos que nos interese escuchar los contactos mediante este mixin:

BodyDef bodyDef = BodyDef(
  ***
  userData: this);

El

userData

corresponde a datos de la aplicación que son empleados internamente por Forge2D para comprobar los contactos.

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy