Lag Compensated Games part 3

Developing Lag Compensated Multiplayer Game, pt. 3: The Client

Now that we have the server ready, we can start working on the client side. Soon we will finally be able to actually play our game over the network!

This is the third part of the series that will show you how to make a lag compensated multiplayer mode for the arcade classic (number three out of four articles). In case you missed the previous ones, you can check them out below:
Developing lag compensated multiplayer game – pt.1 – available here
Developing lag compensated multiplayer game – pt.2 – available here
Developing lag compensated multiplayer game – Part IV – coming soon.

The Client

Conceptually, the client will be something that sends Player’s Controls to the server, receives a game state and is able to render it. No movement or collisions will be computed on the client side – instead the server will manipulate game objects like puppets. Later this will change a little as we introduce lag compensation techniques for the client but for now, it will be all.

Infrastructural chores, again

Remember when we’ve introduced server module in the previous part? This will be quite similar, except that new module will be called client and it won’t be runnable (as opposed to desktop). Keep in mind that just like previously, all that work is already done for you in the reference repository, so if you want to go straight into implementing stuff, clone it and just delete everything in the src directories.
Although we could just lump all the client side code into desktop, it would go against LibGDX’s multiplatform nature and we wouldn’t be able to use our universal client side logic in future html, android or ios projects, therefore let’s keep it separated. We’ll go ahead and create the client module, copy build.gradle from core, create /src/com/asteroids/game/client namespace and then we’ll go up to the project root, include client in settings.gradle and open up build.gradle.

There will be one addition and one change in the build process. desktop will no longer depend on core, it will instead depend on client which in turn will depend on core. This is because in a sense, desktop will be a subset of client – it’s one possible client, just like html would be another. Here’s how client will look and how desktop will change:

You’ll notice that we’ve included SocketIO client to be able to talk to our SocketIO server.

Extending mappers

Lastly we’ve introduced a couple of mappers to help tie our Dtos with the rest of the game code. It’s time to extend them a little in order to cover client’s needs. Some of these changes will contain a bit of logic, so contrary to how they were introduced we’ll now go through these changes individually. Changes that we’re going to introduce will have to deal with creating and updating game models. Creation will take place when Dtos coming from the server are describing models not yet present on the client, and update will be performed whenever they already exist. We won’t handle deleting models in mappers, because we won’t need a mapper to perform deletion.

Bullet Mapper

We know that a Bullet belongs to a Player, so in order to introduce new Bullet, we’ll need a Container of available Players passed along BulletDto.
Updating will be easier, as we’ll just set the position of the particular Bullet.

Ship Mapper

When mapping from ShipDto, there’ll be a possibility that a Player won’t have any Ship at the moment, therefore we’ll need to handle a null ShipDto case (which will eventually get transformed into empty Optional through Player’s setter). Otherwise we’ll return brand new Ship. Updating will set Ship’s position and rotation according to ShipDto.

Player Mapper

We’ve already seen one interesting method in PlayerMapper, remotePlayerFromDto, that was used on the server to map incoming PlayerDtos to Players. Now we’re going to introduce it’s client side counterpart which will also create a Player but there will be two differences.

Firstly, it will construct these new Players with provided Controls .
Secondly, it’ll pass some of the work to ShipMapper in order to deal with Ship-related mapping. updateByDto won’t actually deal with any of Player’s own properties (because we don’t have any that could change during the course of the game), but rather delegate work to ShipMapper based on whether the Player has a Ship or not.

Controls Mapper

Lastly, we’ll extend ControlsMapper. Not much will happen here as we’ll just add a method to dump Controls state into ControlsDto.

You don’t get to choose what skin color you’re born with

We’ll need some way of distinguishing between all the different Players being visible on the same screen. In more serious game that would be a nickname and chosen color, but ain’t nobody got time for that, so we’ll just declare list of possible Colors that a Player can have and assign those at random.
The list will be declared in the Player model:

We’ll also create a Randomize tool inside core/util package:

Moving things around

Some of the things currently being in core won’t really make sense on server side, so we can move them to the client module.

First obvious candidate will be rendering package, we can just move it as a whole.
Secondly, we’ll create controls package inside client module and move KeyboardControls there, as there’s no point to have them on the server.

Connection, again

Having flashbacks from the previous part yet?
We’ll have to introduce connection package one more time, on the client side. It’ll contain Client interface, analogous to the Server, which will declare events and actions that we’ll be able to handle.

Apart from connect and isConnected, these methods should ring a bell. They’ll be very much related to those already declared on the server side, completing the picture of our overall connection scheme.

We’ll need a concrete implementation of Client interface to be able to connect with our SocketIOServer. connect method will be particularly interesting, as it’ll be responsible for conducting full Player initialization before handing control over to game logic. Right after low-level socket connection will be established, we’ll inform the Server who the Player is and wait for IntroductoryStateDto to be sent back to consider ourselves connected. SocketIoClient won’t do much more beyond establishing connection and transforming JSON strings to Dtos, leaving most of the game work for upper layer.

Using the connection

Let’s use SocketIoClient in the client Screen and utilize the connection logic we’ve been working on. AsteroidsClientScreen will be responsible for sending and handling connection events, and applying their results to the game loop. It won’t compute any collisions or positions (yet), but merely be there to render game state received by the server and send Players Controls.

Similarly to how we did it earlier, we’ll implement event handlers in show method.

When connection is established and Server responded with IntroductoryStateDto, we’ll loop through it’s contents to update local Player‘s Ship state and populate Containers with other Players and Bullets. For now we’ll pass NoopControls because Ships will be updated directly from ShipDtos coming from the server rather than any sort of local Controls.

Whenever other Player connects or disconnects, we need to reconcile that with local playersContainer.

When we’ll receive the game state, there will be a couple of interesting things going on. Let’s walk through them. At first, we’ll update existing Players (and subsequently their Ships too), which will be pretty straightforward:

For Bullets we’ll need a bit more work to do, mostly because contrary to Player we don’t have dedicated events for when Bullet was introduced or removed (there would be a lot of them). If Bullet coming from the Server doesn’t exist yet, we’ll need to add it. If it’s there, we’ll need to update it. Finally, if it’s present in our local game but not on the Server, we’ll need to delete it locally.

Why .filter().collect().forEach()instead of just .filter().forEach()? Because you shouldn’t modify Stream’s underlying List during pipeline execution.

We’ll end our work in show by executing connection request for local Player:

Client game

As we did before, we’ll configure and inject dependencies in the Game class, this time it will be AsteroidsClientGame:

While we’re at it, we can delete most of AsteroidsGame in core since we won’t need it anymore. What will still be shared between the client and the server are world dimensions, so they’ll be all that’s left in this class:

Naturally, desktop‘s DesktopLauncher will now have to use AsteroidsClientGame instead of AsteroidsGame:

An actual multiplayer

If everything went smooth, you should now be able to run the server and two clients and see that they’re connected. On Linux/macOS it would be ./gradlew server:run and ./gradlew desktop:run, on Windows you’d use gradlew.bat instead of ./gradlew.

Here’s how launch procedure looks from IntelliJ IDEA (click on the image to see it in full screen):

game launching procedure

And here’s the game in action:

playing the game

If you’d like to run the game on different machines in LAN network, be sure to find out the local IP address of the machine that will run the server and export it on both server machine and all the connected clients as HOST environment variable.

That’s it for this part. Next time we’ll introduce proper lag compensation to make the game feel smooth even when there are latencies between clients and the server. See you then!

Subscribe to our newsletter