Building a multiple player game.
System Architecture
There are 2 parts to the system.
-
Flow of user onboarding.
-
Flow of User activity within game.
Gameplay
There were 3 things to figure out.
- Package the existing game to be re-used for multiplayer game server.
- Figuring out a communication channel.
- Creating a client which would accept user inputs and render the gameplay.
Going foreward
Solving #1 problem
I extracted the core game runtime logic into a sub module in game server. (submodule)
SimpleShootingShipEnvironmentFactory
factory was responsible for creating a game instance.
Solving #2 problem
This was a bit tricky. First i used apache kafka as a communication mechanism for streaming the gameplay to client, but i hit a road block with a configuration.
Next I tried using Rabbit MQ
as the message channel for communication. Even with rabbit mq there was a bit of boilerplate code required for establishing the communications.
Upon further research, I landed on an awesome spring library spring-cloud-stream
. It really abstracted almost all the boiler plate code. I just needed to add server location properties.
The annotations for listening on queues were really simple to implement.
Solving #3 problem
Creating the client was the most easiest task, which i would not go deep into.
How it Works, the most exciting part.
3 important components play the vital part.
-
How do we know when new user joins ?
The game client would push a message to user-registration queue, which is picked up by this listener.
The User is then added to lobby.
public class MessageListener { ... ... ... @StreamListener(target = Sink.INPUT) public void createUser(String encodedUser) throws JsonProcessingException { final var user = this.typeConverter.getUser(encodedUser); this.gameLobbyService.addUserToLobby(user); this.matchUsers(); } }
-
When would the users play ?
When the user is added to the lobby, we would check if a user pair is found.
If a user pair is found, it creates a new game instance and assigns both users to the game instance.
public class MessageListener { ... ... private void matchUsers() { final var matchedPlayers = this.gameLobbyService.matchPlayers(); for (MatchedPlayerGroup matchedPlayer : matchedPlayers) { this.gameInstanceManager.createGameInstance(matchedPlayer); } } }
-
How the game instances would run for multiple people ?
Each game instance is a thread which would run the game and send the gameplay frames back to the user.
The
GameInstanceManager
is responsible for handling the game threads.The
GameInstance
is derived class ofThread
and has arun()
method which will make the game roll.public class GameInstanceManager { ... ... public void createGameInstance(MatchedPlayerGroup matchedPlayerGroup) { final var gameInstance = new GameInstance(matchedPlayerGroup, this.messageDispatcher); this.gameInstanceThreads.put(matchedPlayerGroup.id, gameInstance); this.mapUserToInstance(matchedPlayerGroup); gameInstance.start(); } }
-
How would the actual game run inside the thread ?
Some important things happen in the
GameInstance
constructorwe create a new
GameEnvironment
instance of the shooting ship game.The
refresh()
method is responsible for taking the input from user and updating the game environment based on the user input.final var updatedFrame = this.gameEnvironment.updateEnvironment(nextInput);
Once we get the updated frame, we dispatch the game to each of the user with a message dispatcher.
public class GameInstance extends Thread { public GameInstance(MatchedPlayerGroup matchedPlayerGroup, MessageDispatcher messageDispatcher) { ... this.gameEnvironment = GameInstanceFactory.defaultInstance(); ... } @Override public void run() { while (isActive) { this.refresh(); this.sleep(); } } public void refresh() { final var nextInput = getNextInput(); if (GameInput.QUIT.equals(nextInput.getGameInput())) { isActive = false; } final var updatedFrame = this.gameEnvironment.updateEnvironment(nextInput); this.matchedPlayerGroup.users.forEach(user -> this.messageDispatcher.dispatchMessage(user.getId(), updatedFrame)); } }
© 2023 bsybin