In the previous part of the series, we’ve implemented an offline game and got ourselves ready to tackle the multiplayer. This time we’ll get our hands dirty fleshing out the server part.

This is the second part of the series. In case you missed the first one or want to go further, check them out below:

The server

Out of client-server architecture we’re going to develop the server first, since the client has to connect to something. In a nutshell, the server will run in cycles: gather Controls input from Players, run through game logic computations and finally send back updated game state to the clients.

Several key points can be drawn from this overview. Clients will only send Controls input, not their game state. This eliminates the need for client state validation on the server side (client can only say “my forwards key is pressed” which is always a valid possibility, rather than “I’m totally at [20x,30y]” even though it was [20x,5y] in the last frame). This in turn naturally implies that the server has to run game logic to become a single source of truth.

In order to make it all possible clients and a server have to communicate using a common format and maintain a fast two-way channel of exchanging messages. They also need to have some way of uniquely identifying game objects across the network barrier, so when the server says “this Bullet collided with this Player” the client knows which this Bullet and this Player it is. Knowing this, we’re ready to implement our server-related logic.

Infrastructural Chores

Remember that the infrastructure is already taken care of in the supplementing repository for this article series at part 2: the server, so if you’re coding along you can just delete everything in module source code and have an infrastructure up and ready.

Still here? A stubborn little fella, aren’t ya? Ok, first we will create a brand new server module, alongside of other top-level module (core, desktop etc.). It will be somewhat similar to a desktop module in a sense that it’s runnable, so we can just take build.gradle from desktop and adjust names accordingly for server. Then we’ll create necessary src/com/asteroids/game/server/ namespace inside. Then we’ll need to go back to the Asteroids project root where we’ll include server module in settings.gradle, add proper directories in .gitignore, and for a final mundane step, add tasks for new project declaration:

project(":server") { apply plugin: "java" dependencies { compile project(":core") compile "com.badlogicgames.gdx:gdx-backend-headless:$gdxVersion" compile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" compile 'com.corundumstudio.socketio:netty-socketio:1.7.12' } } 1 2 3 4 5 6 7 8 9 10 11 project ( ":server" ) { apply plugin : "java" dependencies { compile project ( ":core" ) compile "com.badlogicgames.gdx:gdx-backend-headless:$gdxVersion" compile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" compile 'com.corundumstudio.socketio:netty-socketio:1.7.12' } }

There are two interesting dependencies here.

We will include a headless LibGDX backend instead of a normal desktop one. Why? We won’t really need a GPU in order to run a server code, so that shaves off a bit of overhead and you’ll save yourself some hassle if your hosting service doesn’t provide boxes equipped with GPU at all. natives-desktop is just there to make headless backend work.

Another thing is netty-socketio. This will be our underlying workhorse socket server. It’s an implementation of Socket.IO that suits our needs for fast bidirectional message exchange.

There’s one more place in which we’ll need to add dependencies, and that’s the core project:

project(":core") { ... dependencies { ... compile "com.fasterxml.jackson.core:jackson-databind:2.8.4" compile "org.slf4j:slf4j-simple:1.7.25" } } 1 2 3 4 5 6 7 8 project ( ":core" ) { . . . dependencies { . . . compile "com.fasterxml.jackson.core:jackson-databind:2.8.4" compile "org.slf4j:slf4j-simple:1.7.25" } }

As you may have guessed, we’re going to use plain old JSON as message format and Jackson is a fine tool to convert objects back and forth. “Back and forth” part also explains why this is a core dependency rather than server one. Side note: Keep in mind that there will be a lot of messages flying between the machines and, if your future game sends large state messages, you may want to get out of debug mode. For our little Asteroids JSON will suffice.

netty-socketio uses SLF4J API for logging and will complain if no implementation is found on the classpath, so we’ll provide the simple one.

Whew. That’s it, onto the proper development now.

Identifying things

First of all, we need some way to uniquely identify those models that will need to have their state updated – those will be a Player (with its Ship), and a Bullet. An ability to identify a thing by id seems like a trait, so we’ll implement it in Identifiable interface that will be placed in model package:

public interface Identifiable { UUID getId(); default boolean isIdEqual(UUID otherId) { return getId().equals(otherId); } } 1 2 3 4 5 6 public interface Identifiable { UUID getId ( ) ; default boolean isIdEqual ( UUID otherId ) { return getId ( ) . equals ( otherId ) ; } }

Next we’ll make a Player and a Bullet implement this interface:

public class Player implements Identifiable { private final UUID id; ... public Player(UUID id, Controls controls, Color color) { this.id = id; ... } ... @Override public UUID getId() { return id; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Player implements Identifiable { private final UUID id ; . . . public Player ( UUID id , Controls controls , Color color ) { this . id = id ; . . . } . . . @Override public UUID getId ( ) { return id ; } }

public class Bullet implements Visible, Identifiable { ... private final UUID id; ... public Bullet(UUID id, Player shooter, Vector2 startPosition, float rotation) { ... this.id = id; ... } @Override public UUID getId() { return id; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Bullet implements Visible , Identifiable { . . . private final UUID id ; . . . public Bullet ( UUID id , Player shooter , Vector2 startPosition , float rotation ) { . . . this . id = id ; . . . } @Override public UUID getId ( ) { return id ; } }

Ids are now mandatory for Player and Bullet, so make sure you’ll add them where those objects are created.

First place is inside of Ship:

... public Optional<Bullet> obtainBullet() { if(canShoot && wantsToShoot) { lastShot = Instant.now(); return Optional.of(new Bullet( UUID.randomUUID(), owner, bulletStartingPosition(), shape.getRotation()) ); } return Optional.empty(); } ... 1 2 3 4 5 6 7 8 9 10 11 12 13 14 . . . public Optional <Bullet> obtainBullet ( ) { if ( canShoot && wantsToShoot) { lastShot = Instant.now(); return Optional . of ( new Bullet ( UUID . randomUUID ( ) , owner , bulletStartingPosition ( ) , shape . getRotation ( ) ) ) ; } return Optional . empty ( ) ; } . . .

And another in AsteroidsGame:

... Player player1 = new Player(UUID.randomUUID(), new KeyboardControls(), Color.WHITE); Player player2 = new Player(UUID.randomUUID(), new NoopControls(), Color.LIGHT_GRAY); ... 1 2 3 4 . . . Player player1 = new Player ( UUID . randomUUID ( ) , new KeyboardControls ( ) , Color . WHITE ) ; Player player2 = new Player ( UUID . randomUUID ( ) , new NoopControls ( ) , Color . LIGHT_GRAY ) ; . . .

As we’ll be able to identify models, we’ll also want to pick and remove them out of Containers by id.

Let’s declare it in the Container interface. Note that we’ll also need to modify our Containers to only accept Things that are Identifiable.

public interface Container<Thing extends Identifiable> { ... default Optional<Thing> getById(UUID id) { return stream() .filter(thing -> thing.isIdEqual(id)) .findAny(); } default Optional<Thing> getById(String id) { return getById(UUID.fromString(id)); } default void removeById(UUID id) { getAll().removeIf(thing -> thing.isIdEqual(id)); } default void removeById(String id) { removeById(UUID.fromString(id)); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface Container < Thing extends Identifiable > { . . . default Optional <Thing> getById ( UUID id ) { return stream ( ) . filter ( thing - > thing . isIdEqual ( id ) ) . findAny ( ) ; } default Optional <Thing> getById ( String id ) { return getById ( UUID . fromString ( id ) ) ; } default void removeById ( UUID id ) { getAll ( ) . removeIf ( thing - > thing . isIdEqual ( id ) ) ; } default void removeById ( String id ) { removeById ( UUID . fromString ( id ) ) ; } }

We’ll also have to update ContainerRenderer and its generic bounds accordingly:

public class ContainerRenderer<Thing extends Identifiable> implements Renderer 1 public class ContainerRenderer < Thing extends Identifiable > implements Renderer

Additionally we’ll need the ability to delete Bullets from BulletsContainer by shooter id.

This will come in handy when a Player will disconnect and we’ll want to remove all of his Bullets.

public class BulletsContainer implements Container<Bullet> { ... public void removeByPlayerId(UUID id) { bullets.removeIf(bullet -> bullet.getShooterId().equals(id)); } ... } 1 2 3 4 5 6 7 public class BulletsContainer implements Container <Bullet> { . . . public void removeByPlayerId ( UUID id ) { bullets . removeIf ( bullet - > bullet . getShooterId ( ) . equals ( id ) ) ; } . . . }

Opening up models to obtain and set their data

We’ll need to pass a bunch of data to and from the server about the current state of our models. It means that they can no longer be those closed boxes of state anymore. We need to open them up. We have to figure out where these things are to send their position. We’ve already abstracted behavior of Visible things into an interface, and having a position (and rotation) is clearly a trait of a Visiblething, so we’ll add methods to set and retrieve these properties on said interface:

public interface Visible { ... default Vector2 getPosition() { return new Vector2(getShape().getX(), getShape().getY()); } default void setPosition(Vector2 position) { getShape().setPosition(position.x, position.y); } default float getRotation() { return getShape().getRotation(); } default void setRotation(float degrees) { getShape().setRotation(degrees); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public interface Visible { . . . default Vector2 getPosition ( ) { return new Vector2 ( getShape ( ) . getX ( ) , getShape ( ) . getY ( ) ) ; } default void setPosition ( Vector2 position ) { getShape ( ) . setPosition ( position . x , position . y ) ; } default float getRotation ( ) { return getShape ( ) . getRotation ( ) ; } default void setRotation ( float degrees ) { getShape ( ) . setRotation ( degrees ) ; } }

In order to connect Bullets with Players we’ll also need a shooter id, so let’s expose that from Bullet:

public class Bullet implements Visible, Identifiable { ... public UUID getShooterId() { return shooter.getId(); } ... } 1 2 3 4 5 6 7 public class Bullet implements Visible , Identifiable { . . . public UUID getShooterId ( ) { return shooter . getId ( ) ; } . . . }

Finding a spot for new Player

Whenever a Player connects, we need to place his Ship somewhere. This will be a job for the new method on previously introduced Respawner called respawnFor. Note the minor refactor in respawn that will now use this newly introduced method too:

... public void respawn() { playersContainer.stream() .filter(player -> !player.getShip().isPresent()) .forEach(this::respawnFor); } public void respawnFor(Player player) { player.setShip(new Ship(player, randomRespawnPoint(), 0)); } ... 1 2 3 4 5 6 7 8 9 10 11 . . . public void respawn ( ) { playersContainer . stream ( ) . filter ( player - > ! player . getShip ( ) . isPresent ( ) ) . forEach ( this : : respawnFor ) ; } public void respawnFor ( Player player ) { player . setShip ( new Ship ( player , randomRespawnPoint ( ) , 0 ) ) ; } . . .

Remotely controlled Players

Players that will be kept on the server won’t be controlled by the server – instead, they’ll be controlled by the clients and those controls will be sent to the server to pass on. So, they’ll be remotely controlled. Let’s implement RemoteControls class then, it will create a mutable value objects that can be updated with data coming from the client.

public class RemoteControls implements Controls { private boolean forward; private boolean left; private boolean right; private boolean shoot; @Override public boolean forward() { return forward; } @Override public boolean left() { return left; } @Override public boolean right() { return right; } @Override public boolean shoot() { return shoot; } public void setForward(boolean state) { forward = state; } public void setLeft(boolean state) { left = state; } public void setRight(boolean state) { right = state; } public void setShoot(boolean state) { shoot = state; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class RemoteControls implements Controls { private boolean forward ; private boolean left ; private boolean right ; private boolean shoot ; @Override public boolean forward ( ) { return forward ; } @Override public boolean left ( ) { return left ; } @Override public boolean right ( ) { return right ; } @Override public boolean shoot ( ) { return shoot ; } public void setForward ( boolean state ) { forward = state ; } public void setLeft ( boolean state ) { left = state ; } public void setRight ( boolean state ) { right = state ; } public void setShoot ( boolean state ) { shoot = state ; } }

When we’ll have RemoteControls we’ll want to get it out of Player on the server to set its control state. This could be as simple as passing RemoteControls as controls to Player and adding a getter:

public Controls getControls() { return controls; } 1 2 3 public Controls getControls ( ) { return controls ; }

…yeah, ok. But we’re referring to Controls interface inside of Player and we specifically want to get RemoteControls back.

Maybe we could get away with a little casting:

public RemoteControls getRemoteControls() { return (RemoteControls)controls; } 1 2 3 public RemoteControls getRemoteControls ( ) { return ( RemoteControls ) controls ; }

Yuck. Nope, this is just wrong. Remote controls make no sense in context of plain Player and force-casting RemoteControls onto whatever Controls there are would obfuscate the code intent. We’re missing a type here, and that would be a new model called RemotePlayer

public class RemotePlayer extends Player { private final RemoteControls remoteControls; public RemotePlayer(UUID id, RemoteControls remoteControls, Color color) { super(id, remoteControls, color); this.remoteControls = remoteControls; } public RemoteControls getRemoteControls() { return remoteControls; } } 1 2 3 4 5 6 7 8 9 10 11 12 public class RemotePlayer extends Player { private final RemoteControls remoteControls ; public RemotePlayer ( UUID id , RemoteControls remoteControls , Color color ) { super ( id , remoteControls , color ) ; this . remoteControls = remoteControls ; } public RemoteControls getRemoteControls ( ) { return remoteControls ; } }

Inheriting from one model into another may initially raise your eyebrows, but the most important thing here is what meaning this code will convey. RemotePlayer is a Player. It’s a specialized type of Player, one that will always be remotely controlled and we’ll have just one additional method to retrieve those RemoteControls in order to update their state. Other than that, it’s perfectly fine to use RemotePlayer in whatever way you’d use a plain Player.

Sadly, due to how generics are implemented in Java, Container<RemotePlayer> is not a subtype of Container<Player>, so we’ll have to refactor the code that operates on it. Let’s start with PlayersContainer:

public class PlayersContainer<PlayerType extends Player> implements Container<PlayerType> 1 public class PlayersContainer < PlayerType extends Player > implements Container <PlayerType>

Same thing with Collider:

public class Collider<PlayerType extends Player> 1 public class Collider < PlayerType extends Player >

And also Respawner:

public class Respawner<PlayerType extends Player> 1 public class Respawner < PlayerType extends Player >

Of course, you’ll also have to change all occurrences of Player to PlayerType inside of these classes.

Data Transfer Objects and their mappers

AKA boring stuff again. Sorry, but it has to be done. We could get away with mapping JSON data directly into models, but I find that ugly and confusing, so I’ll verbosely express data exchange formats instead. Some of the source code in this section will be omitted because otherwise, it would go on and on, so in order to get the full source please refer to the corresponding repo.

First, let’s create a new package in core called dto. All our DTOs will be immutable value objects with very similar structure and we can encapsulate all the JSON conversion details within an interface, so let’s do that:

public interface Dto { ObjectMapper objectMapper = new ObjectMapper(); default String toJsonString() { try { return objectMapper.writeValueAsString(this); } catch (JsonProcessingException e) { throw new RuntimeException("Error while converting Dto to JSON", e); } } static <DtoType extends Dto> DtoType fromJsonString(String json, Class<DtoType> dtoTypeClass) { try { return objectMapper.readValue(json, dtoTypeClass); } catch (IOException e) { throw new RuntimeException("Error while creating Dto from JSON", e); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface Dto { ObjectMapper objectMapper = new ObjectMapper ( ) ; default String toJsonString ( ) { try { return objectMapper . writeValueAsString ( this ) ; } catch ( JsonProcessingException e ) { throw new RuntimeException ( "Error while converting Dto to JSON" , e ) ; } } static < DtoType extends Dto > DtoType fromJsonString ( String json , Class <DtoType> dtoTypeClass ) { try { return objectMapper . readValue ( json , dtoTypeClass ) ; } catch ( IOException e ) { throw new RuntimeException ( "Error while creating Dto from JSON" , e ) ; } } }

We’ll also need to annotate constructor fields so that Jackson knows what’s what. Take an exemplary ShipDto:

public class ShipDto implements Dto { private final float x; private final float y; private final float rotation; @JsonCreator public ShipDto( @JsonProperty("x") float x, @JsonProperty("y") float y, @JsonProperty("rotation") float rotation) { this.x = x; this.y = y; this.rotation = rotation; } public float getX() { return x; } public float getY() { return y; } public float getRotation() { return rotation; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class ShipDto implements Dto { private final float x ; private final float y ; private final float rotation ; @JsonCreator public ShipDto ( @JsonProperty ( "x" ) float x , @JsonProperty ( "y" ) float y , @JsonProperty ( "rotation" ) float rotation ) { this . x = x ; this . y = y ; this . rotation = rotation ; } public float getX ( ) { return x ; } public float getY ( ) { return y ; } public float getRotation ( ) { return rotation ; } }

There’s really not much to it. Just an immutable value object consisting of primitive types that will get encoded as JSON out of the box. The point here is that we’ll be keeping all this JSON business out of our models, which is beneficial for code readability and cleanness. Code for other DTOs will be omitted because it follows the same pattern, but I’ll briefly describe them all and their properties here:

ControlsDto – will represent Controls state as boolean values.

private final boolean forward; private final boolean left; private final boolean right; private final boolean shoot; 1 2 3 4 private final boolean forward ; private final boolean left ; private final boolean right ; private final boolean shoot ;

BulletDto – will contain all the data necessary to create/update a Bullet, including shooting Player id.

private final String id; private final float x; private final float y; private final float rotation; private final String shooterId; 1 2 3 4 5 private final String id ; private final float x ; private final float y ; private final float rotation ; private final String shooterId ;

ShipDto – will be similar to BulletDto, but won’t include Player id because ShipDto will always be nested inside of PlayerDto.

private final float x; private final float y; private final float rotation; 1 2 3 private final float x ; private final float y ; private final float rotation ;

PlayerDto – aside from Player properties, it will also contain ShipDto for a given Player.

private final String id; private final String color; private final ShipDto shipDto; 1 2 3 private final String id ; private final String color ; private final ShipDto shipDto ;

UuidDto will be a little wrapper around UUID when we’ll need to send just that.

private final String uuid; 1 private final String uuid ;

GameStateDto – will contain complete game state necessary to render a screen. Specifically, it will be contents of PlayersContainer and BulletsContainer encoded as Lists of PlayerDto and BulletDto.

private final List<PlayerDto> players; private final List<BulletDto> bullets; 1 2 private final List <PlayerDto> players ; private final List <BulletDto> bullets ;

IntroductoryStateDto – this one will be a bit special. It will be sent once to a newly connected Player with PlayerDto containing its introductory data (id assigned by the server, ship position) as well as GameStateDto of the game right until this new Player joined.

private final PlayerDto connected; private final GameStateDto gameState; 1 2 private final PlayerDto connected ; private final GameStateDto gameState ;

Onto the mappers now, which we’ll place in a mapper package inside of previously introduced dto.

A mapper will be a bridge between Dtos and models. Every model that needs to be synced over the network (Player, Ship etc.) will have its own mapper that will be responsible for things like creating Dtos from models, setting models state according to Dto and so on. They will be entirely stateless, and therefore consist of static methods only. Again, as in case of Dtos, mappers will be somewhat similar to each other and it would be redundant to show the code of them all here, so let’s take a look at example ShipMapper:

public class ShipMapper { public static ShipDto fromShip(Ship ship) { Vector2 shipPosition = ship.getPosition(); return new ShipDto(shipPosition.x, shipPosition.y, ship.getRotation()); } } 1 2 3 4 5 6 public class ShipMapper { public static ShipDto fromShip ( Ship ship ) { Vector2 shipPosition = ship . getPosition ( ) ; return new ShipDto ( shipPosition . x , shipPosition . y , ship . getRotation ( ) ) ; } }

There are more of them here, and that will conclude this section.

After all this is done, we’ll be able to finally start working on the actual server implementation.

Connection

…is the name of the package that will contain our, well, connection-related code. We’ll make such package inside core first, and it will contain declaration of high-level Events that can happen in the client-server communication.

public enum Event { PLAYER_CONNECTING, PLAYER_CONNECTED, OTHER_PLAYER_CONNECTED, OTHER_PLAYER_DISCONNECTED, CONTROLS_SENT, GAME_STATE_SENT } 1 2 3 4 5 6 7 8 public enum Event { PLAYER_CONNECTING , PLAYER_CONNECTED , OTHER_PLAYER_CONNECTED , OTHER_PLAYER_DISCONNECTED , CONTROLS_SENT , GAME_STATE_SENT }

Let’s go briefly through them all:

PLAYER_CONNECTING will be sent by the client to the server when this client wants to join the game.

will be sent by the client to the server when this client wants to join the game. PLAYER_CONNECTED will be sent by the server to the client when connecting process is finished and the client is ready to participate in the game.

will be sent by the server to the client when connecting process is finished and the client is ready to participate in the game. OTHER_PLAYER_CONNECTED will be sent by the server to the remaining clients other than the one that just joined to let them know that it happened.

will be sent by the server to the remaining clients other than the one that just joined to let them know that it happened. OTHER_PLAYER_DISCONNECTED will be the same as previous, but it will let remaining clients know that some other client has just left.

will be the same as previous, but it will let remaining clients know that some other client has just left. CONTROLS_SENT will be sent by the client to the server and will contain current state of client’s controls.

will be sent by the client to the server and will contain current state of client’s controls. GAME_STATE_SENT will be sent by the server to all the clients to let them know about the current state of the game.

Based on those events we’re going to build a Server that will react to them and send them to the clients. Even though we’ll use SocketIO for that, there’s no reason to not have some other Server implementation in the future, so we’ll start with a common interface. It’s going to be placed in another package named connection just like the recent one, but this time we’ll create it in the server module.

public interface Server { void start(); void onPlayerConnected(Consumer<PlayerDto> handler); void onPlayerDisconnected(Consumer<UUID> handler); void onPlayerSentControls(BiConsumer<UUID, ControlsDto> handler); void broadcast(GameStateDto gameState); void sendIntroductoryDataToConnected(PlayerDto connected, GameStateDto gameState); void notifyOtherPlayersAboutConnected(PlayerDto connected); } 1 2 3 4 5 6 7 8 9 public interface Server { void start ( ) ; void onPlayerConnected ( Consumer <PlayerDto> handler ) ; void onPlayerDisconnected ( Consumer <UUID> handler ) ; void onPlayerSentControls ( BiConsumer < UUID , ControlsDto > handler ) ; void broadcast ( GameStateDto gameState ) ; void sendIntroductoryDataToConnected ( PlayerDto connected , GameStateDto gameState ) ; void notifyOtherPlayersAboutConnected ( PlayerDto connected ) ; }

This will give us an overview of Server responsibilities. It’ll provide ability to react to 3 types of events: any Player connecting, disconnecting and sending controls, and also be able to send 3 types of data: game state broadcast for all, introductory data for newly connected Player and notification about that Player to the remaining Players.

As you may expect, there’ll have to be something above the Server, that will supply it with data to act upon. This is true, in that sense the Server will be a tool that’s used by game logic (pretty similar to AsteroidsScreen you’ve seen earlier), but for now let’s focus on implementing SocketIO variant of the Server.

In the constructor, we’ll initialize SocketIOServer with desired host and port. It’ll be ready to connect whenever we’ll call a start method. We’ll also set up exceptions behavior and set up events, more on that later.

public class SocketIoServer implements Server { private static final Logger logger = LoggerFactory.getLogger(SocketIoServer.class); private final SocketIOServer socketio; private Consumer<PlayerDto> playerJoinedHandler; private BiConsumer<UUID, ControlsDto> playerSentControlsHandler; private Consumer<UUID> playerLeftHandler; public SocketIoServer(String host, int port) { Configuration config = new Configuration(); config.setHostname(host); config.setPort(port); config = setupExceptionListener(config); socketio = new SocketIOServer(config); } @Override public void start() { Configuration config = socketio.getConfiguration(); socketio.start(); logger.info("Game server started at " + config.getHostname() + ":" + config.getPort()); setupEvents(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class SocketIoServer implements Server { private static final Logger logger = LoggerFactory . getLogger ( SocketIoServer . class ) ; private final SocketIOServer socketio ; private Consumer <PlayerDto> playerJoinedHandler ; private BiConsumer < UUID , ControlsDto > playerSentControlsHandler ; private Consumer <UUID> playerLeftHandler ; public SocketIoServer ( String host , int port ) { Configuration config = new Configuration ( ) ; config . setHostname ( host ) ; config . setPort ( port ) ; config = setupExceptionListener ( config ) ; socketio = new SocketIOServer ( config ) ; } @Override public void start ( ) { Configuration config = socketio . getConfiguration ( ) ; socketio . start ( ) ; logger . info ( "Game server started at " + config . getHostname ( ) + ":" + config . getPort ( ) ) ; setupEvents ( ) ; }

Event handler methods will enable us to expose Server events to higher level layer:

@Override public void onPlayerConnected(Consumer<PlayerDto> handler) { playerJoinedHandler = handler; } @Override public void onPlayerDisconnected(Consumer<UUID> handler) { playerLeftHandler = handler; } @Override public void onPlayerSentControls(BiConsumer<UUID, ControlsDto> handler) { playerSentControlsHandler = handler; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public void onPlayerConnected ( Consumer <PlayerDto> handler ) { playerJoinedHandler = handler ; } @Override public void onPlayerDisconnected ( Consumer <UUID> handler ) { playerLeftHandler = handler ; } @Override public void onPlayerSentControls ( BiConsumer < UUID , ControlsDto > handler ) { playerSentControlsHandler = handler ; }

These methods will enable higher level layer to communicate events to clients:

@Override public void broadcast(GameStateDto gameState) { sendEvent(socketio.getBroadcastOperations(), Event.GAME_STATE_SENT, gameState); } @Override public void sendIntroductoryDataToConnected(PlayerDto connected, GameStateDto gameState) { socketio.getAllClients().stream() .filter(client -> client.getSessionId().equals(UUID.fromString(connected.getId()))) .findAny() .ifPresent(client -> sendEvent(client, Event.PLAYER_CONNECTED, new IntroductoryStateDto(connected, gameState))); } @Override public void notifyOtherPlayersAboutConnected(PlayerDto connected) { socketio.getAllClients().stream() .filter(client -> !client.getSessionId().equals(UUID.fromString(connected.getId()))) .forEach(client -> sendEvent(client, Event.OTHER_PLAYER_CONNECTED, connected)); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public void broadcast ( GameStateDto gameState ) { sendEvent ( socketio . getBroadcastOperations ( ) , Event . GAME_STATE_SENT , gameState ) ; } @Override public void sendIntroductoryDataToConnected ( PlayerDto connected , GameStateDto gameState ) { socketio . getAllClients ( ) . stream ( ) . filter ( client - > client . getSessionId ( ) . equals ( UUID . fromString ( connected . getId ( ) ) ) ) . findAny ( ) . ifPresent ( client - > sendEvent ( client , Event . PLAYER_CONNECTED , new IntroductoryStateDto ( connected , gameState ) ) ) ; } @Override public void notifyOtherPlayersAboutConnected ( PlayerDto connected ) { socketio . getAllClients ( ) . stream ( ) . filter ( client - > ! client . getSessionId ( ) . equals ( UUID . fromString ( connected . getId ( ) ) ) ) . forEach ( client - > sendEvent ( client , Event . OTHER_PLAYER_CONNECTED , connected ) ) ; }

Next, we’ll need to handle low level connection logic before exposing it to event handlers. PLAYER_CONNECTING is most interesting here, so we’ll take a closer look at it. Whenever Player is connecting, it won’t yet have an id – we need to assign it. In this case, it will make the most sense to assign Player with the same id as its assigned socket client, because that will make it easier to perform some operations later (ie. handle disconnections). Then we’ll pass this new Player‘s Dto to higher layer.

private void setupEvents() { addEventListener(Event.PLAYER_CONNECTING, (client, json, ackSender) -> { PlayerDto connecting = Dto.fromJsonString(json, PlayerDto.class); PlayerDto withAssignedId = new PlayerDto(client.getSessionId().toString(), connecting.getColor(), connecting.getShipDto()); playerJoinedHandler.accept(withAssignedId); }); addEventListener(Event.CONTROLS_SENT, (client, json, ackSender) -> { ControlsDto dto = Dto.fromJsonString(json, ControlsDto.class); playerSentControlsHandler.accept(client.getSessionId(), dto); }); socketio.addDisconnectListener(client -> { UUID id = client.getSessionId(); playerLeftHandler.accept(id); sendEvent(socketio.getBroadcastOperations(), Event.OTHER_PLAYER_DISCONNECTED, new UuidDto(id.toString())); }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void setupEvents ( ) { addEventListener ( Event . PLAYER_CONNECTING , ( client , json , ackSender ) - > { PlayerDto connecting = Dto . fromJsonString ( json , PlayerDto . class ) ; PlayerDto withAssignedId = new PlayerDto ( client . getSessionId ( ) . toString ( ) , connecting . getColor ( ) , connecting . getShipDto ( ) ) ; playerJoinedHandler . accept ( withAssignedId ) ; } ) ; addEventListener ( Event . CONTROLS_SENT , ( client , json , ackSender ) - > { ControlsDto dto = Dto . fromJsonString ( json , ControlsDto . class ) ; playerSentControlsHandler . accept ( client . getSessionId ( ) , dto ) ; } ) ; socketio . addDisconnectListener ( client - > { UUID id = client . getSessionId ( ) ; playerLeftHandler . accept ( id ) ; sendEvent ( socketio . getBroadcastOperations ( ) , Event . OTHER_PLAYER_DISCONNECTED , new UuidDto ( id . toString ( ) ) ) ; } ) ; }

Lastly, we’ll have the server exceptions logging and some small helper methods. This will be just a bit of boilerplate we need to ease our work with the server.

private Configuration setupExceptionListener(Configuration config) { // event exception handling - to keep it simple just throw them as runtime exceptions config.setExceptionListener(new ExceptionListenerAdapter() { @Override public void onEventException(Exception e, List<Object> data, SocketIOClient client) { throw new RuntimeException(e); } @Override public void onDisconnectException(Exception e, SocketIOClient client) { throw new RuntimeException(e); } @Override public void onConnectException(Exception e, SocketIOClient client) { throw new RuntimeException(e); } @Override public boolean exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // connection error, log and move along if(cause instanceof IOException) { logger.warn(cause.getMessage()); return true; } return false; } }); return config; } private void addEventListener(Event eventName, DataListener<String> listener) { socketio.addEventListener(eventName.toString(), String.class, listener); } private void sendEvent(ClientOperations client, Event eventName, Dto data) { client.sendEvent(eventName.toString(), data.toJsonString()); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private Configuration setupExceptionListener ( Configuration config ) { // event exception handling - to keep it simple just throw them as runtime exceptions config . setExceptionListener ( new ExceptionListenerAdapter ( ) { @Override public void onEventException ( Exception e , List <Object> data , SocketIOClient client ) { throw new RuntimeException ( e ) ; } @Override public void onDisconnectException ( Exception e , SocketIOClient client ) { throw new RuntimeException ( e ) ; } @Override public void onConnectException ( Exception e , SocketIOClient client ) { throw new RuntimeException ( e ) ; } @Override public boolean exceptionCaught ( ChannelHandlerContext ctx , Throwable cause ) { // connection error, log and move along if ( cause instanceof IOException ) { logger . warn ( cause . getMessage ( ) ) ; return true ; } return false ; } } ) ; return config ; } private void addEventListener ( Event eventName , DataListener <String> listener ) { socketio . addEventListener ( eventName . toString ( ) , String . class , listener ) ; } private void sendEvent ( ClientOperations client , Event eventName , Dto data ) { client . sendEvent ( eventName . toString ( ) , data . toJsonString ( ) ) ; } }

Using the connection

In previous section, the notion of higher level layer using Server was brought up a few times. Now we’ll implement it. Basically it’s going to be a variation on the AsteroidsScreen, but it will register events and gather input from them. It’ll also conduct sending events to Players.

Let’s create AsteroidsServerScreen inside server module:

public class AsteroidsServerScreen extends ScreenAdapter { private final Server server; private final PlayersContainer<RemotePlayer> playersContainer; private final BulletsContainer bulletsContainer; private final Arena arena; private final Respawner respawner; private final Collider collider; public AsteroidsServerScreen(Server server, Container<RemotePlayer> playersContainer, BulletsContainer bulletsContainer, Arena arena, Respawner respawner, Collider collider) { this.server = server; this.playersContainer = playersContainer; this.bulletsContainer = bulletsContainer; this.arena = arena; this.respawner = respawner; this.collider = collider; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class AsteroidsServerScreen extends ScreenAdapter { private final Server server ; private final PlayersContainer <RemotePlayer> playersContainer ; private final BulletsContainer bulletsContainer ; private final Arena arena ; private final Respawner respawner ; private final Collider collider ; public AsteroidsServerScreen ( Server server , Container <RemotePlayer> playersContainer , BulletsContainer bulletsContainer , Arena arena , Respawner respawner , Collider collider ) { this . server = server ; this . playersContainer = playersContainer ; this . bulletsContainer = bulletsContainer ; this . arena = arena ; this . respawner = respawner ; this . collider = collider ; }

After server will be injected we can start listening to events. onPlayerConnected is particularly interesting, so let’s examine it in detail. When Player will send its representation as PlayerDto we will create a RemotePlayer to keep on the server, spawn a place for its Ship using Respawner and when that’s done send IntroductoryStateDto to that Player with information about its newly spawned Ship and general game state. We’ll also notifyOtherPlayersAboutConnected.

@Override public void show() { server.onPlayerConnected(playerDto -> { RemotePlayer connected = PlayerMapper.remotePlayerFromDto(playerDto); respawner.respawnFor(connected); PlayerDto connectedDto = PlayerMapper.fromPlayer(connected); GameStateDto gameStateDto = GameStateMapper.fromState(playersContainer, bulletsContainer); server.sendIntroductoryDataToConnected(connectedDto, gameStateDto); server.notifyOtherPlayersAboutConnected(connectedDto); playersContainer.add(connected); }); server.onPlayerDisconnected(id -> { playersContainer.removeById(id); bulletsContainer.removeByPlayerId(id); }); server.onPlayerSentControls((id, controlsDto) -> { playersContainer .getById(id) .ifPresent(sender -> ControlsMapper .setRemoteControlsByDto(controlsDto, sender.getRemoteControls())); }); server.start(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public void show ( ) { server . onPlayerConnected ( playerDto - > { RemotePlayer connected = PlayerMapper . remotePlayerFromDto ( playerDto ) ; respawner . respawnFor ( connected ) ; PlayerDto connectedDto = PlayerMapper . fromPlayer ( connected ) ; GameStateDto gameStateDto = GameStateMapper . fromState ( playersContainer , bulletsContainer ) ; server . sendIntroductoryDataToConnected ( connectedDto , gameStateDto ) ; server . notifyOtherPlayersAboutConnected ( connectedDto ) ; playersContainer . add ( connected ) ; } ) ; server . onPlayerDisconnected ( id - > { playersContainer . removeById ( id ) ; bulletsContainer . removeByPlayerId ( id ) ; } ) ; server . onPlayerSentControls ( ( id , controlsDto ) - > { playersContainer . getById ( id ) . ifPresent ( sender - > ControlsMapper . setRemoteControlsByDto ( controlsDto , sender . getRemoteControls ( ) ) ) ; } ) ; server . start ( ) ; }

render method will be almost the same as in AsteroidsScreen, except that it will lack rendering (since we won’t need any), but it will instead broadcast game state to all the connected Players at the end of each frame.

@Override public void render(float delta) { respawner.respawn(); collider.checkCollisions(); playersContainer.update(delta); playersContainer.streamShips() .forEach(arena::ensurePlacementWithinBounds); playersContainer.obtainAndStreamBullets() .forEach(bulletsContainer::add); server.broadcast(GameStateMapper.fromState(playersContainer, bulletsContainer)); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public void render ( float delta ) { respawner . respawn ( ) ; collider . checkCollisions ( ) ; playersContainer . update ( delta ) ; playersContainer . streamShips ( ) . forEach ( arena : : ensurePlacementWithinBounds ) ; playersContainer . obtainAndStreamBullets ( ) . forEach ( bulletsContainer : : add ) ; server . broadcast ( GameStateMapper . fromState ( playersContainer , bulletsContainer ) ) ; } }

Almost there

At this point we will have two things remaining to conclude the server part (for now).

First one will be an equivalent of AsteroidsGame that will configure and inject dependencies into AsteroidsServerScreen. Only new stuff here is reading server’s host and port from environment variables:

public class AsteroidsServerGame extends Game { private Screen asteroids; @Override public void create() { Arena arena = new Arena(AsteroidsGame.WORLD_WIDTH, AsteroidsGame.WORLD_HEIGHT); BulletsContainer bulletsContainer = new BulletsContainer(); PlayersContainer<RemotePlayer> playersContainer = new PlayersContainer(); Respawner respawner = new Respawner(playersContainer, WORLD_WIDTH, WORLD_HEIGHT); Collider collider = new Collider(playersContainer, bulletsContainer); Map<String, String> env = System.getenv(); String host = env.getOrDefault("HOST", "localhost"); int port = Integer.parseInt(env.getOrDefault("PORT", "8080")); Server server = new SocketIoServer(host, port); asteroids = new AsteroidsServerScreen( server, playersContainer, bulletsContainer, arena, respawner, collider); setScreen(asteroids); } @Override public void dispose() { asteroids.dispose(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class AsteroidsServerGame extends Game { private Screen asteroids ; @Override public void create ( ) { Arena arena = new Arena ( AsteroidsGame . WORLD_WIDTH , AsteroidsGame . WORLD_HEIGHT ) ; BulletsContainer bulletsContainer = new BulletsContainer ( ) ; PlayersContainer <RemotePlayer> playersContainer = new PlayersContainer ( ) ; Respawner respawner = new Respawner ( playersContainer , WORLD_WIDTH , WORLD_HEIGHT ) ; Collider collider = new Collider ( playersContainer , bulletsContainer ) ; Map < String , String > env = System . getenv ( ) ; String host = env . getOrDefault ( "HOST" , "localhost" ) ; int port = Integer . parseInt ( env . getOrDefault ( "PORT" , "8080" ) ) ; Server server = new SocketIoServer ( host , port ) ; asteroids = new AsteroidsServerScreen ( server , playersContainer , bulletsContainer , arena , respawner , collider ) ; setScreen ( asteroids ) ; } @Override public void dispose ( ) { asteroids . dispose ( ) ; } }

Lastly, in order for the server module to be runnable in a similar manner as desktop, we’re going to need a launcher:

public class AsteroidsServerLauncher { public static void main(String[] args) { new HeadlessApplication(new AsteroidsServerGame()); } } 1 2 3 4 5 public class AsteroidsServerLauncher { public static void main ( String [ ] args ) { new HeadlessApplication ( new AsteroidsServerGame ( ) ) ; } }

This is it

Let’s give our server a spin with ./gradlew server:run (on Linux/macOS) or gradlew server:run (on Windows) and see it in action!

We got it! This is what we’ve been working for this entire part.

Yeah.

Your face right now:

I know, it seems underwhelming. But it’ll make sense when we’ll implement the client side, and that’s what the next part will be all about. Stay tuned 🙂