Unity RTS Game Architecture

on tutorials rts

I have created many games over the years -- mostly small Flash games. And I feel that I have finally constructed a robust game architure which should be the basis of every game. This is inspired by the MVC pattern but with some modifications -- a game isn't a user interface but instead revolves around a game loop.

I organize my games with an engine which can contain many controllers and communication between them is done through a global event manager. The engine contains the game loop and handles user input, while the controllers contain the logic.

     Player Input
          +
          |
          v

+-------------------------------+ | | <-----------+ | Engine | | | | | | +--------------------------+ | | | | +-------> +------+-----+ | | Selection Controller | | | | | | | | | Event | | +--------------------------+ | | Manager | | | | | | +--------------------------+ | | | | | +-------> +------------+ | | UI Controller | | | | | | | +--------------------------+ | +-------------------------------+

If you are familiar with the MVC pattern you're probably wondering where the models and views live. I haven't included that in this diagram and will write a separate post about that. This focuses on running the core game logic.

Let look at the actors:

Engine

This will listen for player input and direct it to the appropriate controller. This is essentially the main entry point into the game world. Nothing should hold a reference to the engine, it is the highest level of the game. Think of the engine as the glue which connects everything together but doesn't contain any core logic itself.

Controller

This contains a slice of game logic and is always invoked by the engine. Each controller has access to an instance of level which contains all the models in the game. More on that in an other blog post.

Selection Controller this keeps track of units selected and attempts to make new selections.

UI Controller this displays UI elements on the screen based on action in the game. When the mouse is dragged it will display the UI element but contains none of the selection logic. It's important to keep a separation between logic / display.

Controllers allow for separation of game logic, which makes testing and maintainability easier.

You can have as many controllers as you like, and as the game grows you can even have nested controllers inside one an other.

Event Manager

The last piece is the Event Manager, I have already talked about this in an other blog post. It's used for sending out events that happened in the game, this keeps the system decoupled from other controllers.

Don't worry if none of this makes sense, let's dive the example code.

The Engine

public class Engine {

    public Level level { get; private set; }

    private SelectionController _selectionController;
    private CommandController _commandController;
    private UiController _uiController;
    private GameOverController _gameOverController;

    public void Update(){
        if (level.IsGameOver()) {
            _gameOverController.EndGame();
            return;
        }

        if (Input.GetButtonDown("Fire1")) {
            _selectionController.CreateBoxSelection();
        }

        if (Input.GetButtonUp("Fire1")) {
            _selectionController.SelectEntities();
        }

        _selectionController.DragBoxSelection();

        if (Input.GetButtonDown("Fire2")) {
            _commandController.ActionAtPoint(Input.mousePosition);
        }

    }

    public void LoadLevel(LevelData levelData){
        level.LoadData(levelData, Teams.One, Teams.Two);

        _selectionController = new SelectionController(this.level);
        _commandController = new CommandController(this.level);
        _uiController = new UiController();
        _gameOverController = new GameOverController(level);

        SpawnUtils.SpawnAll(level.teamList);

        EventManager.Instance.AddListener<SelectionEvent>(OnSelection);
        EventManager.Instance.AddListener<StartSelectBoxEvent>(OnStartSelectBox);
        EventManager.Instance.AddListener<DragSelectBoxEvent>(OnDragSelectBox);
    }

    private void OnSelection(SelectionEvent e) {
      _commandController.selection = e.selection;

      _uiController.ClearBox();
    }

    private void OnStartSelectBox(StartSelectBoxEvent e) {
      _uiController.StartSelectBox(e.anchor);
    }

    private void OnDragSelectBox(DragSelectBoxEvent e) {
      _uiController.DragSelectBox(e.outer);
    }

    private void OnGameOver(GameOverEvent e) {
        _uiController.ClearAll();
    }
}

The Engine isn't actually a MonoComponent. The engine is initialized by an other component called Game. It initializes the engine, and calls LoadLevel.

LoadLevel will initialize the level object, controllers, attach the event listeners and then spawn all entities. Effectively starting the game.

You can see how input is listened to, and directed to appropriate controllers. In the Unity docs, they like to create a component for each type of input, one for mouse, key controls, etc.

However, I like making all input pass through a single method.

  • Easier to control the order of the input
  • Easier to handling pausing / game over screens
  • Much easier to debug

Example Controller

public class SelectionController {

  private Level level;
  private Team playerTeam;

  public List<Entity> selection { get; private set; }

  public SelectionController(Level level){
    this.level = level;
    selection = new List<Entity>();
    this.playerTeam = level.playerTeam;
  }

  public void ClearSelection() {
    RemoveAllSelections();

    EventManager.Instance.TriggerEvent(new SelectionEvent(selection));
  }

  private void AddToSelection(Entity entity) {
    DebugUtil.Assert(entity != null, "Selected object with no entity");

    selection.Add(entity);

    entity.transform.Find("Halo").gameObject.SetActive(true);
  }

  private void AddAllWithinBounds() {
    Bounds bounds = SelectUtils.GetViewportBounds(Camera.main, anchor, outer);

    this.level.playerTeam.ForEach( (Entity entity) => {
      if (SelectUtils.IsWithinBounds(Camera.main, bounds, entity.transform.position)) {
        AddToSelection(entity);
      }
    });
  }

  private void AddSingleEntity() {
    Entity entity = SelectUtils.FindEntityAt(Camera.main, anchor);

    if (entity != null) {
      if (entity.team == level.playerTeam) {
        AddToSelection(entity);
      }
    }
  }

  private void RemoveAllSelections() {

    foreach (Entity entity in selection) {
      entity.transform.Find("Halo").gameObject.SetActive(false);
    }

    selection.Clear();
  }

  public void SelectEntities() {
    RemoveAllSelections();

    if (outer == anchor) {
      AddSingleEntity();
    } else {
      AddAllWithinBounds();
    }

    hasActiveBox = false;

    EventManager.Instance.TriggerEvent(new SelectionEvent(selection));
  }

  public void CreateBoxSelection() {
    hasActiveBox = true;

    anchor = Input.mousePosition;
    outer = Input.mousePosition;

    EventManager.Instance.TriggerEvent(new StartSelectBoxEvent(anchor));
  }

  public void DragBoxSelection() {
    if (hasActiveBox) {
      outer = Input.mousePosition;

      EventManager.Instance.TriggerEvent(new DragSelectBoxEvent(outer));
    }
  }
}

The first thing to notice is the controller maintains its own state, by holding a reference of selection. The controller will send out events when ever something interesting happens. The Engine will then pick up those events and relay them to other controllers, in this case the UiController.

This is beautiful, because controllers are now decoupled from the owner --the engine-- and have no knowledge of other controllers but are still able to fully collaborate through events.

Testing this is a breeze as well.

Example Events

public abstract class GameEvent {

}

public class SelectionEvent : GameEvent {

  public List<Entity> selection { get; private set; }

  public SelectionEvent(List<Entity> selection){
    this.selection = selection;
  }
}

public class StartSelectBoxEvent : GameEvent {

  public Vector3 anchor { get; private set; }

  public StartSelectBoxEvent(Vector3 anchor){
    this.anchor = anchor;
  }
}

public class DragSelectBoxEvent : GameEvent {

  public Vector3 outer { get; private set; }

  public DragSelectBoxEvent(Vector3 outer){
    this.outer = outer;
  }
}

This shouldn't come as a surprise if you have read my Event Manager Post. Important data about the event is passed through to give context to the event, for example the SelectionEvent:

// Engine.cs
private void OnSelection(SelectionEvent e) {
  _commandController.selection = e.selection;

  _uiController.ClearBox();
}

The selection found from the SelectionController is passed to the CommandController on selection.

// Engine.cs
if (Input.GetButtonDown("Fire2")) {
  _commandController.ActionAtPoint(Input.mousePosition);
}

Now, this line of code will command the selected units to do something.

Watch For Coupling

// SelectionController.cs
entity.transform.Find("Halo").gameObject.SetActive(false);

This is an ugly piece of code, I just haven't had the chance to refactor it. What I would do is replace this with an event, DeselectEntity, with the entity passed as a reference. The UiController can then pick the event and display the halo.

Conclusion

In the end, this is a great way to architect games. I have used this in many of my own games and it has always be sufficient. Please share your thoughts.

Next part: Unit Selection