Patrón “Humble Object”

Hace un tiempo escribí un post en el blog de Wave Engine sobre cómo podemos aplicar el patrón Humble Object para aislar dependencias en nuestro código y aquí tenéis la versión traducida al español.

Unit Testing en Wave Engine

El Testing Unitario es una técnica muy útil cuando desarrollamos juegos. Ofrece un montón de beneficios como test automáticos que se pueden ejecutar en un servidor de integración continua, evitan regresiones de bugs, y muchos más.

Este artículo describe cómo podemos usar el patrón Humble Object para evitar algunas dependencias que nos ayudarán a escribir tests unitarios para nuestros juegos.

Imaginemos que tenemos un "Behavior" como este:

public class ShipBehavior : Behavior
{
    private TimeSpan shootRate;        

	[RequiredComponent]
	public Transform2D Transform;

	[DataMember]
	public int BulletsLeft { get; set; }

	public ShipBehavior()
		: base()
	{           
	}

	protected override void Update(TimeSpan gameTime)
	{
		var keyboardState = WaveServices.Input.KeyboardState;
		var mouseState = WaveServices.Input.MouseState;

		this.Transform.X = mouseState.X;
		this.Transform.Y = mouseState.Y;

		if (shootRate > TimeSpan.Zero)
		{
			shootRate -= gameTime;
		}

		if (mouseState.LeftButton == ButtonState.Pressed)
		{
			if (BulletsLeft > 0 && shootRate <= TimeSpan.Zero)
			{
				shootRate = TimeSpan.FromMilliseconds(250);
				BulletsLeft--;
				var bullet = new Entity("bullet" + BulletsLeft.ToString())
					 .AddComponent(new Transform2D())
					 .AddComponent(new Sprite(WaveContent.Assets.Bullet_png))
					 .AddComponent(new SpriteRenderer());


				bullet.FindComponent<Transform2D>().Position = this.Transform.Position;
				bullet.AddComponent(new BulletBehavior());

				EntityManager.Add(bullet);
			}
		}
		if (mouseState.RightButton == ButtonState.Pressed)
		{
			BulletsLeft = 5;
		}
	}
}

Este behavior se encarga de mover la nave, disparar y recargar con el ratón.

Si queremos testar el comportamiento del disparo tendremos que lidiar con algunas dependencias que parecen imposibles de salvar. Para esto podemos hacer uso del patrón Humble Object

Humble Object

La idea de este patrón es extraer la lógica que queremos testar a base de desacoplarla de esas dependencias creando un componente que sea más fácil de testar.

Vamos a testar sólo la lógica de disparo. Podemos ver en el código que sólo es posible disparar cuando hay balas y después de un tiempo de cadencia.

Así que, la "dependencia imposible" parece ser la entidad "bullet" que se crea en cada disparo, así que extraeremos ese código a un método en la misma clase "ShipBehavior" que será nuestro "Humble Object"

 

var bullet = new Entity()
        .AddComponent(new Transform2D())
        .AddComponent(new Sprite(WaveContent.Assets.Bullet_png))
        .AddComponent(new SpriteRenderer());

bullet.FindComponent<Transform2D>().Position = this.Transform.Position;
bullet.FindComponent<Transform2D>().Origin = Vector2.One / 2;
bullet.FindComponent<Transform2D>().DrawOrder = 10;


bullet.AddComponent(new BulletBehavior());

EntityManager.Add(bullet);

Esa es la dependencia que necesitamos extraer a una interfaz "IFire" que nuestro "Humble Object" debe implementar para que la podamos "mockear" en el nuevo componente que llamaremos GunController.

Extraeremos la lógica para comprobar si podemos disparar, la funcionalidad de recargar y el contador de la cadencia de disparo. Así, después de un poco de refactoring, la clase ShipBehavior tiene una pinta tal que esta:

 

public class ShipBehavior : Behavior, IFire
{
    public GunController gunController;

    [RequiredComponent]
    public Transform2D Transform;

    protected override void DefaultValues()
    {
        base.DefaultValues();

        gunController = new GunController();
        gunController.fireController = this;
    }

    protected override void Update(TimeSpan gameTime)
    {
        var keyboardState = WaveServices.Input.KeyboardState;
        var mouseState = WaveServices.Input.MouseState;

        this.Transform.X = mouseState.X;
        this.Transform.Y = mouseState.Y;

        gunController.CountShooRate(gameTime);

        if (mouseState.LeftButton == ButtonState.Pressed)
        {
            gunController.ApplyFire();
        }
        if (mouseState.RightButton == ButtonState.Pressed)
        {
            gunController.Reload();
        }
    }

    public void Fire()
    {
        var bullet = new Entity()
                .AddComponent(new Transform2D())
                .AddComponent(new Sprite(WaveContent.Assets.Bullet_png))
                .AddComponent(new SpriteRenderer());

        bullet.FindComponent<Transform2D>().Position = this.Transform.Position;
        bullet.FindComponent<Transform2D>().Origin = Vector2.One / 2;
        bullet.FindComponent<Transform2D>().DrawOrder = 10;


        bullet.AddComponent(new BulletBehavior());

        EntityManager.Add(bullet);
    }
}

La clase GunController es una clase normal sin ninguna dependencia que podemos testar:

 

public class GunController
{
    public int bulletsLeft = 5;
    private TimeSpan shootRate;

    public IFire fireController;

    public void ApplyFire()
    {
        if (bulletsLeft > 0 && shootRate <= TimeSpan.Zero) { shootRate = TimeSpan.FromMilliseconds(250); bulletsLeft--; fireController.Fire(); } } public void Reload() { bulletsLeft = 5; } internal void CountShooRate(TimeSpan gameTime) { if (shootRate > TimeSpan.Zero)
        {
            shootRate -= gameTime;
        }
    }
}

Ahora podemos testar unitariamente la funcionalidad de disparo muy fácilmente:

 

public void IfThereAreBulletsAShootSucceed()
{
    Mock<IFire> fireControllerMock = new Mock<IFire>();

    GunController controller = new GunController();
    controller.fireController = fireControllerMock.Object;

    controller.ApplyFire();

    Assert.AreEqual(4, controller.bulletsLeft);

    fireControllerMock.Verify(c => c.Fire());
}

Si miramos al diagrama del "Humble Object" podemos identificar cada componente:

HumbleObjectCorresponding

Descárgate el proyecto completo aquí y feliz testing

2 pensamientos en “Patrón “Humble Object”

  1. Titán

    Estáis haciendo un trabajo fantástico en Wave Engine. Siempre me han llamado la atención los motores de videojuegos, ¿qué hace falta saber para programar un Wave Engine?

    Responder
  2. juanlao Autor

    Gracias Titán por tu comentario.
    Para programar un Wave Engine lo que hace más falta es tiempo y ganas :).
    Si quieres ver algo parecido pásate por el proyecto MonoGame. (http://www.monogame.net/) Aunque no es lo mismo que Wave Engine, MonoGame es una implementación de XNA 4 multiplataforma, una librería gráfica, no un framework como Wave Engine.
    Es un buen punto para empezar 🙂

    Responder

Deja un comentario