Designing a Distributed System for an Online Multiplayer Game — Game Server (Part 5–2)
This is part number five of the Garage series, you can access all parts at the end of this post.
This part is more related to game development, we are going to implement the game mechanics and logic.
The game server simulates a drag car race. The gameplay is simple, players can control the number of gears and they should change it at right time.
Each car has a different config and was categorized in a class. The car config includes engine power, gears ratio, differentials ratio, mass …
The car engine produces power (torque) and the gearbox multiplies it and the resultant force is conveyed to the wheels, some opposite forces like aerodynamic force reduce it and the result makes the car move forward.
I developed a simulator to balance cars configurations. It helps to categorize the cars in classes easier.
The game logic is run in an interval of time, it is called the game loop and the interval depends on the gameplay, for example for a fast-paced game, we need to run the game in 50 frames per second which means a 20ms interval.
The loop iterates a code to load the client input and simulate the car acceleration to calculate the velocity and the position for each frame, sequentially, the game server broadcasts the world state (snapshot) to the game clients.
On another hand, the game clients run the same loop (shared code with exact implementations in different languages) in the same intervals to sync with the server calculations. (there are a lot of challenges here, we’ll talk about this in the game client logic post)
The game client, streams player inputs with the frame number to the game server at a high rate. the
udpsocket package receives the inputs, decrypts them, authorizes the user session id, and then proxies the inputs to the game inputs golang channel. The game server caches the inputs to use in the game loop for that frame number.
There is some other type of player inputs that we call player action. These kinds of messages must be delivered correctly and in order, so we use the WebSocket channel to transport them. When users connect to the game server, they must wait until all other players connect too. After that they must change their status to Ready by pushing a button. The player ready message is an example of player action.
There are two kinds of events to publish:
All clients should receive this type of event, like
CountDown. The game server uses the WebSocket channel to broadcast public events.
The game server uses the broker to publish internal events to inform the game manager of its state, like
GameFinished. The game manager receives events and logs them into the database.
The game server waits for the players to connect after startup. When all players send the
Ready action, the game changes the state to
Running and publishes the
Cutscene event, then it sleeps for the cutscene duration, afterward it publishes the
Countdown events, and in the following, it starts the game loop and listens for the inputs in another goroutine.
The game clients start to stream the inputs with the frame number to the server and the game input listener caches them.
The game loop loads the player inputs from the cache and calculates the cars’ positions for each frame, then it broadcasts the snapshots to the clients.
Finally, when all cars cross the finish line, the game publishes
GameFinished events with the leaderboard to the clients. Then it publishes the finish event into the broker and calls the finish function to shut down the game server. Here the pod is destroyed… to the darkness.
The game manager receives the
GameFinished event and makes sure that the pod had been deleted. After that, it updates the game state in the database, then deletes all temporary game caches.