Using Components

When working on larger scale games with a lot of complex Entities it can sometimes help to break down behaviors and data into smaller Components that can be added to Entities.  While Otter is not truly a pure Entity Component system, it does offer support for adding Components to Entities that can do things like track health, move the Entity, and fire a weapon.

Here's a relatively lightweight example of designing a small game with Components in mind.

using Otter;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UsingComponents {
  class Program {
    static void Main(string[] args) {
      // Create a Game.
      var game = new Game("Components Example");

      // Set the color to something nice.
      game.Color = Color.Shade(0.15f);

      // Create a Scene.
      var scene = new Scene();

      // Add a Player.
      scene.Add(new Player(game.HalfWidth, game.HalfHeight));

      // Add some Targets.
      for (int i = 0; i < 15; i++) {
        scene.Add(new Target());
      }

      // Start up the Game.
      game.Start(scene);
    }
  }

  // Tags to use for collision checks.
  enum Tags {
    Target,
    Bullet
  }

  class Player : Entity {
    // A simple white rectangle to see the player.
    Image image = Image.CreateRectangle(32, 32, Color.White);

    public Player(float x, float y) : base(x, y) {
      // Create two Axes, one that uses WASD, and another that uses arrows.
      var axisMove = Axis.CreateWASD();
      var axisShoot = Axis.CreateArrowKeys();

      // Add and center the image.
      AddGraphic(image);
      image.CenterOrigin();

      // Add all the Components we'll need to make a functioning player.
      AddComponents(
        axisMove,
        axisShoot,
        new Weapon(axisShoot),
        new TopDownMovement(axisMove, 4),
        new Heart(5),
        new ClampInWindow()
        );
    }
  }

  class Target : Entity {
    public Target() {
      // Add and center a CircleCollider to the target.
      AddCollider(new CircleCollider(20, Tags.Target));
      Collider.CenterOrigin();

      // Add and center a circle Graphic to match the Collider.
      AddGraphic(Image.CreateCircle(20, Color.Cyan));
      Graphic.CenterOrigin();

      // Create a health Component and set the max health to 3.
      var heart = new Heart(3);
      // When the health Component dies remove the Target.
      heart.OnDeath += () => { RemoveSelf(); };
      // Add the Component.
      AddComponent(heart);
    }

    public override void Added() {
      base.Added();

      // Randomize the position when it's added to the Scene.
      SetPosition(Rand.Float(Game.Width), Rand.Float(Game.Height));
    }

    public override void Removed() {
      base.Removed();

      // If the last Target is removed add another 15 to the Scene.
      if (Scene.GetCount<Target>() == 0) {
        for (int i = 0; i < 15; i++) {
          Scene.Add(new Target());
        }
      }
    }

    public override void Update() {
      base.Update();

      // Check for a collision with Bullets.
      if (Collider.Overlap(X, Y, Tags.Bullet)) {
        // Hurt the Target.
        GetComponent<Heart>().Hurt();
        // Remove the bullet that did the damage.
        Collider.CollideEntity(X, Y, Tags.Bullet).RemoveSelf();
      }
    }
  }

  class Bullet : Entity {
    public Bullet(float x, float y, float x2, float y2) : base(x, y) {
      // Add and center a CircleCollider.
      AddCollider(new CircleCollider(5, Tags.Bullet));
      Collider.CenterOrigin();

      // Add a Component to move the bullet.
      AddComponent(new BulletMovement(x2 - x, y2 - y, 10));

      // Add and center a Graphic.
      AddGraphic(Image.CreateCircle(5, Color.Yellow));
      Graphic.CenterOrigin();

      // Remove the Bullet after 60 frames.
      LifeSpan = 60;
    }
  }

  class ClampInWindow : Component {
    public override void Update() {
      base.Update();

      // Clamp the Entity's position inside the Game window.
      Entity.X = Util.Clamp(Entity.X, 0, Entity.Game.Width);
      Entity.Y = Util.Clamp(Entity.Y, 0, Entity.Game.Height);
    }
  }

  class TopDownMovement : Component {
    // The Axis to use for moving.
    Axis axis;

    // The speed to move the Entity at.
    float moveSpeed;

    public TopDownMovement(Axis movementAxis, float speed) {
      axis = movementAxis;
      moveSpeed = speed;
    }

    public override void Update() {
      base.Update();

      // Move the Entity with the Axis and multiply by the moveSpeed.
      Entity.AddPosition(axis, moveSpeed);
    }
  }

  class BulletMovement : Component {
    // The velocity to move the Entity each frame.
    Vector2 velocity;

    public BulletMovement(float x, float y, float speed) {
      // Set the velocity.
      velocity = new Vector2(x, y);
      velocity.Normalize(speed);
    }

    public override void Update() {
      base.Update();

      // Move the Entity by the velocity each frame.
      Entity.AddPosition(velocity);
    }
  }

  class Weapon : Component {
    // The Axis to use for shooting Bullets.
    Axis axis;

    public Weapon(Axis shootingAxis) {
      axis = shootingAxis;
    }

    public override void Update() {
      base.Update();

      // If one of the Buttons on the Axis is pressed shoot a Bullet in that direction.
      if (axis.Up.Pressed) {
        Scene.Add(new Bullet(Entity.X, Entity.Y, Entity.X, Entity.Y - 1));
      }
      if (axis.Down.Pressed) {
        Scene.Add(new Bullet(Entity.X, Entity.Y, Entity.X, Entity.Y + 1));
      }
      if (axis.Left.Pressed) {
        Scene.Add(new Bullet(Entity.X, Entity.Y, Entity.X - 1, Entity.Y));
      }
      if (axis.Right.Pressed) {
        Scene.Add(new Bullet(Entity.X, Entity.Y, Entity.X + 1, Entity.Y));
      }
    }
  }

  class Heart : Component {
    // The maximum health.
    public int MaxHealth;

    // The current health.
    public int Health;

    // The method that will execute when the health reaches 0.
    public Action OnDeath = delegate { };

    public Heart(int maxHealth) {
      // Set the max health and health.
      MaxHealth = maxHealth;
      Health = maxHealth;
    }

    public void Hurt() {
      // Reduce the health by 1.
      Health--;
      if (Health <= 0) {
        // If the Health reaches 0 call the OnDeath method.
        OnDeath();
      }
    }
  }
}

In this example take control of a small Player object that uses the W, A, S, and D keys to move, and the arrow keys to fire bullets in any of the cardinal directions.  Destroy the targets by hitting them with bullets.  Each target takes 3 hits to destroy.

Components are a great way to break down different resuable parts of Entities, and usually offers more flexibility than inheritance. Figuring out the right way to take advantage of Components can be a challenge though!

Examples