Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building the game “2048” in Swift

Avatar for Realm Realm
July 24, 2014

Building the game “2048” in Swift

As given by Austin Zheng at the Swift Language User Group

Watch the video: http://realm.io/news/swift-2048/

Avatar for Realm

Realm

July 24, 2014
Tweet

More Decks by Realm

Other Decks in Technology

Transcript

  1. Who am I? • My name is Austin Zheng •

    I work at LinkedIn as an iOS developer • Before that I wrote firmware for an embedded systems startup in Redwood City
  2. What Is 2048? • swift-2048 is a port of an

    Objective- C version of 2048 I wrote… • …which was a clone of the web game “2048” by Gabriele Cirulli • In turn based on iOS game “Threes” by Asher Vollmer • Slide in a direction to combine like tiles • Make a ‘2048’ tile, or fill up the board and lose
  3. Architecture (very high level) View Game Logic (Model) View Controller

    Actions View Commands (forwarded to view) Backing Store
  4. Backing Store (Old) @interface F3HGameModel () @property (nonatomic, strong) NSMutableArray

    *gameState; @property (nonatomic) NSUInteger dimension; //... @end
  5. Backing Store struct SquareGameboard<T> { var boardArray: [T]; let dimension:

    Int init(dimension d: Int, initialValue: T) { dimension = d boardArray = [T](count:d*d, repeatedValue:initialValue) } subscript(row: Int, col: Int) -> T { get { return boardArray[row*dimension + col] } set { boardArray[row*dimension + col] = newValue } } }
  6. Structs • Like classes, they can have properties and methods.

    • Unlike classes, structs can’t inherit from other structs. • Unlike classes, structs are value types
  7. Generics struct SquareGameboard<T> { let dimension: Int var boardArray: [T]

    init(dimension d: Int, initialValue: T) { dimension = d boardArray = [T](count:d*d, repeatedValue:initialValue) } }
  8. Subscripts subscript(row: Int, col: Int) -> T { get {

    return boardArray[row*dimension + col] } set { boardArray[row*dimension + col] = newValue } } gameboard[x, y] = TileObject.Empty let someTile = gameboard[x, y]
  9. TileModel (Old) // This is an Objective-C class which represents

    a tile @interface F3HTileModel : NSObject @property (nonatomic) BOOL empty; @property (nonatomic) NSUInteger value; @end
  10. TileObject enum TileObject { case Empty case Tile(value: Int) }

    let anEmptyTile = TileObject.Empty let eightTile = TileObject.Tile(value: 8) let anotherTile = TileObject.Tile(value: 2)
  11. Swift Enums • They can do everything C or Objective-C

    enums can… • They can also do everything structs in Swift can do - methods and properties… • Optionally, you can have an enum value store associated data. (variants, tagged unions, sum types, case classes)
  12. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  13. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  14. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  15. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  16. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  17. func performMove(direction: MoveDirection) -> Bool { func coordinateGenerator(currentRowOrColumn: Int) ->

    [(Int, Int)] { var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, currentRowOrColumn) case .Down: buffer[i] = (self.dimension - i - 1, currentRowOrColumn) case .Left: buffer[i] = (currentRowOrColumn, i) case .Right: buffer[i] = (currentRowOrColumn, self.dimension - i - 1) } } return buffer } for i in 0..<dimension { let coords = coordinateGenerator(i) let tiles = coords.map() { (c: (Int, Int)) -> TileObject in let (x, y) = c return self.gameboard[x, y] } let orders = merge(tiles) // ... } // ... }
  18. A single row… • Condense the row to remove any

    space - some might move, some might stay still • Collapse two adjacent tiles of equal value into a single tile with double the value • Convert our intermediate representation into ‘Actions’ that the view layer can easily act upon
  19. Tracking changes? • We want to know when a tile

    is moved, when it stays still, when it’s combined • Let’s posit an ActionToken • An ActionToken lives in an array. Its position in the array is the final position of the tile it represents • An ActionToken also tracks the state of the tile or tiles that undertook the action it describes
  20. ActionToken (old) typedef enum { F3HMergeTileModeNoAction = 0, F3HMergeTileModeMove, F3HMergeTileModeSingleCombine,

    F3HMergeTileModeDoubleCombine } F3HMergeTileMode; @interface F3HMergeTile : NSObject @property (nonatomic) F3HMergeTileMode mode; @property (nonatomic) NSInteger originalIndexA; @property (nonatomic) NSInteger originalIndexB; @property (nonatomic) NSInteger value; + (instancetype)mergeTile; @end
  21. ActionToken enum ActionToken { case NoAction(source: Int, value: Int) case

    Move(source: Int, value: Int) case SingleCombine(source: Int, value: Int) case DoubleCombine(source: Int, second: Int, value: Int) }
  22. func condense(group: [TileObject]) -> [ActionToken] { var tokenBuffer = [ActionToken]()

    for (idx, tile) in enumerate(group) { switch tile { case let .Tile(value) where tokenBuffer.count == idx: tokenBuffer.append(ActionToken.NoAction(source: idx, value: value)) case let .Tile(value): tokenBuffer.append(ActionToken.Move(source: idx, value: value)) default: break } } return tokenBuffer; }
  23. func condense(group: [TileObject]) -> [ActionToken] { var tokenBuffer = [ActionToken]()

    for (idx, tile) in enumerate(group) { switch tile { case let .Tile(value) where tokenBuffer.count == idx: tokenBuffer.append(ActionToken.NoAction(source: idx, value: value)) case let .Tile(value): tokenBuffer.append(ActionToken.Move(source: idx, value: value)) default: break } } return tokenBuffer; }
  24. Swift ‘switch’ • At its most basic, works like the

    C or Objective-C switch statement • But it can do far more! • One example: take the values out of an enum • Cases can be qualified by ‘where’ clauses • Has to be comprehensive, and no default fallthrough
  25. func collapse(group: [ActionToken]) -> [ActionToken] { func quiescentTileStillQuiescent(inputPosition: Int, outputLength:

    Int, originalPosition: Int) -> Bool { return (inputPosition == outputLength) && (originalPosition == inputPosition) } var tokenBuffer = [ActionToken]() var skipNext = false for (idx, token) in enumerate(group) { if skipNext { skipNext = false continue } switch token { case .SingleCombine: assert(false, "Cannot have single combine token in input") case .DoubleCombine: assert(false, "Cannot have double combine token in input") case let .NoAction(s, v) where (idx < group.count-1 && v == group[idx+1].getValue() && quiescentTileStillQuiescent(idx, tokenBuffer.count, s)): let nv = v + group[idx+1].getValue() skipNext = true tokenBuffer.append(ActionToken.SingleCombine(source: next.getSource(), value: nv)) case let t where (idx < group.count-1 && t.getValue() == group[idx+1].getValue()): let next = group[idx+1] let nv = t.getValue() + group[idx+1].getValue() skipNext = true tokenBuffer.append(ActionToken.DoubleCombine(source: t.getSource(), second: next.getSource(), value: nv)) case let .NoAction(s, v) where !quiescentTileStillQuiescent(idx, tokenBuffer.count, s): tokenBuffer.append(ActionToken.Move(source: s, value: v)) case let .NoAction(s, v): tokenBuffer.append(ActionToken.NoAction(source: s, value: v)) case let .Move(s, v): tokenBuffer.append(ActionToken.Move(source: s, value: v)) default: break } } return tokenBuffer }
  26. Game Logic • Convert - create ‘move orders’ for the

    view enum MoveOrder { case SingleMoveOrder(source: Int, destination: Int, value: Int, wasMerge: Bool) case DoubleMoveOrder(firstSource: Int, secondSource: Int, destination: Int, value: Int) }
  27. func convert(group: [ActionToken]) -> [MoveOrder] { var moveBuffer = [MoveOrder]()

    for (idx, t) in enumerate(group) { switch t { case let .Move(s, v): moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx, value: v, wasMerge: false)) case let .SingleCombine(s, v): moveBuffer.append(MoveOrder.SingleMoveOrder(source: s, destination: idx, value: v, wasMerge: true)) case let .DoubleCombine(s1, s2, v): moveBuffer.append(MoveOrder.DoubleMoveOrder(firstSource: s1, secondSource: s2, destination: idx, value: v)) default: break } } return moveBuffer }
  28. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  29. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  30. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  31. func insertTile(pos: (Int, Int), value: Int) { let (row, col)

    = pos let x = tilePadding + CGFloat(col)*(tileWidth + tilePadding) let y = tilePadding + CGFloat(row)*(tileWidth + tilePadding) let r = (cornerRadius >= 2) ? cornerRadius - 2 : 0 let tile = TileView(position: CGPointMake(x, y), width: tileWidth, value: value, radius: r, delegate: provider) tile.layer.setAffineTransform(CGAffineTransformMakeScale(tilePopStartScale, tilePopStartScale)) addSubview(tile) bringSubviewToFront(tile) UIView.animateWithDuration(tileExpandTime, delay: tilePopDelay, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in // Make the tile 'pop' tile.layer.setAffineTransform(CGAffineTransformMakeScale(self.tilePopMaxScale, self.tilePopMaxScale)) }, completion: { (finished: Bool) -> Void in // Shrink the tile after it 'pops' UIView.animateWithDuration(self.tileContractTime, animations: { () -> Void in tile.layer.setAffineTransform(CGAffineTransformIdentity) }) }) }
  32. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  33. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  34. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  35. Selectors UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(upButtonTapped)]; upSwipe.numberOfTouchesRequired =

    1; upSwipe.direction = UISwipeGestureRecognizerDirectionUp; [self.view addGestureRecognizer:upSwipe]; - (void)upButtonTapped { [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) { if (changed) [self followUp]; }]; }
  36. Selectors in Swift let upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:"))

    upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe) @objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }
  37. Selectors in Swift let upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:"))

    upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe) @objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }
  38. Selectors in Swift let upSwipe = UISwipeGestureRecognizer(target: self, action: Selector("up:"))

    upSwipe.numberOfTouchesRequired = 1 upSwipe.direction = UISwipeGestureRecognizerDirection.Up view.addGestureRecognizer(upSwipe) @objc(up:) func upCommand(r: UIGestureRecognizer!) { assert(model != nil) let m = model! m.queueMove(MoveDirection.Up, completion: { (changed: Bool) -> () in if changed { self.followUp() } }) }