RPG Game State Management with Dapr

Last month, within one week after .NET Conf Singapore 2019 took place, Microsoft announced their Dapr (Distributed Application Runtime) project. Few days after that, Scott Hanselman invited Aman Bhardwaj and Yaron Schneider to talk about Dapr on Azure Friday.

🎨 Introducing Dapr. (Image Source: Azure Friday) 🎨

Dapr is an open-source, portable, and event-driven runtime which makes the development of resilient micro-service applications easier.

In addition, Dapr is light-weight and it can run alongside our application either as a sidecar process or container. It offers us some capabilities such as state management, which will be demonstrated in this article today, pub-sub, and service discovery which are useful in building our distributed applications.

🎨 Dapr building blocks which can be called over standard HTTP or gRPC APIs. (Image Credit: Dapr GitHub Project) 🎨

Dapr makes developer’s life better when building micro-service application by providing best-practice building blocks. In addition, since building blocks communicate over HTTP or gRPC, another advantage of Dapr is that we can use it with our favourite languages and frameworks. In this article, we will be using NodeJS.

🎨 Yaron explains how developers can choose which building blocks in Dapr to use. (Image Source: Azure Friday) 🎨

In this article, we will be using only the state management feature in Dapr and using one of them doesn’t mean we have to use them all.

Getting Started

We will first run Dapr locally. Dapr can be run in either Standalone or Kubernetes modes. For our local development, we will run it in Standalone mode first. In the future then we will deploy our Dapr applications to Kubernetes cluster.

In order to setup Dapr on our machine locally and manage the Dapr instances, we need to have Dapr CLI installed too.

Before we begin, we need to make sure we have Docker installed on our machine and since the application we are going to build is a NodeJS RPG game, we will need NodeJS (version 8 or greater).

After having Docker, we can then proceed to install Dapr CLI. The machine that I am using is Macbook. On MacOS, the installation is quite straightforward with the following command.

curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash

After the installation is done, we then can use the Dapr CLI to install the Dapr runtime with the following command.

dapr init

That’s all for setting up the Dapr locally.

Project Structure

The NodeJS game that we have here is actually copied from the html-rpg project done by Koichiro Mori on GitHub. The following architecture diagram illustrates the components that make up our application.

🎨 Architecture diagram, inspired by the hello-world sample of Dapr project. 🎨

For the project, we have two folders in the project root, which is backend and game.

🎨 Project structure. 🎨

The game project is just a normal NodeJS project where all the relevant codes of the html-rpg is located in the public folder. Then in the app.js, we have the following line.

app.use(express.static('public))
🎨 Four character types (from top to bottom): King, player, soldier, and minister. 🎨

We also update the code of html-rpg so that whenever the player encounters the soldier or the minister face-to-face, the player HP will drop 10 points. To do so, we simply send HTTP POST request to the Dapr instance which is listening on port 4001 (will explain where this port number comes from later).

...
var data = {};
data["data"] = {};
data["data"]["playerHp"] = map.playerHp;

// construct an HTTP request
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:4001/v1.0/invoke/backend/method/updatePlayerHp", true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');

// send the collected data as JSON
xhr.send(JSON.stringify(data));
...

In the backend project, we will have the code to handle the /updatePlayerHp request, as shown in the code below.

app.post('/updatePlayerHp', (req, res) => {
    const data = req.body.data;
    const playerHp = data.playerHp;

    const state = [{
        key: "playerHp",
        value: data
    }];

    fetch(stateUrl, {
        method: "POST",
        body: JSON.stringify(state),
        headers: {
            "Content-Type": "application/json"
        }
    }).then((response) => {
        console.log((response.ok) ? "Successfully persisted state" : "Failed to persist state: " + response.statusText);
    });

    res.status(200).send();
});

The code above will get the incoming request and then persist the payer HP to the state store.

CosmosDB as State Store

By default, when we run Dapr locally, Redis state store will be used. The two files in the components directory in the backend folder, i.e. redis_messagebus.yaml and redis.yaml are automatically created when we run Dapr with the Dapr CLI. If we delete the two files and run Dapr again, it the two files will still be re-generated. However, that does not mean we cannot choose another storage as state store.

Besides Redis, Dapr also supports several other types of state stores, for example CosmosDB.

🎨 Supported state stores in Dapr as of 9th November 2019. I am one of the contributors to the documentation! =) 🎨

To use CosmosDB as state store, we simply need to replace the content of the redis.yaml with the following.

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
   name: statestore
spec:
   type: state.azure.cosmosdb
   metadata:
   - name: url
     value: <CosmosDB URI> 
   - name: masterKey
     value: <CosmosDB Primary Key>
   - name: database
     value: <CosmosDB Database Name>
   - name: collection
     value: <CosmosDB Collection Name> 

The four required values above can be retrieved from the CosmosDB page on the Azure Portal. There is, however, one thing that we need to be careful, i.e. the Partition Key of the container in CosmosDB.

🎨 Partition Key is a mandatory field during the container creation step. 🎨

When I was working on this project, I always received the following error log from Dapr.

== APP == Failed to persist state: Internal Server Error

Since Dapr project is quite new and it is still in experimental stage, none of my friends seem to know what’s happening. Fortunately, Yaron is quite responsive on GitHub. Within two weeks, my question about this error is well answered by him.

I had a great discussion with Yaron on GitHub and he agreed to update the documentation to highlight the fact that we must use “/id” as the partition key.

So, after correcting the partition key, I finally can see the state stored on CosmosDB.

🎨 CosmosDB reflects the current HP of the player which has dropped from 100 to 60. 🎨

In the screenshot above, we can also clearly see that “backend-playerHP” is automatically chosen as id, which is what being explained in the Partition Keys section of the documentation.

References