Inspired by the Entity Component System Architecture, I started to refactor my 2D tiled-based game to follow the rule "favor composition over inheritance".
However, after writing the movement related components and system, I wasn't sure how to continue and implement the AIControllerSystem:
On one hand, I want to decuple as much as I can the MovementSystem and AIControllerSystem, and on the other hand I want to be able to send a movement action from AIControllerSystem to MovementSystem so I won't need to reimplement the movement logic.
Currently, my movement code looks like this:
public class PositionComponent : IComponent
{
public int X { get; set; }
public int Y { get; set; }
public Direction FacingDirection { get; set; }
}
public class MovementComponent : IComponent
{
public int Speed { get; set; }
public Direction Direction { get; set; }
public bool IsMoving { get; set; }
public byte OffsetInTile { get; set; }
}
public class MovementSystem : ISystem
{
private long _timer;
// states the behavior depends on
private PositionComponent _positionComponent;
private MovementComponent _movementComponent;
public MovementSystem(PositionComponent positionComponent, MovementComponent movementComponent)
{
this._positionComponent = positionComponent;
this._movementComponent = movementComponent;
this._timer = 0;
}
public void Update(long elapsedTime)
{
// some logic
}
public bool CanMove(Direction moveDirection)
{
return !this._movementComponent.IsMoving;
}
public void Move(Direction moveDirection, bool updateFacing=true)
{
if (this.CanMove())
{
this._timer = 0;
this._movementComponent.IsMoving = true;
this._movementComponent.Direction = moveDirection;
if (updateFacing)
this._positionComponent.FacingDirection = moveDirection;
}
}
}
My Approach
I thought about several ways to handle the AIControllerSystem->MovementSystem dependency:
- Let the AIControllerSystem hold a reference to the MovementSystem, and send messages via a simple method call.
- Let the AIControllerSystem send messages to the MovementSystem via a message queue.
- Declare interface IMovable for movable game objects, and let the AIControllerSystem hold a reference to such interface.
- Sticking to the ECS architecture, expose the MovementComponent and PositionComponent to the AIControllerSystem, and "communicate" through them.
Personaly, I think options 3 and 4 are better, although option 4 may lead to code duplications (e.g. check if move is valid, unless the MovementComponent will be changed).
Option 3 will allow me to add game objects like the following:
public class Player : IMovable
{
// states
MovementComponent _movementComponent;
PositionComponent _positionComponent;
// behaviors
MovementSystem _movementSystem
//...
}
public class NPC : IMovable, IAIControllable
{
// states
MovementComponent _movementComponent;
PositionComponent _positionComponent;
// behaviors
MovementSystem _movementSystem
AIControllerSystem _aiControllerSystem // will be dependent on the NPC
//...
}
I know my design is not "pure" ECS or even close to, but I think this hybrid approach is pretty modular to easily perform changes and add game objects with unique behavior combinations.
What do you think?
Thanks.
Edit
Some clarifications:
- The MovementSystem is responsible for updating position (old tile -> neighbor tile) based on Speed, while the OffsetInTile represent a fixed point current movement progress (0-255).
- For now, the AIControllerSystem is responsible for the movement "thinking" and operating of non-player entities (like NPCs).
- The problem in short: keeping the described systems decupled, while letting the AIControllerSystem to communicate with the MovementSystem to perform movement actions.