Lummox Labs

Mobile app maker since 2015

Filtering by Tag: Forbidden Desert

Action Animation in Forbidden Desert

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

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


In the last few posts I've discussed how Forbidden Desert uses the Action pattern, with GameAction objects, to organize game changes in the app.

Here I'm going to look at another huge part of the app, and a part that Actions drive: Animation.

The vast majority of the visual changes during a game of FD come from changes in the game itself: A player moves to a new tile, sand piles on a tile, the storm rages across the screen, etc. All those animations are directly tied to changes in the game's state. And all game state changes are directly tied to GameAction objects, so it's natural to use those Actions to organize all the animations (and there are a lot of them).

Here's the basic idea in code:

class GameViewController: UIViewController {
...
  func doActionAndUpdate(action: GameAction, completion: (() -> ())? = nil) {
    // Change the game state
    action.doAction(self.game) 
    
    // Animate!
    self.animationManager.animateAction(action,
      controller: self,
      game: self.game,
      isUndo: isUndo,
      completion: completion)
  }
}

class AnimationManager {
...
  func animateAction(action: GameAction, controller: GameViewController, game: Game, isUndo: Bool, completion: (() -> ())?) {
    
    let completionOperation = NSBlockOperation(block: { () -> Void in
      if let completionUnwrap = completion {
        dispatch_async(dispatch_get_main_queue()) { completionUnwrap(areThereMoreActions: false)}
      }
    })
    
    if let animateOperation = action.animationOperation(controller, game: game, isUndo: isUndo) {
      completionOperation.addDependency(animateOperation)
      self.animationQueue.addOperation(animateOperation)
    }
    
    self.animationQueue.addOperation(completionOperation)
  }
}

So! Animations all happen with NSOperations, which provide a very flexible way to organize them. Sometimes we'll need to show 2 animations simultaneously, and sometimes we'll need to show 2 animations serially, and that logic is all wrapped up in an NSOperation, based on the specific needs of that GameAction.

The most important part of the code is this:

let animateOperation = action.animationOperation(controller, game: game, isUndo: isUndo)

You can see that each action is responsible for creating an NSOperation that fully contains all the related animations. Let's check out what that looks like:

extension GameAction {
...
  func animationOperation(controller: GameViewController, game: Game, isUndo:Bool) -> NSOperation? {
    
    // Get the animation for the main part of the GameAction.
    let operationOpt: NSOperation? = (self as? FDAnimatable)?.animationOperationInternal(controller, game: game, isUndo: isUndo)
    
    // Recursively get all animations for SubActions.
    let subactionOpt: self.subactionOperation(controller, game: game, isUndo: isUndo)
    
    // Combine them all into one!
    let operations = [operationOpt, subactionOpt].flatMap{ $0 }
    return MultiOperationOperation(operations: operations)
  }
    
  func subactionOperation(controller: GameViewController, game: Game, isUndo:Bool, serialized: Bool = false) -> NSOperation? {
    let subOperations = subactions.flatMap {
      $0.animationOperation(controller, game: game, isUndo: isUndo)
    }
    
    let subactionOperation = MultiOperationOperation(operations: subOperations, serialized: serialized)
    return subactionOperation
  }
}

Each GameAction specifies its own animations, and is given complete flexibility to do so. In the simple case, an action can ignore animating subactions, since those GameAction classes will define their own animation NSOperations.

A couple of notes about the code:

FDAnimatable is a protocol that's used here to allow GameAction subclasses to optionally provide an operation. When this was written, protocols were much less powerful, so there may be a more elegant way to accomplish this in Swift 2.

MultiOperationOperation is a brilliantly-named custom class that just groups other operations together, and finishes when all the suboperations have finished.

So what does this look like for actual actions?

extension JetpackAction: FDAnimatable {

  func animationOperationInternal(controller: GameViewController, game: Game, isUndo: Bool) -> NSOperation? {
    
    let animateOperation = MainThreadAsyncOperation { (operation) -> Void in
      controller.playerViewWithId(self.playerId).putJetpackAway() {
        operation.finish()
      }
    }
    
    return animateOperation
  }
}

That's ... quite short. But other stuff happens when a player uses the jetpack. Most importantly, the player moves to a new tile. But remember that JetpackAction has subactions, one of which is a MoveAction. So MoveAction will define its own animation, and it gets rolled up automatically into the animation NSOperation for JetpackAction. The great thing about this is MoveAction can be a subaction of different GameActions, and this behavior means in all those cases, we'll see the player move between tiles.

If you're interested, here's MoveAction:

extension MoveAction: FDAnimatable {
  func animationOperationInternal(controller: GameViewController, game: Game, isUndo:Bool) -> NSOperation? {
    
    let animateOperation = MainThreadAsyncOperation(mainThreadBlock: { (operation: MainThreadAsyncOperation) -> Void in
    
      let playerView = controller.playerViewWithId(self.playerId)
    
      UIView.animateWithDuration(0.3,
        animations: { () -> Void in
          playerView.center = // Position on the new tile.
        },
        completion: { _ -> Void in
          operation.finish()
      })
    })
    
    return animateOperation
  }
  
}

Once that architecture is in place, you have total flexibility on how to make actions look in the UI. They compose together nicely, and it's easy enough to add code to do the same thing with sounds in the game, achievements, etc. 

And that's how Forbidden Desert does animations!

Game History with Actions in Forbidden Desert

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

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


This post is one in a series about the Action pattern used in Forbidden Desert on iPad.

So in the previous post we looked at what an Action is, and what it looks like in Swift. Now let's talk about one of the great benefits the game gets from the Action pattern.

The actions have these two methods:

final func doAction(game:Game) -> ()
final func undoAction(game:Game) -> ()

Which are super-easy to use for any Action.

let action = ExcavateAction(tileId: "equip0")
action.doAction(game)
...
action.undoAction(game)

So how can we use this to make an easy game history? Well, let's add a list of everything that's changed the game.

class Game {
  var actionHistory: [GameAction] = [] 
  ...
}

And now every time you perform an action, add it to the history.

let action = ExcavateAction(tileId: "equip0")
action.doAction(game)
game.actionHistory.append(action)

Now you've got enough information to rewind the entire game right to the beginning, action by action. Just keep pulling the last action in the list, and calling undoAction(game). 

You could add a slider to your game that lets a user slide back and forth in history, or easily get a count of how many moves there ever were. There's lots of power hidden in that list of actions.

But the biggest win is just giving the user the ability to Undo to the beginning of their turn, with confidence that the game state won't get messed up with so much change flying around. As long as each action is internally consistent (that undoActionInternal perfectly reverses doActionInternal) then the game will stay consistent too.

We'll take a look in future blog posts at some of the other benefits to the Action pattern.

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.