Choices and Motivation
There were two goals of rewriting the game to Akka.Net. First of all, I wanted to try out a few design patterns. Secondly, if you run a multi-player game and your server needs to bring different players into the same context, you will run into big technical challenges if you store state in a file or database.
The previous version of the game stores state in a file, and uses file-locking to synchronize/sequence the threads for multi-player access. This choice was made due to past bad experiences with a cheap hosting-party in India, whose load-balancers took away any form of pan-threaded resource sharing, except the filesystem. Even if I chose a DB to store state, then the technical solution would still be quite hard, because then you end up migrainial row-locking feasts, with the risk of time-wasting deadlocks.
Akka.Net seemed to be the ultimate solution to the problem of different players working together in the same state-full context. That could of course not run on the cheap Indian hosting provider with their faulty Plesk interface. But it could certainly run as a service under different operating systems.
Because from work-side, we were investigating Akka.Net as a solution to some project, I was extra interested in Akka.Remote. So this project was separated in a Game Engine part and a Presentation part, instead of having the GameEngine embedded in the Web-Server.
The Presentation part was written with Websharper, because I wanted to (easily) use the HTML5 Websocket as a bidirectional communication channel between client and Game Engine. The previous version of the game had a polling mechanism to determine for each client whether it was his turn, or whether one of the other players had made a move and changed the board.
The Presentation part communicates with the Game Engine via Akka.Remote. One issue popped up quite quickly, Akka.Remote did not like my Discriminated Unions. Well, the issue was not really in Akka.Remote, but in the underlying serialization library from Newtonsoft. There have been numerous fixes over the years but none satisfactory, so I was adviced to use “Wire” for serialization. Instead of configuring this myself, I chose to use an experimental side-project, Akkling, which uses “Wire” by default and provides extra F# features such as static typing. With Akkling, you need to know what you’re doing. But when you get the hang of it, it works nicely. Wire processes Discriminated Unions flawlessly, so it certainly solves the issue.
If you compare working in C# or F# in respect to creating actors with Akka.Net, then the difference is that F# code is about 70% smaller than the C# version. In C#-land people are already making ML’s and/or DSL’s to speed up the work. Akkling reduced the work even more than Akka.Net’s standard F# support, with more features and extra static typing, which greatly reduced the error-risk. I really hope that this library will see a serious future.
Application Architecture
The basic application architecture is as follows:
Design Patterns
At the Game Engine side, I wanted to create a system with a waiting room. The first thing a new Player-Server actor does, is connect with a Waiting-Room actor. The Waiting-Room waits until enough players have entered, 3 players in this game. When enough players have entered, the Waiting-Room actor starts a Game-Room actor, and tells the players in the waiting room that they can connect to the Game-Room. The Game-Room determines who starts the game, who has which color, what the game-board looks like, and after this the game begins. During the game, the Game-Room informs all players about whether it is their turn, what choice other players made, what the game-board looks like after a player made a move. To support NPC AI’s, whenever a human-player enters the Waiting room, two AI’s are started, who wait 20 seconds before entering the Waiting-Room themselves. If their seat is taken (by another human player), then their attendance is ignored. Otherwise the AI is treated as a regular player.
The specific design pattern I wanted to try out in this game, was to find a way to move a player from the Waiting-Room to the Game-Room. The problem is though that there are limitations in how far an actor may be a container of other actors.
I thought of the “supervision” capability, where both Waiting-Room and Game-Room have Supervision on player-actors hang under them. The player actors could then communicate with their context (one of the rooms) via the “Parent” property. But I found two problems in this approach, -1- I did not find clear documentation how to create hierarchy in actors with F#, and I did not have much confidence in my own experiment-results. -2- I did not find any documentation on how to rehang an actor from one supervisor, to the other supervisor. I also did not want to re-create the player-actors under their new supervisor (the Game-Room), because this would at some point interrupt the communication, somewhere in the pipe between Browser and GameEngine.
One could also think of a construction in which both Waiting-Room and Game-Room have an array/list with player-actors. But then you still need to tell the player-actors about their context, ie. tell them in which room they are in. This implies that you manually program something into the player actor, that looks like the “Parent” property I mentioned above. Telling the player-actors about their context would make the whole array-structure obsolete anyway.
Instead of one actor being container of other actors, I decided for a different pattern. The Player Actor would have a flexible communication-line to his current Room. So a Player-Server actor should first talk to the Waiting-Room actor, and then change its connection to the Game-Room actor.
The player actor has max only one connection to a room, and this is the room he is in. This finally became the pattern. Thanks to Akkling I was able to use static typing and many Discriminated Unions to enable this quite complex mechanism.
Also the AI’s got a slightly different design pattern. In the previous TIC version, the AI’s had access to the centralized game-board state. That was possible because everything was happening in the same thread. In this version, everything is split it into different actors, and sharing game-board became less obvious. Now the AI-player actor keeps track of the game-board state locally. This also caused many more message to be send through the system, to keep the data consistent.
How to Run
The complete code can be downloaded from Github. The two most important projects are “GameEngine.Service” and “GameUI.Owin”, which should both be started when running this code in Visual Studio 2015.
The “GameEngine.Service” project can also be run stand-alone, and with this Fsharp script you can run it step-by-step.