Kotan Code 枯淡コード

In search of simple, elegant code

Menu Close

Building an MMO Server in Akka : Using MongoDB as a Backing Store

The story so far: I’ve created an Akka application that is using non-blocking Akka IO over sockets to communicate via Google Protocol Buffers with iOS (or potentially others…) clients. In the last blog post, I created some Actors that allow objects that have a physical presence in space to respond to messages, namely radar pings. This allows objects to “appear” to players. Their client will get radar pong messages, which will allow the client GUI to create icons for the items in the radar pong like planets, star ports, ships (owned players or NPCs), harvestable asteroids, and so on.

That’s all well and good, but in the last blog post, I had to hard code things like the name, description, and location of the objects in the universe. The first thing I needed to do was to refactor the PhysicalObject trait … in the last iteration it had “magic knowledge” of the types of physical objects that might be using that trait. So here’s my new physical object:

package com.kotancode.ulysses.space

import com.kotancode.ulysses.space.Vector3D._
import akka.actor._

case class RadarPing(source: ActorRef, origin: Vector3D, scanDistance: Int)
case class RadarPong(source: ActorRef, origin: Vector3D, objectType: Int, name:String, description: String)

object RadarObjectTypes {
	val Planet = 1
	val Station = 2
	val Port = 3
	val Ship = 4

	val Unknown = 99
}

trait PhysicalObject extends Actor with ActorLogging {
	def replyPong(p:RadarPing): Option[RadarPong]

	def receiveSpatial: Receive = {
		case p:RadarPing => {
			replyPong(p).map { pongReply => p.source ! pongReply }
		}
	}
}

There’s a couple of new things happening here. First, this trait no longer has a member variable called location. Second, in the last iteration, this trait constructed the pong reply itself. This would have been really problematic because I would have ended up with one method with a pile of “switch” cases to determine how it should reply in different circumstances… and that would’ve been smelly. Now, it is up to the actor using this trait to define a method that creates pong replies. Also, note that the abstract method that the concrete actors will implement returns an Option so I can just run a map over that – if the method returns a None then the above code won’t deliver the pong message.

Now that I’ve cleaned up my physical objects (to make them easier to be instantiated dynamically based on data from a backing store like Mongo), I can create a Persistable trait:

package com.kotancode.ulysses.state

import akka.actor._
import com.mongodb.casbah.Imports._
import org.bson.types.ObjectId

case class LoadState(id:String)
case object PersistState

object BackingStoreKeys {
	val Name = "name"
	val Location = "location"
	val Description = "description"
	val ID = "_id"
}

trait Persistable extends Actor {
	var backingObject: MongoDBObject = null

	def receivePersistable: Receive = {
		case LoadState(id) => {
			loadFromMongo(id)
		}
		case PersistState => {
		  // Nothing here yet...
		}
	}

	def persistenceCollectionName:String

	def loadFromMongo(id:String) = {
		val mongoClient = MongoClient()
		val ulysses = mongoClient("ulysses")
		val collection = ulysses(persistenceCollectionName)
		val objectId = new ObjectId(id)
		backingObject = collection.findOne(MongoDBObject(BackingStoreKeys.ID -> objectId)).getOrElse { throw new IllegalArgumentException(s"id ${id} not found in ${collection}")}
		println(s"backing object ${backingObject}")
	}
}

Here I’m using the Casbah library for Scala to talk to MongoDB. What’s worth mentioning here is that the only way a persistable actor should load its state is after it has received a message to do so. This keeps the data-loading activity in the thread-safe message pattern already enforced by Akka, so I don’t have to worry about race conditions where I have to delay some activity until after my reference data has been loaded (this is a common problem I see regularly in message-based applications).

Let’s see what our new, cleaner Planet actor looks like now:

package com.kotancode.ulysses.space

import com.kotancode.ulysses.space.Vector3D._
import com.kotancode.ulysses.state.Persistable
import com.kotancode.ulysses.state.BackingStoreKeys
import com.mongodb.casbah.Imports._

import akka.actor._

object Planet {
	val BackingCollectionName = "planets"
}

class Planet extends Actor with PhysicalObject with Persistable {

	override def persistenceCollectionName = Planet.BackingCollectionName

	override def replyPong(ping:RadarPing): Option[RadarPong] = {
		val currentLocation = Vector3D.fromBackingObject(backingObject.as[BasicDBObject](BackingStoreKeys.Location))
		if ( (currentLocation distanceFrom ping.origin) < ping.scanDistance) {
			Some(RadarPong(self,
				currentLocation,
				RadarObjectTypes.Planet,
				backingObject.as[String](BackingStoreKeys.Name),
				backingObject.as[String](BackingStoreKeys.Description)
			))
		}
		else {
			None
		}
	}

	def receivePlanet: Receive = {
		case _ => log.debug("Received some message for a planet.")
	}

	def receive = {
		receivePersistable orElse receiveSpatial orElse receivePlanet
	}
}

In my Mongo database, the location field contains a nested object with the properties x, y, and z. The Vector3D class has a utility method on it (I will refactor this into a “pimp” or “augment” pattern later) that converts a Casbah BasicDBObject into a 3D vector by extracting these properties.

Finally, remember that Universe class? The one that hard-coded the instantiation of the “Utopia 1” planet? Here’s what it looks like now:

package com.kotancode.ulysses.space
import com.kotancode.ulysses.state.LoadState
import com.kotancode.ulysses.state.Repository

import akka.actor._

class Universe extends Actor with ActorLogging {

	def loadPlanets = {
		for (id <- Repository.allIds(Planet.BackingCollectionName)) {
			context.actorOf(Props(new Planet()), name=id) ! LoadState(id)
		}
	}
	override def preStart() = {
		loadPlanets
	}

	def receive = {
		case p:RadarPing => context.children foreach (_.forward(p))
		case _ => log.debug("Received a universe message")
	}
}

Here I’m just making use of a simple object I created called Repository that issues a Casbah query to Mongo – it’s an empty query on a collection so it returns all the objects and then converts the Object IDs into strings and yields them so they can be used in an enumerator, as shown in the above code.

Now, instead of having a hard coded planet, I can create multiple planets in my database and this code will instantiate an actor for each of those planets, each planet will be backed by its own unique Mongo DB document. Here’s some trace log output that shows a pong reply from each of the 3 planets I created in my MongoDB:

[DEBUG] [03/24/2013 08:31:12.998] [UlyssesAgenda-akka.actor.default-dispatcher-1] [akka://UlyssesAgenda/user/ServerCore/Clients/96e1e42b-5cda-4faa-aa89-019c2bde9794] I received a radar pong RadarPong(Actor[akka://UlyssesAgenda/user/ServerCore/Universe/514de823b485acb28e02bb91],(5.0,5.0,5.0),1,Utopia 1,This is a utopian planet, chock full of win.)

[DEBUG] [03/24/2013 08:31:12.999] [UlyssesAgenda-akka.actor.default-dispatcher-1] [akka://UlyssesAgenda/user/ServerCore/Clients/96e1e42b-5cda-4faa-aa89-019c2bde9794] I received a radar pong RadarPong(Actor[akka://UlyssesAgenda/user/ServerCore/Universe/514e194004538147414b4675],(6.0,6.0,6.0),1,Utopia 2,This is a little less utopian than the other one.)

[DEBUG] [03/24/2013 08:31:12.999] [UlyssesAgenda-akka.actor.default-dispatcher-1] [akka://UlyssesAgenda/user/ServerCore/Clients/96e1e42b-5cda-4faa-aa89-019c2bde9794] I received a radar pong RadarPong(Actor[akka://UlyssesAgenda/user/ServerCore/Universe/514e19b304538147414b4676],(7.0,7.0,7.0),1,Doom,Every game needs a planet doom.)

Now that I’ve got the core framework for persistence up and running, I can do some more useful things like giving players persistence so I can keep their names and locations separate. Once I have that, I can move on to creating the most basic of the actual gameplay functions.