System Architecture

There are 2 parts to the system.

  1. Flow of user onboarding. UserOnboarding

  2. Flow of User activity within game. UserInteractionUpdates

Gameplay

Gameplay

There were 3 things to figure out.

  1. Package the existing game to be re-used for multiplayer game server.
  2. Figuring out a communication channel.
  3. 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.

  1. 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();
       }
    }
    
  2. 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);
             }
          }
    
       }
    
    
  3. 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 of Thread and has a run() 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();
          }
    
       }
    
  4. How would the actual game run inside the thread ?

    Some important things happen in the GameInstance constructor

    we 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));
          }
       }
    

Standalone game github