Using MongoDB for development of massively multiplayer online (MMO) games

Tuesday, January 11, 2011 » database, MMO, Mongo, MongoDB, NoSQL

Recently I participated in the implementation of  a new MMO (Massively Multiplayer Online) game.  This game is browser/text based, has no concept akin to "rooms," and no direct realtime communications between players although players can perform actions against other players.  In the past, most of the games this team has built have been coded in Flash on the client, Java on the server, and have been implemented atop a socket server.   In this case, due to the type of game, the game was to be implemented in Python directly over a database with no socket server in the mix.  In selecting a database, our main criteria was speed and scalability.  Along the way, we discovered how to gain more.

<!--break-->We selected MongoDB, a document-centric NoSQL database.  MongoDB stores documents instead of rows and columns. Documents are stored in "collections" instead of tables.  Documents are objects (think XML or JSON representations).  This meant that the various objects used in our game, such as player objects, level objects, and item objects could be stored in the database without having to map them to SQL tables.  In essense, our schema is defined by our code.

A powerful attribute of MongoDB's documents is that they can contain complex data structures, such as arrays, maps, and even other documents.  So, for example, a player object can contain arrays of the item objects that they possess.   Player objects can be retrieved all with all their related objects in a single step without performing any join operations on the database. This all makes storage and retrieval of these objects ridiculously simple. 

We performed tests with MongoDB using collections up to 600GB with both a single machine and using  three machines configured as a replica set, and  MongoDB appears to be wicked fast for our application.  Simulations of thousands of users yielded average record retrieval times under 100 milliseconds - roughly 4-6 times faster than a MySQL database we set up for comparison (we tried several different schemas in MySQL to try to optimize retrievals).

Another design decision we made was to use MongoEngine (an ORM - Object Relational Mapper) to map our objects to MongoDB documents.  Use of an ORM allows the programmers to focus more on the logic of the game by relieving them of writing database code (most of it, anyways).  Using MongoEngine, objects are declared, instantiated, assigned, and used.  For example (NOTE: these examples are not from our game, but contrived for this article):

from mongoengine import *

 

class ItemObject(EmbeddedDocument):

    name = StringField(required = True)

    itemType = StringField(required = True)

    itemStrength = IntField(required = True)

 

 

class PlayerObject(Document):

    name = StringField(required = True, unique=True)

    raceName = StringField(required = True, default = "Human")

    strength= IntField(required = True, default = 10)

    dexterity = IntField(required = True, default = 10)

    constitution = IntField(required = True, default = 10)

    intelligence = IntField(required = True, default = 10)

    wisdom = IntField(required = True, default = 10)

    dexterity = IntField(required = True, default = 10)

    hitPoints = IntField(required = True, default = 10)

    weapon = EmbeddedDocumentField(ItemObject)

    defense = EmbeddedDocumentField(ItemObject)

    bag = ListField(EmbeddedDocumentField(ItemObject), default = [])

    meta = { 'indexes' : ['name'] }

 

connect('ddgame', username = 'dddbuser', password = 'secretpassword', host = dbserver, port = 27017)

 

 

newPlayer = PlayerObject(name = "Gandolf")

newPlayer.defense = ItemObject(name = "shield", itemType = "defense", itemStrength = 10)

newPlayer.weapon = ItemObject(name = "sword", itemType = "offense", itemStrength = 10)

newItem1 = ItemObject(name = "magic rock", itemType = "magical", itemStrength = 10)

newItem2 = ItemObject(name = "gemstone", itemType = "valuable", itemStrength = 10)

newPlayer.bag=[newItem1,newItem2]

newPlayer.save()

 

 

Note how PlayerObject.items is a list of embedded documents.  It is this feature of Mongo that allows rapid and efficient retrieval of related objects without performing joins.
 
Retrieval of records is equally easy.  Iterating through the entire list of players and printing their names could be performed by a statement such as:

 

 

for players in PlayerObject.objects:

    print players.name

 

 

While retrieving a single player's record could be performed with this code:

 

 

existingPlayer = PlayerObject.objects(name = "Gandolf").first()

print existingPlayer.name+" is carrying a "+existingPlayer.defense.name </span>

 +" and a "+existingPlayer.weapon.name+"."

 
 
The combination of  MongoEngine ORM and MongoDB make storage and retrieval of objects both simple to program as well as fast and efficient database operations.   There are a number of potential pitfalls in using MongoDB, none of them outweighed the benefits or deterred us from using it, but they are worth discussing here.
 
Schema-less databases, such as MongoDB provide the ability to store objects without regard to schema.  This allows changes to what's saved without any further database redesign or migration of data.   There are a few dangers to this approach, such as errors that could be introduced by changing types of existing structures, but careful management of storage objects (along with a few simple rules, like "don't do that") should be able to mitigate those risks.  Another potential issue over time is orphaned data.  As objects are evolved, it's possible that certain attributes will fall out of use.  We expect this to happen and while we initially do not anticipate creating any special routines in the near future to clean up orphaned attributes we will maintain careful documentation so that the legacy data in the database and it's use (or obsolescence) can be understood.
 
I've heard the argument  made that "schema-less" is "bad" because "there's less documentation on the use or intent of what's in the database."   Through use of the MongoEngine ORM, the schema is very well-defined by the code.  Reading through the objects declared in MongoEngine is arguably much easier than reading a bunch of SQL code, or a visualization of a database as presented through a tool.  
 
Speaking of tools, we found a useful database tool called MongoHub (or a Mac version found here).  It features a reasonably usable graphical user interface and is invaluable for checking and tweaking data within the database.
 
Jumping back to potential pitfalls, perhaps the largest potential pitfall of MongoDB is that, by default it doesn't enforce ACID.  A large part of it's write speed comes from deferred writes and lack of distributed locking.  There are a number of operations  in MongoDB that guarantee atomicity for single document transactions.   MongoEngine facilitates access to them too. We've used them in operations where it's crucial to ensure atomicity.  Although these operations are far slower than "normal" write operations, in some spots, they are crucial to the proper consistency of our application.   Our application requires reads far more often (10x) than writes, so the overall impact on performance is minimal.
 
Overall, MongoDB and MongoEngine have improved our teams productivity and vastly improved our efficiency in database operations.   By eliminating the overhead of explicitly creating and managing schema, and programming persistence with SQL, our team can spend the "gained" time writing the logic of the game.    While not necessarily the right solution for every task, MongoDB in particular and NoSQL databases in general are well-worth consideration.

comments powered by Disqus