After we’ve dealt with setting up the server and the client, there’s one last thing to do in order to make the game smooth: how to compensate lags between them.

This is the fourth and final part of the series that will show you how to make a lag compensated multiplayer mode for the arcade classic. In case you missed the previous ones, you can check them out below:

The Lag Compensation

Render-only client, that we came up with in the previous part, works nicely when network latency is close to 0, but that’s almost never the case on the internet. There’s no way to avoid lags, by the sheer laws of physics. Even in a perfect world, if you’d have an optical fiber connection set up between, say, Los Angeles and New York and your signal traveled at the speed of light, you’d still have latencies of about 13ms. Now add a bunch of network hops with all kinds of bridges, routers, gateways, proxies and whatnot and you’re realistically looking at 100ms lag at best, all the way up to barely playable 300+ms. It may not sound like much, but I’ll demonstrate to you that in fact, it is.

Introducing delays

We’ll make a class that lets us perform delayed actions. It will be capable of creating instances bound to a specific amount of delay time. By default we’ll use daemon threads for that, because we don’t care for delayed tasks after main thread has returned.

If we created this class, we wouldn’t know anything about the types of tasks that it will perform. We’ll put it in general core/util package.

public class Delay { private final long amount; private final Timer timer; public Delay(long amount) { this(amount, new Timer(true)); } public Delay(long amount, Timer timer) { this.amount = amount; this.timer = timer; } public void execute(Runnable task) { if(amount == 0) { task.run(); return; } timer.schedule(new TimerTask() { @Override public void run() { task.run(); } }, amount); } } 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 public class Delay { private final long amount ; private final Timer timer ; public Delay ( long amount ) { this ( amount , new Timer ( true ) ) ; } public Delay ( long amount , Timer timer ) { this . amount = amount ; this . timer = timer ; } public void execute ( Runnable task ) { if ( amount == 0 ) { task . run ( ) ; return ; } timer . schedule ( new TimerTask ( ) { @Override public void run ( ) { task . run ( ) ; } } , amount ) ; } }

Now let’s hook it up to both the SocketIoClient and the SocketIoServer:

public class SocketIoClient implements Client { ... private final Delay delay; ... public SocketIoClient(String protocol, String host, int port, Delay delay) { ... this.delay = delay; } ... private void emit(Socket socket, Event eventName, Dto payload) { delay.execute(() -> socket.emit(eventName.toString(), payload.toJsonString())); } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class SocketIoClient implements Client { . . . private final Delay delay ; . . . public SocketIoClient ( String protocol , String host , int port , Delay delay ) { . . . this . delay = delay ; } . . . private void emit ( Socket socket , Event eventName , Dto payload ) { delay . execute ( ( ) - > socket . emit ( eventName . toString ( ) , payload . toJsonString ( ) ) ) ; } . . . }

public class SocketIoServer implements Server { ... private final Delay delay; ... public SocketIoServer(String host, int port, Delay delay) { ... this.delay = delay; } ... private void sendEvent(ClientOperations client, Event eventName, Dto data) { delay.execute(() -> client.sendEvent(eventName.toString(), data.toJsonString())); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 public class SocketIoServer implements Server { . . . private final Delay delay ; . . . public SocketIoServer ( String host , int port , Delay delay ) { . . . this . delay = delay ; } . . . private void sendEvent ( ClientOperations client , Event eventName , Dto data ) { delay . execute ( ( ) - > client . sendEvent ( eventName . toString ( ) , data . toJsonString ( ) ) ) ; } }

We’ll inject both Delays with 100ms amount of time in AsteroidsServerGame and AsteroidsClientGame:

Server server = new SocketIoServer(host, port, new Delay(100)); 1 Server server = new SocketIoServer ( host , port , new Delay ( 100 ) ) ;

Client client = new SocketIoClient(protocol, host, port, new Delay(100)); 1 Client client = new SocketIoClient ( protocol , host , port , new Delay ( 100 ) ) ;

Combined delays along with cross-process communication, game loop computation lag etc. will sum up to about 210ms. Run the game and try to play it.

It’s quite awful, right? Well, it should be. 200+ms is a noticeable delay between a key press and things actually happening, and it’s super frustrating when you’re trying to shoot an enemy and he flees before the delayed bullet reaches him. But we’ll alleviate that.

General approach

There are three entity types that we can apply lag compensation techniques for: the local Player, other Players and Bullets. These techniques will differ in details, but they’ll also share a fundamental pattern: due to network latencies we cannot know the exact state of the entity when we need to render it, but we can give it our best guess and apply corrections later, if needed. In other words, we’re going to pretend that we know how things are, show that, hope our bluff isn’t way off, and if it is, fix it as soon as possible.

Before you move on, I highly recommend you read the excellent series on developing fast-paced multiplayer games by Gabriel Gambetta. A lot of techniques shown here will be implementations of concepts described there.

Players lag compensation

Lag compensation for a local Player is most likely to match the actual state because we have most data to do it correctly. Here’s the gist: we’ll continue to calculate Player‘s position based on Controls as if it was purely local and eventually reconcile state with the server response. Given that we’ll run the same game logic and physics on the client, and the server and there’ll be no randomness involved, we should be able to come up with correct position most of the time and the server will just confirm that. An exception might be a situation when Player was shot down, and we haven’t received its new position yet.

Local controls

Ok, so right now with the server calculating all the game logic and the client only rendering it we have something like this:

But we really want the server to only confirm local state, so it can look seamless:

That should be easy enough, right? We’ll just make localPlayer use localControls and then server state will be just confirmation of what we already have on the client side. Let’s do just that.

First, we’ll pass localControls rather than new NoopControls() when Player connects in AsteroidsClientScreen:

... @Override public void show() { client.onConnected(introductoryStateDto -> { localPlayer = PlayerMapper.localPlayerFromDto(introductoryStateDto.getConnected(), localControls); ... 1 2 3 4 5 6 . . . @Override public void show ( ) { client . onConnected ( introductoryStateDto - > { localPlayer = PlayerMapper . localPlayerFromDto ( introductoryStateDto . getConnected ( ) , localControls ) ; . . .

And we’ll also update playersContainer in the render in order to apply controls, right after they’re sent to the server:

... client.sendControls(ControlsMapper.mapToDto(localControls)); playersContainer.update(delta); ... 1 2 3 4 . . . client . sendControls ( ControlsMapper . mapToDto ( localControls ) ) ; playersContainer . update ( delta ) ; . . .

Now let’s run the game and check out how our newly developed compensation works.

Well, does it? Nope, not at all. There’s hardly any change and now the ship is doing a weird little dance of going forward and backward before it finally moves.

The reason for that is we’re constantly updating based on the state from the server, but this state is from the past. By the time the server receives client input, processes it and sends it back it’s a whole other situation on the client side, but this past state gets accepted and applied. Let’s zoom in a bit on previous flow to get a better grasp of it:

To address this issue we’ll have to treat the server’s response as both validation of past state for everything that we can compute locally (like Player’s or Bullet’s next position), and as an update on everything that we can’t compute locally (like new Bullet being shot by other Player or new spawn position for the Ship).In order to achieve that we’ll need to keep indexed states locally – state 1, state 2, state N – and refer to them whenever server responds to check if our past state N is the same as server’s just received state N. If so then great, we can just move on, as illustrated below:

From the client’s perspective:

At state 0 Ship was at 0x, 0y and the player pressed key up

At state 1 Ship moved to 0x, 1y

At state 2 nothing significant happened

At state 3 the client received server’s state 0, which said that Ship was then at 0x, y0

At state 4 the client received server’s state 1, which said that Ship was then at 0x, 1y

All the client states saved locally matched states sent by the server, so for the player it looked like the game was instantly responsive, even though it took 200ms for a whole state roundtrip and validation. That’s a win.

But what if there was a difference in states? Let’s say that due to some glitch one player’s render loop is called with slightly higher delta than others, resulting in a faster-perceived Ship movement – so every time he presses an arrow he locally goes forward not 1, but 1.5 unit. Of course, the server is oblivious to that, as it should be, but how is it going to get reconciled on the client side?

…so in the end, Ship is at 2.5y at the client side. Wait, what? How did that happen?

At state 0 Ship was at 0x, 0y and the player pressed key up

was at 0x, 0y and the player pressed key up At state 1 Ship moved to 0x, 1.5y

moved to 0x, 1.5y At state 2 player pressed key up again

At state 3 Ship moved to 0x, 3y

moved to 0x, 3y At state 4 the client received GameStateDto saying that at state 1 it’s Ship should’ve been at 0x 1y, not 0x 1.5y as it was computed locally. So local history was rewinded to state 1 according to the server (0x 1y) and then all the player actions from states 2 to 4 were reapplied and passed through game loop instantly. Through states 2 to 4 player has performed one action – an up key press, which locally means going 1.5 unit, therefore in the end Ship is seen on the client at (1 + 1.5)y. The client might be still glitchy, but it gets corrected as smoothly as possible.

From those examples we can extract an algorithm for dealing with Ship latencies:

Whenever ControlsDto is sent to the server, mark it using an index

is sent to the server, mark it using an index Compute GameStateDto locally according to the Controls and save them both, along with index number and render ’s delta

locally according to the and save them both, along with index number and ’s delta When GameStateDto with index matching one sent with ControlsDto comes from the server, discard all saved states with lower index and compare locally stored GameStateDto with arriving one

with index matching one sent with comes from the server, discard all saved states with lower index and compare locally stored with arriving one If they’re equal don’t do anything

If they’re not equal, apply GameStateDto from the server and run game logic locally from received server index until last saved local index, using saved ControlsDto and delta

Back to the code

Ok, so here’s the plan: we’ll need to add indexes to ControlsDto and GameStateDto, make GameStateDtos comparable by equality, keep some sort of local history that’s able to rewind and rerun game loop and also keep indexes per client on the server. Smooth sailing.

Indexed Dtos and Mappers

Let’s tackle indexing Dtos first. Specifically, we’ll need to wrap our already existing Dtos with indexes, so let’s create a wrapper that is also a Dto:

public class IndexedDto<UnderlyingDto extends Dto> implements Dto { private final UnderlyingDto dto; private final long index; @JsonCreator public IndexedDto( @JsonProperty("dto") UnderlyingDto dto, @JsonProperty("index") long index) { this.dto = dto; this.index = index; } public UnderlyingDto getDto() { return dto; } public long getIndex() { return index; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class IndexedDto < UnderlyingDto extends Dto > implements Dto { private final UnderlyingDto dto ; private final long index ; @JsonCreator public IndexedDto ( @JsonProperty ( "dto" ) UnderlyingDto dto , @JsonProperty ( "index" ) long index ) { this . dto = dto ; this . index = index ; } public UnderlyingDto getDto ( ) { return dto ; } public long getIndex ( ) { return index ; } }

Having that, we’ll introduce missing IndexedControlsDto and IndexedGameStateDto with minimal effort:

public class IndexedControlsDto extends IndexedDto<ControlsDto> { @JsonCreator public IndexedControlsDto( @JsonProperty("dto") ControlsDto dto, @JsonProperty("index") long index) { super(dto, index); } } 1 2 3 4 5 6 7 8 public class IndexedControlsDto extends IndexedDto <ControlsDto> { @JsonCreator public IndexedControlsDto ( @JsonProperty ( "dto" ) ControlsDto dto , @JsonProperty ( "index" ) long index ) { super ( dto , index ) ; } }

public class IndexedGameStateDto extends IndexedDto<GameStateDto> { @JsonCreator public IndexedGameStateDto( @JsonProperty("dto") GameStateDto dto, @JsonProperty("index") long index) { super(dto, index); } } 1 2 3 4 5 6 7 8 public class IndexedGameStateDto extends IndexedDto <GameStateDto> { @JsonCreator public IndexedGameStateDto ( @JsonProperty ( "dto" ) GameStateDto dto , @JsonProperty ( "index" ) long index ) { super ( dto , index ) ; } }

You might be asking yourself, why do we need these concrete classes that just delegate to parent constructor, couldn’t we just use a generic type? Unfortunately no, because there’s no way to obtain .class property from a generic type, like IndexedDto<ControlsDto>.class, which we’ll need for mapping.

One last piece we’ll need to complete indexed Dtos puzzle is a Mapper for them. It won’t do anything else than just wrap existing Dtos with indexes:

public class IndexedDtoMapper { public static <UnderlyingDto extends Dto> IndexedDto<UnderlyingDto> wrapWithIndex(UnderlyingDto dto, long index) { return new IndexedDto<>(dto, index); } } 1 2 3 4 5 6 public class IndexedDtoMapper { public static < UnderlyingDto extends Dto > IndexedDto <UnderlyingDto> wrapWithIndex ( UnderlyingDto dto , long index ) { return new IndexedDto < > ( dto , index ) ; } }

Game State Dto equality

While we’re on the subject of Dtos, let’s introduce equality comparison to them. All we want is a standard equals and hashCode override. We’ll start with GameStateDto and work our way downwards from there, into types referred from GameStateDto: PlayerDto, ShipDto and BulletDto. I’ll omit the code here since it’s very rudimentary (and you’ll generate it with IDE anyway), but we’ll need it nonetheless.

Necessary adjustments

There are a couple of changes we’ll need to get out of the way before we can proceed.

First of all, our compensations will revolve around movement only. Right now we have both movement updates and other internal state updates entangled in update methods of Player, Ship and Container. We’ll need to refactor that in order to get more fine-grained control.

This is how our models and containers will change:

public class Ship implements Visible { ... public void update() { applyShootingPossibility(); } public void move(float delta) { applyMovement(delta); } ... } 1 2 3 4 5 6 7 8 9 10 11 public class Ship implements Visible { . . . public void update ( ) { applyShootingPossibility ( ) ; } public void move ( float delta ) { applyMovement ( delta ) ; } . . . }

public class Player implements Identifiable { ... public void update() { ship.ifPresent(Ship::update); } public void move(float delta) { ship.ifPresent(ship -> { ship.control(controls, delta); ship.move(delta); }); } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Player implements Identifiable { . . . public void update ( ) { ship . ifPresent ( Ship : : update ) ; } public void move ( float delta ) { ship . ifPresent ( ship - > { ship . control ( controls , delta ) ; ship . move ( delta ) ; } ) ; } . . . }

public interface Container<Thing extends Identifiable> { ... void update(); void move(float delta); ... } 1 2 3 4 5 6 public interface Container < Thing extends Identifiable > { . . . void update ( ) ; void move ( float delta ) ; . . . }

public class PlayersContainer<PlayerType extends Player> implements Container<PlayerType> { ... @Override public void update() { players.forEach(Player::update); } @Override public void move(float delta) { players.forEach(player -> player.move(delta)); } } 1 2 3 4 5 6 7 8 9 10 11 12 public class PlayersContainer < PlayerType extends Player > implements Container <PlayerType> { . . . @Override public void update ( ) { players . forEach ( Player : : update ) ; } @Override public void move ( float delta ) { players . forEach ( player - > player . move ( delta ) ) ; } }

public class BulletsContainer implements Container<Bullet> { ... @Override public void update() { bullets.removeIf(bullet -> !bullet.isInRange() || bullet.hasHitSomething()); } @Override public void move(float delta) { bullets.forEach(bullet -> bullet.move(delta)); } } 1 2 3 4 5 6 7 8 9 10 11 12 public class BulletsContainer implements Container <Bullet> { . . . @Override public void update ( ) { bullets . removeIf ( bullet - > ! bullet . isInRange ( ) | | bullet . hasHitSomething ( ) ) ; } @Override public void move ( float delta ) { bullets . forEach ( bullet - > bullet . move ( delta ) ) ; } }

Remember to find usages of PlayersContainer and BulletsContainer update methods and change them to two calls: move and update so that our code still behaves like before. Look for update calls on both client and server side. This split will enable us to target movements for compensation in particular.

Next we’ll need to open the Ship a little bit more. Apart from position and rotation, we’ll also synchronize velocity and rotationVelocity. It will allow us to make better state predictions.

public class Ship implements Visible { ... public Vector2 getVelocity() { return velocity; } public void setVelocity(Vector2 velocity) { this.velocity.set(velocity); } public float getRotationVelocity() { return rotationVelocity; } public void setRotationVelocity(float rotationVelocity) { this.rotationVelocity = rotationVelocity; } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Ship implements Visible { . . . public Vector2 getVelocity ( ) { return velocity ; } public void setVelocity ( Vector2 velocity ) { this . velocity . set ( velocity ) ; } public float getRotationVelocity ( ) { return rotationVelocity ; } public void setRotationVelocity ( float rotationVelocity ) { this . rotationVelocity = rotationVelocity ; } . . . }

We’ll need to update a ShipDto accordingly to these properties. Below you’ll find type declarations, I’ll omit code for initialization and getters here since it’s trivial.

public class ShipDto implements Dto { ... private final float velocityX; private final float velocityY; private final float rotationVelocity; ... } 1 2 3 4 5 6 7 public class ShipDto implements Dto { . . . private final float velocityX ; private final float velocityY ; private final float rotationVelocity ; . . . }

In order to put this additional data to some use we’ll need to include it in ShipMapper, when we’ll be mapping Dto from Ship and also when updating Ship by Dto:

public class ShipMapper { public static ShipDto fromShip(Ship ship) { Vector2 shipPosition = ship.getPosition(); Vector2 velocity = ship.getVelocity(); return new ShipDto(shipPosition.x, shipPosition.y, ship.getRotation(), velocity.x, velocity.y, ship.getRotationVelocity()); } ... public static void updateByDto(Ship ship, ShipDto dto) { ship.setPosition(new Vector2(dto.getX(), dto.getY())); ship.setRotation(dto.getRotation()); ship.setVelocity(new Vector2(dto.getVelocityX(), dto.getVelocityY())); ship.setRotationVelocity(dto.getRotationVelocity()); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ShipMapper { public static ShipDto fromShip ( Ship ship ) { Vector2 shipPosition = ship . getPosition ( ) ; Vector2 velocity = ship . getVelocity ( ) ; return new ShipDto ( shipPosition . x , shipPosition . y , ship . getRotation ( ) , velocity . x , velocity . y , ship . getRotationVelocity ( ) ) ; } . . . public static void updateByDto ( Ship ship , ShipDto dto ) { ship . setPosition ( new Vector2 ( dto . getX ( ) , dto . getY ( ) ) ) ; ship . setRotation ( dto . getRotation ( ) ) ; ship . setVelocity ( new Vector2 ( dto . getVelocityX ( ) , dto . getVelocityY ( ) ) ) ; ship . setRotationVelocity ( dto . getRotationVelocity ( ) ) ; } }

The last thing we’ll need to do is to make Player’s controls mutable. Reason for that is when we’ll rewind and rerun game loop we’ll need to be able to set it’s Controls state to whatever ControlsDto says it was at that moment, and that’s not possible with regular Controls (KeyboardControls in our case). This will make a lot more sense to you when we get to client side synchronization. The code change is trivial: just remove final keyword for controls property in Player class, and give it a getter and a setter.

Server side synchronization

Server won’t be very involved in compensating for lags, it’ll just need to keep state indexes for clients. We’ll create the synchronization package inside of connection and put a StateIndexByClient class there that will do just that:

public class StateIndexByClient { private final Map<UUID, Long> indexes; public StateIndexByClient(Map<UUID, Long> indexes) { this.indexes = indexes; } public StateIndexByClient() { this(new HashMap<>()); } public Long lastIndexFor(UUID id) { Long sequence = indexes.get(id); if(sequence == null) return -1L; return sequence; } public void setIndexFor(UUID id, Long value) { indexes.put(id, value); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class StateIndexByClient { private final Map < UUID , Long > indexes ; public StateIndexByClient ( Map < UUID , Long > indexes ) { this . indexes = indexes ; } public StateIndexByClient ( ) { this ( new HashMap < > ( ) ) ; } public Long lastIndexFor ( UUID id ) { Long sequence = indexes . get ( id ) ; if ( sequence == null ) return - 1L ; return sequence ; } public void setIndexFor ( UUID id , Long value ) { indexes . put ( id , value ) ; } }

There will be a slight change in how server receives and sends data – it won’t receive ControlsDto and send GameStateDto anymore, but IndexedControlsDto and IndexedGameStateDto, since we need to know what GameStateDto was computed for which ControlsDto.

First, we’ll inject StateIndexByClient instance into SocketIoServer:

Server server = new SocketIoServer(host, port, new StateIndexByClient(), new Delay(100)); 1 Server server = new SocketIoServer ( host , port , new StateIndexByClient ( ) , new Delay ( 100 ) ) ;

public class SocketIoServer implements Server { ... private final StateIndexByClient stateIndexByClient; ... public SocketIoServer(String host, int port, StateIndexByClient stateIndexByClient, Delay delay) { ... this.stateIndexByClient = stateIndexByClient; ... } ... } 1 2 3 4 5 6 7 8 9 10 11 public class SocketIoServer implements Server { . . . private final StateIndexByClient stateIndexByClient ; . . . public SocketIoServer ( String host , int port , StateIndexByClient stateIndexByClient , Delay delay ) { . . . this . stateIndexByClient = stateIndexByClient ; . . . } . . . }

We’ll need some way of obtaining index for soon to be computed state. It will come with ControlsDto from the client side, so we need to store it then for a particular client:

private void setupEvents() { ... addEventListener(Event.CONTROLS_SENT, (client, json, ackSender) -> { IndexedDto<ControlsDto> indexedDto = Dto.fromJsonString(json, IndexedControlsDto.class); stateIndexByClient.setIndexFor(client.getSessionId(), indexedDto.getIndex()); playerSentControlsHandler.accept(client.getSessionId(), indexedDto.getDto()); }); ... } 1 2 3 4 5 6 7 8 9 private void setupEvents ( ) { . . . addEventListener ( Event . CONTROLS_SENT , ( client , json , ackSender ) - > { IndexedDto <ControlsDto> indexedDto = Dto . fromJsonString ( json , IndexedControlsDto . class ) ; stateIndexByClient . setIndexFor ( client . getSessionId ( ) , indexedDto . getIndex ( ) ) ; playerSentControlsHandler . accept ( client . getSessionId ( ) , indexedDto . getDto ( ) ) ; } ) ; . . . }

Since we won’t broadcast to all the clients simultaneously anymore but rather do it in sequence, with index dedicated for each client, our SocketIoServer’s broadcast method will change the most:

... @Override public void broadcast(GameStateDto gameState) { socketio.getAllClients().stream() .forEach(client -> { Dto indexedDto = IndexedDtoMapper.wrapWithIndex( gameState, stateIndexByClient.lastIndexFor(client.getSessionId())); sendEvent(client, Event.GAME_STATE_SENT, indexedDto); }); } ... 1 2 3 4 5 6 7 8 9 10 11 . . . @Override public void broadcast ( GameStateDto gameState ) { socketio . getAllClients ( ) . stream ( ) . forEach ( client - > { Dto indexedDto = IndexedDtoMapper . wrapWithIndex ( gameState , stateIndexByClient . lastIndexFor ( client . getSessionId ( ) ) ) ; sendEvent ( client , Event . GAME_STATE_SENT , indexedDto ) ; } ) ; } . . .

Client side synchronization

This is where the most interesting things start to happen. We’ll introduce a bunch of trickery to pretend that the game reacts to player input instantly rather than waits for the server. First, let’s create package synchronization inside of connection and gather everything that encompasses local state (index, delta when game loop ran, ControlsDto sent to the server and GameStateDto computed locally according to these controls) in a value class:

class LocalState { private final long index; private final float delta; private final ControlsDto controlsDto; private final GameStateDto gameStateAfterLoop; LocalState(long index, float delta, ControlsDto controlsDto, GameStateDto gameStateAfterLoop) { this.index = index; this.delta = delta; this.controlsDto = controlsDto; this.gameStateAfterLoop = gameStateAfterLoop; } public boolean gameStateMatches(GameStateDto serverState) { return gameStateAfterLoop.equals(serverState); } public long getIndex() { return index; } public float getDelta() { return delta; } public ControlsDto getControlsDto() { return controlsDto; } } 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 class LocalState { private final long index ; private final float delta ; private final ControlsDto controlsDto ; private final GameStateDto gameStateAfterLoop ; LocalState ( long index , float delta , ControlsDto controlsDto , GameStateDto gameStateAfterLoop ) { this . index = index ; this . delta = delta ; this . controlsDto = controlsDto ; this . gameStateAfterLoop = gameStateAfterLoop ; } public boolean gameStateMatches ( GameStateDto serverState ) { return gameStateAfterLoop . equals ( serverState ) ; } public long getIndex ( ) { return index ; } public float getDelta ( ) { return delta ; } public ControlsDto getControlsDto ( ) { return controlsDto ; } }

Now we’ll move onto the actual synchronization. A class that we’re going to implement will keep track of previous locally computed states, and whenever a server state arrives it will compare it with what has been saved locally, thus apply corrections if needed, using callbacks to game loops.

public class LocalStateSynchronizer { private long currentIndex; private final List<LocalState> recordedStates; private final RemoteControls synchronizationControls; private Consumer<GameStateDto> gameStateUpdater; private Consumer<Float> gameLogicRunner; private Supplier<GameStateDto> gameStateSupplier; private Player localPlayer; 1 2 3 4 5 6 7 8 public class LocalStateSynchronizer { private long currentIndex ; private final List <LocalState> recordedStates ; private final RemoteControls synchronizationControls ; private Consumer <GameStateDto> gameStateUpdater ; private Consumer <Float> gameLogicRunner ; private Supplier <GameStateDto> gameStateSupplier ; private Player localPlayer ;

Properties of this class deserve an explanation:

currentIndex is what we’ll use for indexing ControlsDto that will be sent to the server

is what we’ll use for indexing that will be sent to the server recordedStates will hold, well, recorded LocalState s

will hold, well, recorded s synchronizationControls will be temporarily injected to the localPlayer in order to control it when we’ll rerun game loop

will be temporarily injected to the in order to control it when we’ll rerun game loop gameStateUpdater will be responsible for somewhat similar things that Client ’s onGameStateReceived handler was – updating models according to newly arrived state

will be responsible for somewhat similar things that ’s handler was – updating models according to newly arrived state gameLogicRunner will be a portion of game loop taken from AsteroidServerScreen ’s render method, the one that calls move on Container s

will be a portion of game loop taken from ’s method, the one that calls on s gameStateSupplier will be a way for us to obtain GameStateDto of current client state

will be a way for us to obtain of current client state localPlayer is pretty much self explanatory

public LocalStateSynchronizer(List<LocalState> recordedStates, RemoteControls synchronizationControls) { this.recordedStates = recordedStates; this.synchronizationControls = synchronizationControls; } public LocalStateSynchronizer() { this(Collections.synchronizedList(new ArrayList<>()), new RemoteControls()); } public void updateAccordingToGameState(Consumer<GameStateDto> updater) { gameStateUpdater = updater; } public void runGameLogic(Consumer<Float> runner) { gameLogicRunner = runner; } public void setLocalPlayer(Player localPlayer) { this.localPlayer = localPlayer; } public void supplyGameState(Supplier<GameStateDto> supplier) { gameStateSupplier = supplier; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public LocalStateSynchronizer ( List <LocalState> recordedStates , RemoteControls synchronizationControls ) { this . recordedStates = recordedStates ; this . synchronizationControls = synchronizationControls ; } public LocalStateSynchronizer ( ) { this ( Collections . synchronizedList ( new ArrayList < > ( ) ) , new RemoteControls ( ) ) ; } public void updateAccordingToGameState ( Consumer <GameStateDto> updater ) { gameStateUpdater = updater ; } public void runGameLogic ( Consumer <Float> runner ) { gameLogicRunner = runner ; } public void setLocalPlayer ( Player localPlayer ) { this . localPlayer = localPlayer ; } public void supplyGameState ( Supplier <GameStateDto> supplier ) { gameStateSupplier = supplier ; }

Current index will be obtained before sending ControlsDto to the server.

public long getCurrentIndex() { return currentIndex; } 1 2 3 public long getCurrentIndex ( ) { return currentIndex ; }

State will be recorded right after computation according to Controls mentioned above.

public void recordState(float delta, ControlsDto controlsDto) { recordedStates.add(new LocalState(currentIndex, delta, controlsDto, gameStateSupplier.get())); currentIndex++; } 1 2 3 4 public void recordState ( float delta , ControlsDto controlsDto ) { recordedStates . add ( new LocalState ( currentIndex , delta , controlsDto , gameStateSupplier . get ( ) ) ) ; currentIndex ++ ; }

If there are local states ahead of the server and latest server state is not equal to the corresponding state on the client side, we’ll need to instantly return to that state and rerun game logic based on that corrected state.

public void synchronize(IndexedDto latestState) { discardSnapshotsUntil(latestState.getIndex()); if(recordedStates.size() == 0) return; LocalState latestLocalState = recordedStates.get(0); if(latestLocalState.getIndex() != latestState.getIndex()) return; if(latestLocalState.gameStateMatches(latestState.getDto())) return; returnToLatestServerState(latestState); rerunGameLogic(); } private void rerunGameLogic() { Controls playerOriginalControls = localPlayer.getControls(); localPlayer.setControls(synchronizationControls); for(int i = 1; i < recordedStates.size(); i++) { LocalState localState = recordedStates.get(i); ControlsMapper.setRemoteControlsByDto(localState.getControlsDto(), synchronizationControls); gameLogicRunner.accept(localState.getDelta()); recordedStates.set(i, updateState(localState)); } localPlayer.setControls(playerOriginalControls); } private void returnToLatestServerState(IndexedDto<GameStateDto> latestState) { gameStateUpdater.accept(latestState.getDto()); recordedStates.set(0, updateState(recordedStates.get(0))); } private void discardSnapshotsUntil(long boundaryIndex) { recordedStates.removeIf(localState -> localState.getIndex() < boundaryIndex); } private LocalState updateState(LocalState oldState) { return new LocalState(oldState.getIndex(), oldState.getDelta(), oldState.getControlsDto(), gameStateSupplier.get()); } } 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 public void synchronize ( IndexedDto latestState ) { discardSnapshotsUntil ( latestState . getIndex ( ) ) ; if ( recordedStates . size ( ) == 0 ) return ; LocalState latestLocalState = recordedStates . get ( 0 ) ; if ( latestLocalState . getIndex ( ) ! = latestState . getIndex ( ) ) return ; if ( latestLocalState . gameStateMatches ( latestState . getDto ( ) ) ) return ; returnToLatestServerState ( latestState ) ; rerunGameLogic ( ) ; } private void rerunGameLogic ( ) { Controls playerOriginalControls = localPlayer . getControls ( ) ; localPlayer . setControls ( synchronizationControls ) ; for ( int i = 1 ; i < recordedStates . size ( ) ; i ++ ) { LocalState localState = recordedStates . get ( i ) ; ControlsMapper . setRemoteControlsByDto ( localState . getControlsDto ( ) , synchronizationControls ) ; gameLogicRunner . accept ( localState . getDelta ( ) ) ; recordedStates . set ( i , updateState ( localState ) ) ; } localPlayer . setControls ( playerOriginalControls ) ; } private void returnToLatestServerState ( IndexedDto <GameStateDto> latestState ) { gameStateUpdater . accept ( latestState . getDto ( ) ) ; recordedStates . set ( 0 , updateState ( recordedStates . get ( 0 ) ) ) ; } private void discardSnapshotsUntil ( long boundaryIndex ) { recordedStates . removeIf ( localState - > localState . getIndex ( ) < boundaryIndex ) ; } private LocalState updateState ( LocalState oldState ) { return new LocalState ( oldState . getIndex ( ) , oldState . getDelta ( ) , oldState . getControlsDto ( ) , gameStateSupplier . get ( ) ) ; } }

Client integration

So we have the tool for synchronization. Let’s move one layer up and use it in SocketIoClient to mark ControlsDto with appropriate index, and to perform state synchronization when the server responds. Remember when we’ve introduced IndexedDtos on the server? We’re going to use them now.

Additionally, we’ll keep track of indexes of received game states. There’s a chance that due to the network glitches, states might come out of order. If that happens (newly received state has lower index than one we’ve already seen) we’re going to ignore the state completely, since there’s no point in computing it anymore.

public class SocketIoClient implements Client { ... private final LocalStateSynchronizer localStateSynchronizer; ... private long lastReceivedGameStateIndex = -1; public SocketIoClient(String protocol, String host, int port, LocalStateSynchronizer localStateSynchronizer, Delay delay) { ... this.localStateSynchronizer = localStateSynchronizer; ... } ... @Override public void sendControls(ControlsDto controlsDto) { long index = localStateSynchronizer.getCurrentIndex(); emit(socket, Event.CONTROLS_SENT, IndexedDtoMapper.wrapWithIndex(controlsDto, index)); } ... private void setupEvents() { ... on(socket, Event.GAME_STATE_SENT, response -> { String gameStateDtoJson = (String) response[0]; IndexedDto<GameStateDto> indexedDto = Dto.fromJsonString(gameStateDtoJson, IndexedGameStateDto.class); if(indexedDto.getIndex() <= lastReceivedGameStateIndex) return; lastReceivedGameStateIndex = indexedDto.getIndex(); gameStateReceivedHandler.accept(indexedDto.getDto()); localStateSynchronizer.synchronize(indexedDto); }); } ... } 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 public class SocketIoClient implements Client { . . . private final LocalStateSynchronizer localStateSynchronizer ; . . . private long lastReceivedGameStateIndex = - 1 ; public SocketIoClient ( String protocol , String host , int port , LocalStateSynchronizer localStateSynchronizer , Delay delay ) { . . . this . localStateSynchronizer = localStateSynchronizer ; . . . } . . . @Override public void sendControls ( ControlsDto controlsDto ) { long index = localStateSynchronizer . getCurrentIndex ( ) ; emit ( socket , Event . CONTROLS_SENT , IndexedDtoMapper . wrapWithIndex ( controlsDto , index ) ) ; } . . . private void setupEvents ( ) { . . . on ( socket , Event . GAME_STATE_SENT , response - > { String gameStateDtoJson = ( String ) response [ 0 ] ; IndexedDto <GameStateDto> indexedDto = Dto . fromJsonString ( gameStateDtoJson , IndexedGameStateDto . class ) ; if ( indexedDto . getIndex ( ) < = lastReceivedGameStateIndex ) return ; lastReceivedGameStateIndex = indexedDto . getIndex ( ) ; gameStateReceivedHandler . accept ( indexedDto . getDto ( ) ) ; localStateSynchronizer . synchronize ( indexedDto ) ; } ) ; } . . . }

That’s all good, but it feels incomplete, doesn’t it? I mean, what about gameStateUpdater, gameLogicRunner and gameStateSupplier? All of that will be handled by the layer above, where our game logic related code lives. One more time we need to move up, into AsteroidsClientScreen.

Players lag compensation: the final round

It seems that we’re ahead of the final step now – plugging it all together in AsteroidsClientScreen. Most significant change here will be that we’ll pass executable code into localStateSynchronizer so it can actually perform game actions. The way we’ll be handling Players in Client event listeners will also change because we’ll have to take lag compensation into account.

public class AsteroidsClientScreen extends ScreenAdapter { ... private final LocalStateSynchronizer localStateSynchronizer; ... private final Arena arena; ... public AsteroidsClientScreen( Controls localControls, Client client, LocalStateSynchronizer localStateSynchronizer, Viewport viewport, ShapeRenderer shapeRenderer, Container playersContainer, Container bulletsContainer, ContainerRenderer playersRenderer, ContainerRenderer bulletsRenderer, Arena arena) { ... this.localStateSynchronizer = localStateSynchronizer; ... this.arena = arena; } @Override public void show() { client.onConnected(introductoryStateDto -> { localPlayer = PlayerMapper.localPlayerFromDto(introductoryStateDto.getConnected(), localControls); localStateSynchronizer.setLocalPlayer(localPlayer); playersContainer.add(localPlayer); GameStateDto gameStateDto = introductoryStateDto.getGameState(); gameStateDto.getPlayers().stream() .map(playerDto -> PlayerMapper.localPlayerFromDto(playerDto, new NoopControls())) .forEach(playersContainer::add); gameStateDto.getBullets().stream() .map(bulletDto -> BulletMapper.fromDto(bulletDto, playersContainer)) .forEach(bulletsContainer::add); }); client.onOtherPlayerConnected(connectedDto -> { Player connected = PlayerMapper.localPlayerFromDto(connectedDto, new NoopControls()); playersContainer.add(connected); }); client.onOtherPlayerDisconnected(uuidDto -> playersContainer.removeById(uuidDto.getUuid())); 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 AsteroidsClientScreen extends ScreenAdapter { . . . private final LocalStateSynchronizer localStateSynchronizer ; . . . private final Arena arena ; . . . public AsteroidsClientScreen ( Controls localControls , Client client , LocalStateSynchronizer localStateSynchronizer , Viewport viewport , ShapeRenderer shapeRenderer , Container playersContainer , Container bulletsContainer , ContainerRenderer playersRenderer , ContainerRenderer bulletsRenderer , Arena arena ) { . . . this . localStateSynchronizer = localStateSynchronizer ; . . . this . arena = arena ; } @Override public void show ( ) { client . onConnected ( introductoryStateDto - > { localPlayer = PlayerMapper . localPlayerFromDto ( introductoryStateDto . getConnected ( ) , localControls ) ; localStateSynchronizer . setLocalPlayer ( localPlayer ) ; playersContainer . add ( localPlayer ) ; GameStateDto gameStateDto = introductoryStateDto . getGameState ( ) ; gameStateDto . getPlayers ( ) . stream ( ) . map ( playerDto - > PlayerMapper . localPlayerFromDto ( playerDto , new NoopControls ( ) ) ) . forEach ( playersContainer : : add ) ; gameStateDto . getBullets ( ) . stream ( ) . map ( bulletDto - > BulletMapper . fromDto ( bulletDto , playersContainer ) ) . forEach ( bulletsContainer : : add ) ; } ) ; client . onOtherPlayerConnected ( connectedDto - > { Player connected = PlayerMapper . localPlayerFromDto ( connectedDto , new NoopControls ( ) ) ; playersContainer . add ( connected ) ; } ) ; client . onOtherPlayerDisconnected ( uuidDto - > playersContainer . removeById ( uuidDto . getUuid ( ) ) ) ;

Note that now when we receive GameStateDto we’ll immediately deal with Bullets only as they’re not lag compensated.

client.onGameStateReceived(gameStateDto -> { gameStateDto.getBullets().stream() .forEach(bulletDto -> { Optional<Bullet> bullet = bulletsContainer.getById(bulletDto.getId()); if(!bullet.isPresent()) { bulletsContainer.add(BulletMapper.fromDto(bulletDto, playersContainer)); } else { BulletMapper.updateByDto(bullet.get(), bulletDto); } }); List existingBulletIds = gameStateDto.getBullets().stream() .map(BulletDto::getId) .collect(toList()); bulletsContainer.getAll().stream() .map(Bullet::getId) .map(Object::toString) .filter(id -> !existingBulletIds.contains(id)) .collect(toList()) .forEach(bulletsContainer::removeById); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 client . onGameStateReceived ( gameStateDto - > { gameStateDto . getBullets ( ) . stream ( ) . forEach ( bulletDto - > { Optional <Bullet> bullet = bulletsContainer . getById ( bulletDto . getId ( ) ) ; if ( ! bullet . isPresent ( ) ) { bulletsContainer . add ( BulletMapper . fromDto ( bulletDto , playersContainer ) ) ; } else { BulletMapper . updateByDto ( bullet . get ( ) , bulletDto ) ; } } ) ; List existingBulletIds = gameStateDto . getBullets ( ) . stream ( ) . map ( BulletDto : : getId ) . collect ( toList ( ) ) ; bulletsContainer . getAll ( ) . stream ( ) . map ( Bullet : : getId ) . map ( Object : : toString ) . filter ( id - > ! existingBulletIds . contains ( id ) ) . collect ( toList ( ) ) . forEach ( bulletsContainer : : removeById ) ; } ) ;

This method will be similar to the one above, but it will be run by the LocalStateSynchronizer as a start of synchronization process (set last known server state, then apply local states).

localStateSynchronizer.updateAccordingToGameState(gameStateDto -> { gameStateDto.getPlayers().stream() .forEach(playerDto -> playersContainer .getById(playerDto.getId()) .ifPresent(player -> PlayerMapper.updateByDto(player, playerDto))); }); 1 2 3 4 5 6 localStateSynchronizer . updateAccordingToGameState ( gameStateDto - > { gameStateDto . getPlayers ( ) . stream ( ) . forEach ( playerDto - > playersContainer . getById ( playerDto . getId ( ) ) . ifPresent ( player - > PlayerMapper . updateByDto ( player , playerDto ) ) ) ; } ) ;

Next method will be a way for LocalStateSynchronizer to get to current game state.

localStateSynchronizer.supplyGameState(() -> GameStateMapper.fromState(playersContainer, bulletsContainer)); 1 localStateSynchronizer . supplyGameState ( ( ) - > GameStateMapper . fromState ( playersContainer , bulletsContainer ) ) ;

Here we’ll instruct synchronizer how to run game logic after applying server state. We’ll use method reference because that’s the exact same logic that we’ll also run when computing local predicted state.

localStateSynchronizer.runGameLogic(this::runGameLogic); client.connect(new PlayerDto(null, Randomize.fromList(Player.POSSIBLE_COLORS).toString(), null)); } 1 2 3 4 localStateSynchronizer . runGameLogic ( this : : runGameLogic ) ; client . connect ( new PlayerDto ( null , Randomize . fromList ( Player . POSSIBLE_COLORS ) . toString ( ) , null ) ) ; }

Here’s how our state recording will fit in the middle of the render method: we’ll run the game logic, and then save resulting state with delta and Controls data that was used to compute it.

@Override public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); if(!client.isConnected()) return; client.sendControls(ControlsMapper.mapToDto(localControls)); runGameLogic(delta); localStateSynchronizer.recordState(delta, ControlsMapper.mapToDto(localControls)); viewport.apply(); shapeRenderer.setProjectionMatrix(viewport.getCamera().combined); shapeRenderer.begin(ShapeRenderer.ShapeType.Line); playersRenderer.render(shapeRenderer); bulletsRenderer.render(shapeRenderer); shapeRenderer.end(); } @Override public void resize(int width, int height) { viewport.update(width, height, true); } @Override public void dispose() { shapeRenderer.dispose(); } private void runGameLogic(float delta) { playersContainer.streamShips() .forEach(arena::ensurePlacementWithinBounds); playersContainer.move(delta); } } 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 @Override public void render ( float delta ) { Gdx . gl . glClearColor ( 0 , 0 , 0 , 1 ) ; Gdx . gl . glClear ( GL20 . GL_COLOR_BUFFER_BIT ) ; if ( ! client . isConnected ( ) ) return ; client . sendControls ( ControlsMapper . mapToDto ( localControls ) ) ; runGameLogic ( delta ) ; localStateSynchronizer . recordState ( delta , ControlsMapper . mapToDto ( localControls ) ) ; viewport . apply ( ) ; shapeRenderer . setProjectionMatrix ( viewport . getCamera ( ) . combined ) ; shapeRenderer . begin ( ShapeRenderer . ShapeType . Line ) ; playersRenderer . render ( shapeRenderer ) ; bulletsRenderer . render ( shapeRenderer ) ; shapeRenderer . end ( ) ; } @Override public void resize ( int width , int height ) { viewport . update ( width , height , true ) ; } @Override public void dispose ( ) { shapeRenderer . dispose ( ) ; } private void runGameLogic ( float delta ) { playersContainer . streamShips ( ) . forEach ( arena : : ensurePlacementWithinBounds ) ; playersContainer . move ( delta ) ; } }

Only thing left to do is to inject new dependencies at AsteroidsClientGame

public class AsteroidsClientGame extends Game { ... @Override public void create() { ... Arena arena = new Arena(WORLD_WIDTH, WORLD_HEIGHT); ... LocalStateSynchronizer localStateSynchronizer = new LocalStateSynchronizer(); Client client = new SocketIoClient(protocol, host, port, localStateSynchronizer, new Delay(100)); asteroids = new AsteroidsClientScreen( localControls, client, localStateSynchronizer, viewport, shapeRenderer, playersContainer, bulletsContainer, playersRenderer, bulletsRenderer, arena); ... } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class AsteroidsClientGame extends Game { . . . @Override public void create ( ) { . . . Arena arena = new Arena ( WORLD_WIDTH , WORLD_HEIGHT ) ; . . . LocalStateSynchronizer localStateSynchronizer = new LocalStateSynchronizer ( ) ; Client client = new SocketIoClient ( protocol , host , port , localStateSynchronizer , new Delay ( 100 ) ) ; asteroids = new AsteroidsClientScreen ( localControls , client , localStateSynchronizer , viewport , shapeRenderer , playersContainer , bulletsContainer , playersRenderer , bulletsRenderer , arena ) ; . . . } }

Sweet! Let’s spin it up and see our newly developed synchronizer in action!

What the…

Bonus level: threads synchronization

Ok, it was supposed to be smooth, what’s with all the flickering?

Turns out we have mutable state shared between threads (also known as “debugging this crap was an ordeal”). Event handlers attached to SocketIoClient are running on different threads than rendering loop, so at any given moment we can be in the middle of updating/synchronizing state according to events and rendering the exact same state. Even worse, we’re performing loops in LocalStateSynchronizer.rerunGameLogic, so it’s not as easy as “just use immutable or concurrent data types”. State synchronization should either be applied fully or not at all, like a transaction.

Fortunately, there’s an easy way out of it without sacrificing too much performance.

We know that render loop has to run once every 16ms. We should not mutate state elsewhere when it happens, but locking for a few ms once every 16ms isn’t that bad. We’ll receive game state events from the server at about same pace, and we should also process one at a time, that’s the second constraint. Between local game loop and events there should be plenty of time for event handlers to compute everything and be ready to render next frame, so we’ll take that.

First, let’s extend Client interface with method for locking event handlers:

public interface Client { ... void lockEventListeners(); void unlockEventListeners(); ... } 1 2 3 4 5 6 public interface Client { . . . void lockEventListeners ( ) ; void unlockEventListeners ( ) ; . . . }

Then we’ll need to implement them in our SocketIoClient:

public class SocketIoClient implements Client { ... private final Lock eventListenersLock; ... public SocketIoClient(String protocol, String host, int port, Lock eventListenersLock, LocalStateSynchronizer localStateSynchronizer, Delay delay) { ... this.eventListenersLock = eventListenersLock; ... } ... @Override public void lockEventListeners() { eventListenersLock.lock(); } @Override public void unlockEventListeners() { eventListenersLock.unlock(); } ... private void on(Socket socket, Event eventName, Emitter.Listener handler) { socket.on(eventName.toString(), response -> { eventListenersLock.lock(); try { handler.call(response); } finally { eventListenersLock.unlock(); } }); } 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 public class SocketIoClient implements Client { . . . private final Lock eventListenersLock ; . . . public SocketIoClient ( String protocol , String host , int port , Lock eventListenersLock , LocalStateSynchronizer localStateSynchronizer , Delay delay ) { . . . this . eventListenersLock = eventListenersLock ; . . . } . . . @Override public void lockEventListeners ( ) { eventListenersLock . lock ( ) ; } @Override public void unlockEventListeners ( ) { eventListenersLock . unlock ( ) ; } . . . private void on ( Socket socket , Event eventName , Emitter . Listener handler ) { socket . on ( eventName . toString ( ) , response - > { eventListenersLock . lock ( ) ; try { handler . call ( response ) ; } finally { eventListenersLock . unlock ( ) ; } } ) ; }

In order to lock event handlers while render loop is going and open it up again when it finishes, we’ll need to use these new methods inside of AsteroidsClientScreen.render:

... @Override public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); if(!client.isConnected()) return; client.lockEventListeners(); ... client.unlockEventListeners(); } 1 2 3 4 5 6 7 8 9 10 11 . . . @Override public void render ( float delta ) { Gdx . gl . glClearColor ( 0 , 0 , 0 , 1 ) ; Gdx . gl . glClear ( GL20 . GL_COLOR_BUFFER_BIT ) ; if ( ! client . isConnected ( ) ) return ; client . lockEventListeners ( ) ; . . . client . unlockEventListeners ( ) ; }

And we can’t forget to inject some Lock implementation into SocketIoClient in AsteroidsClientGame:

Client client = new SocketIoClient(protocol, host, port, new ReentrantLock(), localStateSynchronizer, new Delay(100)); 1 Client client = new SocketIoClient ( protocol , host , port , new ReentrantLock ( ) , localStateSynchronizer , new Delay ( 100 ) ) ;

After all is said and done, our game should look way smoother:

The funny thing is, we’re getting other Players lag compensation for free. We cannot know their Controls state in the present, but we do know their velocities from the past, and based on that we can make good enough predictions about where they will be next.

So this is it. You’ve reached the final paragraphs in these series, congrats. I presume you’re a reasonable human being and you’d stop reading a long time ago if you didn’t enjoy it, so I’m glad and flattered that you’ve made it all the way to the end. As always, you can check out the finished code in the reference repository.

Wait, what about lag compensation for bullets? Or threads synchronization on the server? Or…

That’s true, we haven’t covered these. In fact, we haven’t covered a lot more than just these. But you’re a big boy (or girl) now. You don’t need me to show you things anymore. You’re well equipped to explore and experiment on your own. Go do it, be bold, learn as much as you can and have fun while you’re at it! 🙂

I’d like to thank Agnieszka Bień for meticulously going through the code of each part in these series and correcting a few things along the way, and Patryk Mrukot for valuable suggestions regarding descriptions.