📜 ⬆️ ⬇️

Game server on Scala + Akka: Case study



Last time, I outlined the use of Akka for the game server.
Now we will sort simple, but nevertheless working example of the server.

Diskleimer


Those who fumble in the subject, may find inaccuracies and simplifications in the description. So it was intended. I wanted to show general points for those who do not know what it is and how it can be used. The given example should be considered not as ready for production code. But rather as a working pattern for experiments.

In the last article, it was already described in general terms why akka is good.
Therefore, immediately proceed to create a server.
')

Architecture


Our task is to make a multiplayer game, well, let it be tanchiki (yeah, fresh idea).
In source codes, on githabe, there will be both a server and a client. But here we consider only the server.

The server itself will consist of several services.
Each service is an actor that receives messages. In a real system, this actor is likely to be the supervisor for the actors directly processing the message. Those. the service actor itself will not do any work.

It will only launch the working actors and monitor their work, for example, restart if necessary. We have a simplified situation. Therefore, the actors will do all the work themselves.

So let's first draw what we generally do.

This is our megaproved client. Full 3D, by the way:



And this is the server:



Arrows indicate the message flows between actors.
1. TCP service - service responsible for connecting clients. We have a TCP option.
2. Session - actor of the game session. Responsible for messaging with the client.
3. Task service - a service for performing common tasks.
4. Auth service - a service that authenticates the player.
5. GM service - game mechanics service. Responsible for managing rooms and general game activities ...
6. Room - these are the actors who play the role of the rooms in which the game takes place.
7. Storage - a service for working with data storage. DB SQL or something else.

Let us write in more detail what we got.
I will give here only pieces of code. All code is laid out on githab.

TCP service

Akka, at the moment, in the standard package supports TCP and UDP connections. In the experimental branch there is also a WebSocket. We will use TCP. The network stack in Akka takes its roots from Spray and works more efficiently than, for example, Netty. Although Netty, at the same time, more functionality.

So, the client connects to the TCP service . It creates the connection actor responsible for the connection. After the connection is established, we create the Session actor, which is responsible for the game session and through the connection exchanges messages with the client.

When working with TCP there are some nuances. TCP is a persistent connection. And not always the system can say for sure whether the client is still connected or not.

Therefore, to verify the client using the so-called Heartbeat. The server periodically pings the client, with an empty packet, to see if there is still a connection.
To do this, a scheduller is started in the Session , which in our case pings the client every 10 seconds.

scheduler = context.system.scheduler.schedule(10.seconds, 10.seconds, self, Heartbeat) 

Further, as soon as the connection with the client is established, it sends a command for authentication to the server. It takes the actor Session .
Session forwards the message to the Task service . What would he figure out what kind of message it is and what to do with it. It looks like this:

 case class CommandTask(session: ActorRef, comm: PacketMSG) 

Yes, I forgot to explain. As a transport, use Protobuf . So here PacketMSG is a protoobafovsky object, our message from the client. Session is a link to a player's session actor.

Task service

This service is responsible for common tasks performed on the server. In our case, it is the main command router from the client. But this, of course, is not a silver bullet. The message system in Akka is very flexible. And you can quite cleverly configure message routing with built-in tools. But you need to move from simple to complex. Immediately not cover all the possibilities.

In general, the Task service in the real server itself will not do anything. It only launches child actors, which will already do all the work. Or themselves to generate more of its subsidiary actors to perform specific actions. In general, there are already many options. In our case, the Task service will determine that it is an authentication request and simply send a message to the Auth service , with the task to check if there is a player with the specified authentication parameters.

  def handlePacket(task: CommandTask) = { task.comm.getCmd match { case Cmd.Auth.code => authService ! Authenticate(task.session, task.comm) case Cmd.Join.code => gameService ! JoinGame(task.session) case Cmd.Move.code => gameService ! PlayerMove(task.session, task.comm) case _ => log.info("Crazy message") } } 

Auth service

Service responsible for authentication. We have it very primitive. Able only to go to the database for the user:

  override def receive = { case task: Authenticate => handleAuth(task) case task: SomePlayer => handleAuthenticated(task) case task: AuthenticatedFailed => handleFailed(task) case _ => log.info("unknown message") } 

If it gets Authenticate, then you should go to the database to check:

 case class GetPlayerByName(session: ActorRef, comm: PacketMSG) 

If it receives SomePlayer, it means that the authentication is successful and you can communicate this good news to all those interested. Player and GM.

  task.session ! Send(Cmd.AuthResp, login.build().toByteArray) gameService ! task 

And if AuthenticatedFailed, it means that they did not find a player, and this sad news must also be reported to all those interested. In this case, only the player. By the way, in a real server such attempts can be considered and punished as stubborn:

 task.session ! Send(Cmd.AuthErr, Array[Byte]()) 

Actually, no one bothers to screw it in full, providing a variety of authentication options.

Storage

Working with the database in Akka is a separate topic. Since inside work happens on ordinary flows. By creating many “long-playing” tasks for actors, you can hang the whole system. Actors should be lightweight. We use the usual list as “DB”. Therefore, nothing will slow down. But the real database will block the stream for a long time. Therefore, in a real project, an actor working with a database is allocated a separate stream, or a pool of threads, so that it does not slow down the entire system.

If a player is found, a message with successful authentication is sent to the Session actor, which will already pack it and send it to the client.

Well, we went into the game.
Next, the client sends the message "Start the game." Task service will redirect the GM service message, it will create a room and place the player in it.

Then he informs the client about the start of the game.
But our implementation is simple, so immediately after connecting the first player, the server automatically creates a room and places the player in it. Thus, all connected, will be in the same room.

GM service

This is the main service of the game. He knows about all the connected players. He knows how many rooms he has created and can act as part of a load balancing system. We have a session game, so the whole game mechanics is calculated in the rooms. The actor Room is created for them.

And there are nuances. If the game is turn-based. Well, sort of checkers or cards. So to distribute the load on the rooms, in general, do not need anything. All available iron resources will be utilized by Akka evenly.
If the game is realtime like ours, then you can do some optimizations.

The fact is that while we have a small load. Few players or game mechanics are easy, then the calculation of game mechanics in the general pool of threads may not slow down. But as soon as the load increases, the lags will be noticeable.

Next, I will describe what can be done with this.

In the meantime, we started the room and added a player to it. The game is real-time, so we need to notify players about changes in the state of the game regularly. Well, let's say, every 100ms. Although, of course, this time is individual for games.

The world in a real-time game, especially shooters with physics, is calculated deterministically, i.e. in steps. During the step, we take the game world, apply player commands to it obtained by this moment, calculate physics, collisions, hits, some in-game events, NPCs, etc. Accordingly, the faster the game situation is calculated, the more frames give the server. And the more smoothly the game will go.

To do this, we start a sheduller, which every 100ms will be sent to us by “Tick”.
An event that means it's time to recalculate the game situation.

 scheduler = context.system.scheduler.schedule(100.millisecond, 100.millisecond, self, Tick) 

It turns out that each room itself will periodically say “it's time to recalculate the game.”
The recalculation results are sent to all connected players in the room.
In our case, only movements of players are taken into account.

  players.keySet.map(p => getPoint(move, p)) players.values.map(s => s ! Send(Cmd.Move, move.build().toByteArray)) 

As a matter of fact, we created a simple game server, covering basic things. Connection, authentication, work with the database room.

An experienced reader, looking at the code, will say:
- Semyon Semyonitch, yes, I, too, on the simple threads I forget about without problems.

Well, in general, it is. The message system is written on the knee for a couple of hours. We connect Netty and forward, dawn towards. We select each service in a flow, and we exchange messages through collections.
Why use some complicated Akka?

And what can Akka offer us in this case? ..
Well, about the fact that we have all the code is very simple, single-threaded. And about the other amenities Akka, I already wrote. I will not repeat.

In general, simple implementations are bad because they do not reflect the many problems of real applications. For example, you can write to this server in general in one stream. And for the time being, it will work.

Well, for example, rooms that can (and will in most cases) load the CPU with work. In the usual version, if we need to divide the rooms into streams, we will have to think about it. Think and write code for this separation.

What do we have now?

A room is a simple class actor. It is very simple. All code is executed in the common thread pool. On the test with a couple of players, at my place, it will not be enough to feel.

But now we need to test the server already for example with 50 players. We decide to allocate each room to our stream. To do this, simply indicate that this actor uses a separate thread, and not a shared pool. That's all, we did not think about synchronization, did not think about shared data. Moreover, in Akka there is a cluster out of the box. This means that transferring rooms to individual machines on the network will not be a big problem. The code of the room itself will not change at all. It will be the same actor, only it will work on a separate machine.

Each actor has an address. And all the work with him goes through it. The whole system doesn't care where this actor works. In the general pool, in a separate thread, on a separate machine or even on separate machines. There is an address to which we send the message, and what is located behind it, the flow, the machine or the cluster, we already define the config. This gives us on the one hand a convenient and fast scalability, well, if suddenly the game tramples and it will be necessary to quickly raise the server with game mechanics. On the other hand, the convenience of development. The entire cluster can be raised both on the same developer machine and on the cluster. This will be determined only by the config.

Those. any actor or a group of actors in the system can be run either in the common thread pool, or in a separate thread, or in separate threads, or on a separate machine simply by changing the config. While maintaining its simple single-threaded code.

For a game server, this is a very big plus. Akka takes on a very large amount of work. And at the same time practically does not limit the developer. After all, if there is no suitable dispatcher, router or mailbox, you can always write your own implementation. Perfect for the occasion.

If the topic is interesting, it will be possible to continue with refactoring, bringing the code closer to a more realistic production.

All the code, including the server and client, is laid out on GitHub .

Source: https://habr.com/ru/post/259185/


All Articles