Lummox Labs

Mobile app maker since 2015

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!