Lummox Labs

Mobile app maker since 2015

Action Oriented Gaming in Forbidden Desert

Haven't played Forbidden Desert yet? Grab it here!

In the mood for a puzzle game instead? Try out Noodles!


There are so many posts on the web talking about how to accomplish small tasks in a language, like "How to mask an image in Swift". And lots more that talk about abstract architecture patterns, like Model-View-Controller. This is somewhere in-between. I'm going to describe the super-powerful code pattern that powers Forbidden Desert, and what benefits we get from using it. 

Preamble

Forbidden Desert is a user-driven game, owing to it being a physical board game first. When playing the physical version, humans are literally powering the game, and so in the port it's no surprise that everything that happens is triggered by the user. Many other games, particularly action games are time-based, and more tightly tied to an event loop.

When building FD, there were a bunch of features we knew we needed.

  • Undo - players need to be able to rewind their turn
  • Animations for pretty much everything that happens
  • Online Multiplayer 

And there are so many rules in a board game. There are tons of possible things a player can do on their turn, and lots of different combinations based on a player's Role, so our model needs to be really scalable.

The Action Pattern.

Also known as the Command Pattern, it's a simple idea. The game state is an object, and everything that's part of the game is in that object. The players, their roles, where the tiles are on the board, where the sand is on the tiles, what parts have been already retrived, how much water each player has, and on and on. Everything that together makes up the game at any particular moment is in a Game object (and sub-objects).

class Game {
  var players: [Player]
  var board: Board
  var currentPlayerIndex: Int
  ...
}

Any time the game needs to change it's state - say, when a player moves to a new tile, or a new Storm Card flips over - you don't change the Game object directly. Instead, you create an Action object, and perform it on the Game. The Action is the only entity allowed to change the Game object. Let's look at it in code:

class GameAction {
  var subactions: [GameAction] = []
 
  final func doAction(game:Game) {
    // First perform this action, then all subactions in order.
    self.doActionInternal(game)
    
    for subaction in self.subactions {
      subaction.doAction(game)
    }   
  }
  
  final func undoAction(game:Game) {
    for subaction in self.subactions.reverse() {
      subaction.undoAction(game)
    }
    
    self.undoActionInternal(game)
  }
}

Simple! To change the game you just call doAction(game). To undo it, call undoAction(game). The action changes the game object, and every action knows enough to undo itself.

So now you can split every change to the game into small, focused actions. And for anything complex, notice that the GameAction has a subactions array, so you can group multiple actions together easily.

Let's check out an example of an Action ... in action. Here's how players move around the board, with a MoveAction:

class MoveAction: GameAction {
  var playerId = "player1"
  var fromTileId = "mirage"
  var toTileId = "launchPad"
  
  init(playerId: String, fromTileId: String, toTileId: String) {
    ...
  }
  
  override func doActionInternal(game: Game) {
    // Get the player, move them.
    if let player = game.playerWithId(self.playerId) {
      player.tileId = toTileId
    }
  }
  
  override func undoActionInternal(game: Game) {
    if let player = game.playerWithId(self.playerId) {
      player.tileId = fromTileId
    }
  }
}

And what about something with subactions? Well, let's take a look at another action that uses MoveAction: JetpackAction:

class JetpackAction: GameAction {
  var playerId: String = "player1"
  var alongForTheRidePlayerId: String? = nil
  
  var fromTileId: String = "mirage"
  var toTileId: String = "launchPad"
    
  init(playerId: String, fromTileId: String, toTileId: String, withPlayerId: String? = nil) {
    self.playerId = playerId
    self.fromTileId = fromTileId
    self.toTileId = toTileId
    self.alongForTheRide = withPlayerId
    
    super.init()
  }
  
  override func doActionInternal(game: Game) {
    let allIds = [self.playerId, self.alongForTheRidePlayerId].flatMap{ $0 }
    
    let moveActions = allIds.map{ (moverId) -> GameAction in
      let moveAction = MoveAction(playerId: moverId, fromTileId: self.fromTileId, toTileId: self.toTileId)
      return moveAction
      }
    self.addSubactions(moveActions)
    
    self.addSubaction(RemoveEquipmentAction(playerId: self.playerId, equipment: .Jetpack))
  }
  
  override func undoActionInternal(game: Game) {
    // Nothing for undo, since it's all wrapped in subactions. :)
    // The base GameAction class will handle undoing the subactions.
  }

}
  

Seems like a bit of overhead just to change a few variables in the game, right? Well, yeah. But keep all game changes in these actions, and you get a lot of power out of it. I'll talk about these benefits in more detail in upcoming blog posts.