An Akka multiplayer game backend from scratch - Part 1January 06, 2016

Recently I am very interested in Akka for the usage as backbone of a multiplayer game backend. At my work @DsFishlabs we are using Spring Boot with Spring Cloud for our internal microservices.

In this series I want to try to build a similar system based on Akka with Scala, Akka Cluster Sharding, Akka Persistence aka. Event Sourcing and DDD, so the functionality will be similiar to my set up at work but here I want to try a different direction.

All sources for the parts can be found on github, for part1 you should look extractShardId[here]

So lets get started!

Ubiquitous language & our first model

In this series I will act as Developer as well as Domain Expert to make things less complicated. I will orientate myself roughly on the feature set we have in our company and the games. Each project should start with an ubiquitous language and the requirements to design a solution, so lets define some terms.

The Domain Expert says:

Each Player has some amount of XP as well as Credits

This introduces three terms:

Player

The person that is using our game backend is identified as a Player with a unique Identifier.

XP

Each Player will progress through the game which is indicated by the amount of XP.

Credits

This is the in-game currency that is used to buy all kind of stuff.


The `Player` with a typed Id
Figure 1. The Player with a typed Id

This really simple requirement will lead us to the model represented on the left. Here we modeled the Player as an Aggregate Root and added a typed Id (PlayerId).

This should fullfill all requirements from above (ok, it`s just one…​) and its a great class. We also included all terms in the ubiquitous language into our model.

The corresponding scala class should look like this:

Player.scala
class Player {
  var id: PlayerId = PlayerId(UUID.randomUUID.toString)
  var xp: Int = 0
  var credits: Int = 0
}

case class PlayerId(value: String)

Perfect, all requirements are met…​
but we only have a single class that is not really useful, there are also no messages that can be send to the player and it will be initialized with a random UUID that we cannot control, so let us fix that.


The Domain Expert says:

A new Player will be created when a Person registers on the Backend.
It will the be initialized with a given amount of XP and Credits (in this case 0)
When a Player requests his information from the backend then it should return the Id and the amount of XP as well as the amount of Credits

Alright, so we need to be able to create new Players (lets take a REST approach here) and it should be possible to retrieve information about the Player.

For the sake of simplicity we will take care of authentication later on.
We will also incorporate HATEOAS to make the api discoverable at a later point.

Ok, lets implement the new requirements:

Our first Actor

At first let us convert the Player class into an event sourced actor, I will describe the interessting parts:

PlayerActor.scala
class PlayerActor extends PersistentActor {

  var id: PlayerId = PlayerId(self.path.name) (1)
  var xp: Int = _
  var credits: Int = _

  // self.path.name is the entity identifier (utf-8 URL-encoded)
  override def persistenceId: String = "Player-" + self.path.name

  val log = Logging(context.system, this)

  override def receiveCommand = {
    case init: InitializePlayer => (2)
      persist(PlayerInitialized(init.playerId, init.xp, init.credits)) { ev => (3)
        initialize(ev)
        sender() ! ev (4)
      }
    case GetPlayerInformation => sender() ! PlayerInformation(id, xp, credits) (5)
    case _ => log.info("received unknown message")
  }

  override def receiveRecover = { (6)
    case init: PlayerInitialized => initialize(init)
    case _ => log.info("received unknown message")
  }

  def initialize(init: PlayerInitialized) = { (7)
    this.xp = init.xp
    this.credits = init.credits
  }
}
1 As you will see later we can derive the Id of the Player from the actor path, so a Player always has an Id.
2 To implement the requirement that the Player should be initialized with some values we defined the corresponding event.
3 First we will persist the resulting event, and after it was persisted we call the initialized method to initalize the Player
4 After initialization we will return the event to the sender.
5 Here we implemented the requirement that a Player should be able to return its state. There is no command that will result in an event, so we just return the data.
6 This method will be called when events on this actor will be replayed (e.g. it has been relocated to another node). Here we call the initialize method directly, as we dont need to persist the event (it is already persisted).
7 The initialize method will alter the state of the actor and sets the XP and Credits to the event values.

As you can see I extended the PersistentActor Trait which makes sense because our actor will use Event Sourcing as persistence mechanism.

The companion object looks like this, here we define the events:

Companion object of the PlayerActor
object PlayerActor {
  // define compatible commands
  case class InitializePlayer(playerId: PlayerId, xp: Int, credits: Int)
  case class GetPlayerInformation(playerId: PlayerId)

  // define compatible events
  case class PlayerInitialized(playerId: PlayerId, xp: Int, credits: Int)

  // custom responses
  case class PlayerInformation(playerId: PlayerId, xp: Int, credits: Int)

  def extractEntityId(): ShardRegion.ExtractEntityId = { (1)
    case msg@InitializePlayer(id, _, _) => (id.value.toString, msg)
    case msg@GetPlayerInformation(id) => (id.value.toString, msg)
  }

  def extractShardId(numberOfShards: Int): ShardRegion.ExtractShardId = { (2)
    case InitializePlayer(id, _, _) => Math.abs(id.hashCode() % numberOfShards).toString
    case GetPlayerInformation(id) =>  Math.abs(id.hashCode() % numberOfShards).toString
  }
}
1 To make use of Akka Cluster Sharding we need to define an extractEntityId method so akka knows which actor should receive the current message. Here we return the Id of the Player which will also be the path name of the actor (so we can extract the Id in the actor).
2 We also need to define a extractShardId mehtod so akka knows which shard should responsible for the actor (in this sample we define 100 shards) so it can distribute the actors on each node.

The main application

We also add a new register endpoint to our service and connect it together:

Application.scala
object Application extends App with AkkaInjectable {

  implicit val system = ActorSystem()
  implicit val executor = system.dispatcher
  implicit val materializer = ActorMaterializer()
  implicit val timeout = Timeout(5 seconds)

  val config = ConfigFactory.load()

  val logger: LoggingAdapter = Logging(system, getClass)

  implicit val appModule = new PlayerModule (1)
  val player = inject[ActorRef]('player) (2)

  val routes = {
    logRequestResult("server") {
      (post & path("register")) {
        complete {
          // create a new user and send it a message
          val playerId = PlayerId(UUID.randomUUID().toString) (3)
          (player ? InitializePlayer(playerId, 0, 0)).mapTo[PlayerInitialized].map { ev: PlayerInitialized => ev.playerId.value } (4)
        }
      }
    }
  }

  Http().bindAndHandle(routes, config.getString("http.interface"), config.getInt("http.port")) (5)
}
1 Load the PlayerModule where the cluster declarations for our player actors are defined.
2 Inject the reference of the sharding actor so we can send messages to it.
3 Create a new Id
4 Send the command to the actor (akka will take care of the routing and instantiation) and wait for the actor to respond (here we wait up to 5 seconds)
5 Start the Http server and bind the given routes on the given port.

In this series I will use Scaldi for the dependency injection, therefore we need to declare the cluster sharding:

class PlayerModule(implicit system: ActorSystem) extends Module {

  val numberOfShards = 100

  val playerRegion: ActorRef = ClusterSharding(system).start(
    typeName = "Player",
    entityProps = Props[PlayerActor],
    settings = ClusterShardingSettings(system),
    extractEntityId = PlayerActor.extractEntityId(), (1)
    extractShardId = PlayerActor.extractShardId(numberOfShards) (2)
  )

  bind[ActorRef] as 'player to playerRegion (3)

}
1 Reference the extractEntityId method given above.
2 Reference the extractShardId method given above.
3 Bind the shard actor reference to player so we can inject it later

You can find the complete source code here

Great, that looks like it for now

Test it

Let us go ahead and make a quick benchmark for the current part:

Start the server in your IDE or do

sbt run

I am using Httpie which makes the life so much easier :)
You can then POST to the endpoint /register like this:

> http POST :9000/register

HTTP/1.1 200 OK
Content-Length: 36
Content-Type: text/plain; charset=UTF-8
Date: Sun, 10 Jan 2016 18:44:59 GMT
Server: akka-http/2.4.1

62b058cf-6d73-4d5c-9855-2aed6e36ad3d

Let us also run a short benchmark on it (10 seconds, 10 concurrent requests):

> ab -t10 -c10 -mPOST http://localhost:9000/register

This is ApacheBench, Version 2.3 <$Revision: 1663405 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 5000 requests
Completed 10000 requests
Finished 10377 requests


Server Software:        akka-http/2.4.1
Server Hostname:        localhost
Server Port:            9000

Document Path:          /register
Document Length:        36 bytes

Concurrency Level:      10
Time taken for tests:   10.000 seconds
Complete requests:      10377
Failed requests:        0
Total transferred:      2044269 bytes
HTML transferred:       373572 bytes
Requests per second:    1037.70 [#/sec] (mean)
Time per request:       9.637 [ms] (mean)
Time per request:       0.964 [ms] (mean, across all concurrent requests)
Transfer rate:          199.64 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       3
Processing:     2    9  15.0      8     474
Waiting:        2    9  15.0      8     474
Total:          3   10  15.0      9     474

Percentage of the requests served within a certain time (ms)
  50%      9
  66%      9
  75%     10
  80%     10
  90%     11
  95%     12
  98%     14
  99%     17
 100%    474 (longest request)

Nice, we got 10.000 new users in 10 seconds, that is impressive (our spring server does not even get close…​), so I am really looking forward to part2.

You can find the whole code for part 1 here

Let me know what you think.

Happy hakking