Board Game Programming Tutorial – Standard Events

All my boardgames share a standard set of Events. These are used for setting up the game and are common to all my games because of how the MainMenu passes control to the game.

There are two types of Events in most games: Player initiated events and Engine initiated events. I have a convention that Engine events start with “E” and Player events start with “P”. Typically, when a player presses “Undo” all events back to the last Player event are undone.

All events have three parts to their lifecycle: Constructor, Do and Act. When an event is first created it executes all three of those stages. When an event is re-executed during an undo or load, just the Do happens. So the Event needs to store any state needed by the Do function.

The events are saved using Newtonsoft.JSON. They store any public member variables but can’t handle pointers very well.

So Events obey the following conventions:

  • There is a public no-parameter constructor for the JSON loader to use.
  • There is a public constructor with parameters that can include live pointers.
  • The constructor stores anything needed by the Do function as public members that are basic types
  • The Do function uses the public member variables and the game model to update the model. It stores any data needed for animation in private variables
  • The Act function uses all member variables to animate the action

EResetGame

EResetGame is the first event of any game and deletes all the existing Players.

[System.Serializable]
public class EResetGame : EngineEvent
{
  public EResetGame() { QUndoable = false; }

  public override void Do(Timeline c)
  {
    Game.theGame.init();
    Game.theGame.CurrentPlayer = null;
    PlayerList.Players.Clear();
    foreach (PlayerGUI pad in GameGUI.theGameGUI.PlayerPads)
      pad.Player = null;
  }
}

EAddPlayer

The EAddPlayer event is created by the MainMenu and creates a new Player class in the model and adds it to the PlayerList. It is constructed with and stores the table position and color of the player to add. These events are always executed like a load, so the Act never gets called.

[System.Serializable]
public class EAddPlayer : EngineEvent
{
  public int TablePositionId;
  public Player.PlayerColors PlayerColor;

  EAddPlayer() { QUndoable = false; }
  public EAddPlayer( int tableId, Player.PlayerColors color )
  {
    TablePositionId = tableId;
    PlayerColor = color;
  }

  public override void Do(Timeline c)
  {
    Player p = new Player() { Position = TablePositionId, Color = PlayerColor };
    PlayerList.Players.Add(p);
  }
}

EInitialize

EInitialize does all the heavy lifting of setting up the game. It sets random seeds, initializes the model and also does any GUI initialization that is needed.

It is important to initialize the random seed in an event. This will allow the random events to happen the same way when a game is loaded or when the events are re-executed after an undo.

It is important to minimize GUI initialization because this event is re-executed anytime someone presses Undo. I typically keep private member variables in the GUI classes to keep track of the currently drawn state of the GUI so that I don’t have to re-create things that haven’t changed.

This is the last event created by the MainMenu when “Start” is pressed. This event should either add the EStartRound event or draw the GUI so that the players can take their first action.

[System.Serializable]
public class EInitialize : EngineEvent
{
  public int? Seed;

  public EInitialize()
  {
    QUndoable = false;
  }

  public override void Do(Timeline c)
  {
    initializeSeeds();
    initializeModel();

    initializeGUI();

    c.addEvent(new EStartRound());
  }

  public void initializeSeeds()
  {
    if( !Seed.HasValue )
      Seed = Mathf.Abs(System.DateTime.UtcNow.Ticks.GetHashCode());

    UnityEngine.Random.InitState(Seed.Value);
    UniExtensions.Rnd.rnd = new System.Random( Seed.Value );
  }

  public void initializeModel()
  {
    Player startPlayer = PlayerList.Players.GetRandom();
    PlayerList.setOrderToClockwiseWithStartAt(startPlayer);
  }

  public void initializeGUI()
  {
    foreach ( PlayerGUI playerGUI in GameGUI.theGameGUI.PlayerPads)
    {
      if (PlayerList.Players.Any(p => p.Position == playerGUI.Position))
      {
        playerGUI.init();
      }
      else
        playerGUI.gameObject.SetActive(false);
    }

    GameGUI.theGameGUI.PlayerPads = 
      GameGUI.theGameGUI.PlayerPads.Where(p => p.gameObject.activeSelf).ToList();

    //GameGUI.theGameGUI.Scoreboard.buildScoreboard();
  }
  public override float Act( bool qUndo )
  {
    GameGUI.theGameGUI.draw();
    return 0;
  }
}

EStartRound

Many games have rounds and setup that happens at the start of each round. In that case, EInitialize adds EStartRound to the Timeline.

public class EStartRound : EngineEvent
{
  public override void Do(Timeline timeline)
  {
    Game.theGame.CurrentPlayer = PlayerList.Players[0];
    Game.theGame.CurrentGameState = Game.GameState.PLAY;
  }
  public override float Act(bool qUndo = false)
  {
    // clear the load overlay
    GameGUI.theGameGUI.LoadOverlay.SetActive(false);
    GameGUI.theGameGUI.GameCanvas.SetActive(true);

    return 0;
  }
}

EEndGame

Most games have a distinct end event that does final actions, final scoring and displays the place Ribbons.

public class EEndGame : EngineEvent
{
  public override void Do(Timeline timeline)
  {
    Game.theGame.CurrentGameState = Game.GameState.GAME_OVER;
  }
  public override float Act(bool qUndo = false)
  {
    WindowsVoice.speak("Game Over.");
    GameGUI.theGameGUI.draw();
    return 0;
  }