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.
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.
Simulator
I developed a simulator to balance cars configurations. It helps to categorize the cars in classes easier.
Game Loop
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)
Player Inputs
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.
Player Actions
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.
Game Events
There are two kinds of events to publish:
Public events
All clients should receive this type of event, like GameStarted
, StartCutscene
, CountDown
. The game server uses the WebSocket channel to broadcast public events.
Internal Events
The game server uses the broker to publish internal events to inform the game manager of its state, like GameServerIsReady
or GameFinished
. The game manager receives events and logs them into the database.
Pipeline
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.
In the next part, we’ll review the game client logic.
All posts: