Amazon Game Tech Blog

Game Developers Guide to Getting Started with Amazon DynamoDB

 

We all know that a database is an integral part of many games. But, as a game developer, you want to dedicate all your time and expertise to building great games, not engineering databases.

I get it, I’d much rather worry about fixing collision volumes, getting my frame rate up or making the perfect control system, rather than thinking about data storage. So let’s make it easy. In this article I’m going to show you how simple it is to add a database to your game using Amazon DynamoDB, a fast and flexible NoSQL service from AWS.

DynamoDB is a non-relational database that associates keys with data. If you’re not familiar with databases, especially their use in games, take a look at this article, Managed Databases for Awesome Games, which talks about some of the basic uses of databases in games and shows the difference between relational and non-relational databases (sometimes referred to as SQL and NoSQL).

Creating the Dynamo DB table

DynamoDB uses the term table, borrowed from SQL databases, though it’s a NoSQL database. A table in this case is really just a definition of the key features of an item to be stored in the database.

In DynamoDB there is always a primary key for a table to identify the table, and this key must be unique for every item stored in the database.

A useful tip is that a primary key can be a hash of multiple values. So if don’t have a single unique attribute, you can combine different attributes to make a unique value. For example, an item that has a name and a version number. There might be 10 items named “broadsword”, and 100 items that are “version 1” but there is only one “broadsword version 1”.

DynamoDB also offers optional sort keys which allow you to quickly sort items. For example, a sort key for players might be player class. This would accelerate queries where you want to get a list of all the fighter classes in the game. You can also have secondary keys which provide alternate ways to uniquely identify items in the database.

For this demo, I’m going to create a very simple object which represents the attributes of a player character. It will include a unique player ID, the player level, and two stats; strength and intellect. The player ID will be the primary key, and there won’t be any sort keys or secondary keys for this demo.

Note: If you’d like to practice on your own, you could add a player class which would act as a sort key.

The steps to create the DynamoDB table are as follows:

  1. Open the AWS Console.
  2. Search for DynamoDB and select it.
  3. Choose “Create table” from the DyanamoDB console home page.
  4. Name the table “PlayerData”.
  5. Set the primary key to “PlayerID” and leave the data type as “string”.
  6. Use default settings.
  7.  Hit create, the console will spin for a few seconds and voila, you have a Dynamo DB table!

It’s worth noting that all of the steps in creating the table could also be done via code via the AWS SDK so you can have an automatic way to create your services.

Creating the game server

Note that for security reasons, you don’t want to have a game client talk directly to DynamoDB. Instead, we’re going to create a game server which will make requests to DynamoDB. The game client will communicate with the game server and ask it to make DynamoDB queries on its behalf. This model makes it easy to limit what data is available to players.

For simplicity, I’ve used an admin credential file on the machine. Information on creating this file can be found here and details on where to put the credentials is documented here. In practice, you don’t want to deal with credential files. Refer to this article to learn about using credential providers from Amazon Cognito.

Before completing the steps below, you’ll need to add the DynamoDB SDK to your project. You can do this with NuGet (the DynamoDB SDK is named AWSSDKCPP-DynamoDB), or you can build the source and add the library to your project. Go this this link for details. That article is also a good read if you’re not familiar with using the AWS SDK’s. I’ll skip some of the SDK setup in this article as it’s covered by the linked article.

The first step is to create some fake player data so we have something to operate on. Since you don’t have any actual players for this sample, and you don’t want you to have to type in 1000 entries in to a database, let’s make a function to populate the database with randomly generated players.

To follow along, full source code to the project can be found here.

    bool PopulateDatabases()
    {
        ... snip ...
        // Create a bunch of random characters
        const int NUMBER_OF_CHARACTERS_TO_CREATE{ 1000 };
		list<PlayerDesc> newPlayerChunk;
		for (int chrIdx{ 0 }; chrIdx < NUMBER_OF_CHARACTERS_TO_CREATE; ++chrIdx)
		{
            PlayerDesc newPlayer;
            newPlayer.id = GetPlayerIDForInt(chrIdx);
            newPlayer.level = GenerateRandomLevel();
            newPlayer.strength = GenerateRandomStat();
			newPlayer.intellect = GenerateRandomStat();

			newPlayerChunk.push_back(newPlayer);
			if (newPlayerChunk.size() == MAX_DYNAMODB_BATCH_ITEMS)
			{
				cout << "Sending player chunk to DynamoDB..." << endl;
				SendPlayerChunkToDynamoDB(newPlayerChunk);
                newPlayerChunk.clear();
            }
        }
		return true;
    }

In this loop 1000 random players are created. You don’t want to have to make 1000 calls to DynamoDB, that would be inefficient. DynamoDB allows batching of some commands, up to 25 in one API call. So break your generated players in to groups of 25 and send them to the function that will send the data to DynamoDB.

Tip: This code was written on the server in to demonstrate how to do batch calls. If you need to write a function to do a lot of reads or writes to DynamoDB, you should write an AWS Lambda function to do this for you. It’ more efficient, and won’t incur any data transfer costs!

	void SendPlayerChunkToDynamoDB(const list<PlayerDesc>& playerChunk)
	{
		vector<Aws::DynamoDB::Model::WriteRequest> writeRequests;
		for (const auto& chunkItem : playerChunk)
		{
			Aws::DynamoDB::Model::AttributeValue avID;
			avID.SetS(chunkItem.id);
			Aws::DynamoDB::Model::AttributeValue avLevel;
			avStrength.SetN(to_string(chunkItem.level));
            ... snip ...

			Aws::DynamoDB::Model::PutRequest putRequest;
			putRequest.AddItem(DATA_KEY_ID, avID);
            putRequest.AddItem(DATA_KEY_ID, avLevel);		
            ... snip ... 

			Aws::DynamoDB::Model::WriteRequest curWriteRequest;
			curWriteRequest.SetPutRequest(putRequest);
			writeRequests.push_back(curWriteRequest);
		}

		Aws::DynamoDB::Model::BatchWriteItemRequest batchWriteRequest;
		batchWriteRequest.AddRequestItems(PLAYER_DATA_TABLE_NAME, writeRequests);
		... snip ...
        }

In the loop of this function each player description is stored in an AttributeValue, which is a representation of the data we want to store in the item. The attribute is then added to a PutRequest along with the key you want to associate the attribute value with. The PutRequests are then added to the WriteRequest (since this is a batch.)

Tip: This demo assumes the write will succeed. In real life, when under load, DynamoDB may not be able to accommodate all the write requests you send. If that’s the case, the call will succeed, but will send you a list of items that weren’t written. If this is the case, you should go through this list and send them again for writing. Then check that result, and keep going until everything has been written. This should be done with time in between calls, using exponential backoff.

   void SetPlayerAttribueValue(const string& ID, const string& attributeKey, int newValue)
   {
        Aws::DynamoDB::Model::UpdateItemRequest updateItemRequest;
        updateItemRequest.SetTableName(PLAYER_DATA_TABLE_NAME);

        Aws::DynamoDB::Model::AttributeValue avID;
        avID.SetS(ID);
        updateItemRequest.AddKey(DATA_KEY_ID, avID);

        string updateExpression = "SET ";
        updateExpression += attributeKey;
        updateExpression += " = :l";
        updateItemRequest.SetUpdateExpression(updateExpression);
        
        Aws::DynamoDB::Model::AttributeValue av;
        av.SetN(to_string(newValue));
        map<string, Aws::DynamoDB::Model::AttributeValue> attributeValues;
        attributeValues[":l"] = av;
        updateItemRequest.SetExpressionAttributeValues(attributeValues);

        auto outcome{ s_DynamoDBClient->UpdateItem(updateItemRequest) };
        ... snip ...
   }

Once the values are in the system, and players increase their power by playing the game, you’ll need to update the database to reflect the increase. This is done using an “update expression.” Here, we specify the table to update, and then the ID of the player to update. Then create an expression using the expression syntax, with “:l” as the variable. After, the variable is filled in with the appropriate value.

Note: if you’ve used DynamoDB in the past, it’s worth noting that there was a different method of making direct updates. This method has been deprecated in favor of using update expressions.

    bool GetPlayerDesc(const string& ID, PlayerDesc& playerDesc)
    {
        Aws::DynamoDB::Model::QueryRequest queryRequest;
        queryRequest.SetTableName(PLAYER_DATA_TABLE_NAME);
        string conditionExpression{ DATA_KEY_ID };
        conditionExpression += " = :id";
        queryRequest.SetKeyConditionExpression(conditionExpression); //https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html
        Aws::DynamoDB::Model::AttributeValue avID;
        avID.SetS(ID);
        map<string, Aws::DynamoDB::Model::AttributeValue> attributeValues;
        attributeValues[":id"] = avID;
        queryRequest.SetExpressionAttributeValues(attributeValues);
        auto outcome{ s_DynamoDBClient->Query(queryRequest) };
        if (outcome.IsSuccess())
        {
            auto result{ outcome.GetResult() };
            ... snip ...
            auto item = result.GetItems()[0];
            playerDesc.id = item[DATA_KEY_ID].GetS();   // we already know this, just showing how to read it
            playerDesc.strength = stoi(item[DATA_KEY_STRENGTH].GetN());
            playerDesc.intellect = stoi(item[DATA_KEY_INTELLECT].GetN());
        }
        ... snip ...
    }

This is an example of doing a query to fetch data from the database. Notice the similarity to the update request. Here a condition is created which DynamoDB attempts to fulfill. In this case, the condition is pretty straightforward, which grabs the item with the specified ID. At least in the context of using DynamoDB to store player attributes, this will be the most common request. These queries can be used with any of the primary or secondary keys and could also specify any sorting that’s needed. For non-key queries, you’d perform a scan request, as noted in the database theory section.

And finally

In the sample code found here you’ll find a buildable demo that has a server and a very minimal client which accesses the server over a TCP connection. The client instructs the server to retrieve and write data. I’d like to note that though it’s possible to make calls to DynamoDB directly from the client, this is a poor practice from a security standpoint, hence why my demo does all the access from the server.

I hope you have fun with the sample, and I’d love to see what you come up with using DynamoDB in your game. Please check out the AWS subreddit and let us know what you’ve been working on or ask any questions you may have.

More reading and next steps

Amazon DynamoDB start page

Blog post – Amazon DynamoDB: Gaming use cases and design patterns.

Walk through tutorial: Data Modeling a Gaming Application with Amazon DynamoDB

Not ready to go on line? Read this article on running DynamoDB locally.