Using Firebase in a 2D Tile-Based Online Game (MMORPG)

This is part of a free course that teaches game design and the Swift programming language. It contains examples from a real game I created called ForeverMaze. The full course includes the source code for the whole game.

This article will teach a simple way to create a real-time game in Swift (such as a 2D MMORPG). It will build upon the previous article, which set up the data structures.

Buffering the Screen

As I explained in the previous article, one of the hard parts about a 2.5D game like ForeverMaze is the need to load data before it appears on screen (buffering it). This ensures that by the time the object appears, it is already loaded. In that article, I shared code that lets you determine which tiles are on screen. Assuming we already have an onScreenCoordinates  variable in our SKScene class, we want to be able to gracefully load the contents of the screen as the player moves around.

  var loadingTiles:[String:Promise<Void>] = [:]

  func unloadOffScreenObjects() {
    for obj in self.objects.values {
      if !self.isCoordinateOnScreen(obj.coordinate, includeBuffer: true) {
        removeObject(obj)
      }
    }
  }
  
  func unloadOffScreenTiles() {
    for tile in self.tiles.values {
      if !self.isCoordinateOnScreen(tile.coordinate, includeBuffer: true) {
        removeTile(tile)
      }
    }
  }

  //
  // Main `loadTiles` function removes anything offscreen, and loads anything onscreen
  //
  func loadTiles() -> [Promise<Void>] {
    var promises:[Promise<Void>] = []

    // Start by unloading anything off-screen
    self.unloadOffScreenTiles()

    // Then load any new tiles
    for coord in self.onScreenCoordinates {
      let key = coord.description
      if self.tiles[key] != nil {
        continue
      }
      if self.loadingTiles[key] != nil {
        promises.append(self.loadingTiles[key]!)
        continue
      }
      let tile:Tile! = Tile(coordinate: coord, state: TileState.Online)
      let promise = tile.loading.then { (snapshot) -> Void in
        guard snapshot != nil else {
          DDLogWarn("Missing Tile \(key)")
          return
        }
        if !self.isCoordinateOnScreen(tile.coordinate, includeBuffer: true) {
          self.removeTile(tile)
        }
        else if tile.sprite.parent == nil {
          self.addTile(tile)
        }
        self.loadingTiles.removeValueForKey(key)!
      }
      promises.append(promise)
      loadingTiles[key] = promise
    }
    return promises
  }

This code works well because it is minimal. It only loads what needs to be loaded, and keeps track of what is already loading (to avoid double-loading). Since it returns an array of promises, you can wrap it in a when(loadTiles())  call and find out when the screen is ready. However, in most cases simply calling the function each time the player takes a step (and the center  coordinate moves) is sufficient.

Note: I did a lot of experimentation with this, and I’m afraid to say that I don’t think Firebase will perform well under high stress scenarios. If your player can move quickly, or if the game is very zoomed out, it causes the number of tiles which need to be loaded to grow rather large. Check out my StackOverflow topic on this problem, which concluded that Firebase is simply insufficient for this kind of use-case.

When Tiles Contain Objects

Sometimes, objects (like other players) reside on a tile. So the Tile objects in Firebase need to contain references to all of the objects they contain. This preserves the denormalized, flattened structure of Firebase’s tree hierarchy.

After we’ve loaded a tile and we’re drawing it on screen, we can iterate over the objectIds it contains and load them up, too. It looks something like this:

func loadObjectsInTile(tile: Tile) {
    for path in tile.objectIds {
      if self.objects[path] != nil || !self.shouldLoadObjectID(path) {
        continue
      }
      self.loadObject(path).then { (gameObject) -> Void in
        guard gameObject != nil else {
          DDLogWarn("Failed to load object \(path)")
          return
        }
        self.addObject(gameObject!)
      }
    }
  }

Handling Other Players’ Movement

When another player moves, we need to update his position locally and animate the movement. The previous article already covered how we can detect changes, such as movement in the x/y coordinates of another player.

When we detect that x/y coordinates have changed for some object, there are three possible scenarios. The first it was on the screen but left the screen. The second is that it was on the screen and moved around the screen. The third is that it was not on the screen but is now on the screen. You can see how I handle each, here:

  func removeObject(object: GameObject) {
    object.cleanup()
    self.objects.removeValueForKey(object.id)
  }

  func onObjectMoved(object: Mobile) {
    guard object != Account.player else {
      // The local player is moved via checkStep()
      return
    }

    if !self.isCoordinateOnScreen(object.coordinate, includeBuffer: true) {
      // Not on screen? Just remove it.
      if object.allowsForDynamicUnloading {
        removeObject(object)
      }
    }
    else if self.objects.keys.contains(object.id) {
      // Moving from one tile to another...
      self.animateObjectMovement(object)
    }
    else {
      // Newly on screen! Just add it.
      addObject(object)
      self.onObjectFinishedMoving(object)
    }
  }

The animation functions are pretty simple, as well…

  private var objectsMovingTo:[String:CGPoint] = [:]

  func isObjectMoving(object: Mobile) -> Bool {
    return (self.objectsMovingTo[object.id] != nil && !object.sprite.hidden && self.isCoordinateOnScreen(object.coordinate, includeBuffer: true))
  }

  func onObjectFinishedMoving(object: Mobile) {
    object.sprite.zPosition = self.zPositionForYPosition(object.sprite.position.y, zIndex: 10)
    self.objectsMovingTo.removeValueForKey(object.id)
    object.updateAnimation()
  }

  func animateObjectMovement(object: Mobile) {
    let time = Config.stepTime / object.speed
    let point = coordinateToPosition(object.coordinate, closeToCenter: true, includeHeight: true)

    let alreadyMovingTo = self.objectsMovingTo[object.id]
    if alreadyMovingTo == nil || alreadyMovingTo! != point {
      object.sprite.removeActionForKey(Animation.movementKey)
      self.objectsMovingTo[object.id] = point
      object.sprite.runAction(SKAction.sequence([
        SKAction.runBlock(object.updateAnimation),
        SKAction.moveTo(point, duration: time),
        SKAction.runBlock({ () -> Void in
          self.onObjectFinishedMoving(object)
        })
      ]), withKey: Animation.movementKey)
    }
  }

 

If you enjoyed this post, why not enroll in the free course? It contains hours of video content, source code downloads, examples and more. I cover many different intermediate and advanced topics for creating a real-time game in Swift with SpriteKit and Firebase. You can check out the complete game at ForeverMaze.com.

[author]

Leave A Comment

You must be logged in to post a comment.

Back to Top