Latency compensation causing trouble in a track & field remake

Hi!

I’m trying to make a racing game in meteor. Every player controls a character, and presses on the spacebar to run. The position of all the other runners is displayed on the screen of every client.

on the code side, i’ve got a collection with a player object for every client, with a posX variable which is used in a helper to generate CSS, and is updated every time someone presses on the spacebar.

I have a problem though : in most cases, during the race, as updates on the position of every player are being made at a very fast pace, every single client sees himself/herself as being ahead, even though that’s obviously not possible. At the very end of the race, there is a visual glitch as all the other runners suddenly rush forward to join the ending line. It looks like this :

it’s as if meteor was overwhelmed and decided to take care of all the local updates made by the player before deciding what to do with all the updates made by the other players. Is this the normal behaviour of Meteor? What strategy should i use to address this problem? Am i trying to use the wrong technology to make my game, where updates must be replicated on every client at a very fast pace? (say, 10 times per second for a fast player * 30 players maximum).

my method looks something like this, on the client side :

(javascript)

$(document.body).on(‘keyup’, function(e){
_posX = parseInt(Bonhomme.find({_id:playerId}).fetch()[0].posX)
Bonhomme.update(playerId, {$set:{“posX”:_posX+1}
}

(html)

{{#each runner}}

{{/each}}

i’m using meteor 1.4.2.3 and the game is intended to be used over a local area network rather than over the internet.

thank you for your insights and thank you for your contributions to this great framework!
samuel

PS. you can even try it out on the web, although it might not be practical to play on your own. (altough you can open several tabs & click on “autorun”)
http://sympo.shh.ovh/covid19/200 (the last part of the URL is the number of milliseconds between each simulated keydown)
PS. disclaimer : i learnt to use meteor in art school, so my javascript skills are meh! but i’m enjoying meteor since '2015.

1 Like

That is a very interesting project and pretty cool animation! You’re surely hammering the pub/sub :sweat_smile:

Just sharing some thoughts which might help, please take them with a grain of salt.

Since this a race, I think the reliability of the player’s positions is more important than the latency. I’d imagine the players clicking rapidly on the space bar, so they won’t even notice the latency as their avatar will be constantly moving.

With that in mind, I’d approach this slightly differently.

Perhaps, we can keep a map or array for each player and their position at the server and update that array with Meteor methods, then we can broadcast the updates to all clients with all positions (the entire array) using something like Meteor streamer. Then each player will use that array to draw the positions.

With this approach, you’ll decrease the latency since there will be no trip the database and you’ll ensure accuracy since you’ve one source of truth for the board. The downside of this is that there might be some delay between the moment the user clicks the spacebar and that when they see their avatar move…but at least we can be sure that the positions are accurate.

I hope that helps a bit.

3 Likes

One thing you might also want to try before the pursuing the path above is to use Meteor methods to update the positions while keeping the subscriptions.

This will bypass the latency compensation and the players might see changes to the others position before theirs. This might be a good enough solution with little refactoring, especially if you run on a LAN with the server and database on the same machine.

2 Likes

It’s…a…latency compensation game.

Regarding the visual glitch, that’s a strange issue indeed. Perhaps you can introduce a timestamp relative to the start time, so that posX can be throttled to change at a pace that is visually relevant to the player’s space bar activity?

In terms of alternatives, you can try mongodb change streams (a recently announced package maestroqadev:pub-sub-lite apparently uses them on top of Meteor Method calls). Another technology to consider is WebRTC.

1 Like

I actually made a very similar game (for work, so closed source) with horse racing on a webGL canvas.
A bit easier in that there was a single shared screen for the race, and iPads as controllers.

We kept latency down by skipping mongo, and using custom DDP messages with omega:custom-protocol and omega:json-protocol.

We only sent player commands to the screen and it did the movement, but you could just as easily have each message update a position array and broadcast the array to each client. Then each client would listen for messages and update the positions to match the server.

 ┌────────────┐                                             
 │  Client 1  │                                             
 └────────────┘                                             
        │ keydown                                           
        ▼                                                   
 ┌─────────────────────────────┐                            
 │           Server            │                            
 │       position array        │                            
 │    ┌───┬───┬───┬───┬───┐    │                            
 │    │ 1 │ 0 │ 0 │ 0 │ 0 │    │                            
 │    └───┴───┴───┴───┴───┘    │                            
 └─────────────────────────────┘                            
                │Broadcast new positions                    
      ┌─────────┴─┬───────────┬───────────┬───────────┐     
      │           │           │           │           │     
      ▼           ▼           ▼           ▼           ▼     
┌──────────┐┌──────────┐┌──────────┐┌──────────┐┌──────────┐
│ Client 1 ││ Client 2 ││ Client 3 ││ Client 4 ││ Client 5 │
└──────────┘└──────────┘└──────────┘└──────────┘└──────────┘
            All clients draw positions to screen            
5 Likes

Just reiterating that mongo is the biggest bottleneck here. Those data does not need to pass through mongodb.

The redisoplog has an option to bypass mongo writes but there was a long-standing bug. Not sure if it is already fixed

1 Like

thank you SO MUCH for the feedback! I wasn’t expecting so many answers for a pretty broad question.

@alawi thank you for the appreciation! i didn’t know that using methods bypassed latency compensation, will definitely try this first as it doesn’t imply a lot of refactoring as you mentioned. I will also check out meteor streamer and DDP events.

@jonlachlan haha it is indeed! throttling is something i though i might try… the pubsub lite package seems interesting also!

@coagmano awesome! didn’t know of the custom-protocol package, sounds like a cool alternative to DDP.

@rjdavid thank you! i didn’t grasp that simple fact, this confirms that it’s the way to go forward for my game.

1 Like

If the method you are calling is located both on the server and the client, then latency compensation is used. The method is first executed as it is available to the client and the UI updates immediately and then once the result from the server arrives, the optimistic first update is fixed with the actual result if necessary. But if the method is only defined on the server and the client side just calls it, then no latency compensation is used.

So if you want to disable latency compensation for certain methods, just don’t import them in your client side code. Or if you are relying on the old system where code is imported automatically, just move the methods to your server directoy.

2 Likes

thank you for the heads up! i’ll make sure i declare the method on the server side only.

Just to clarify, this is similar to streamer, where it uses the DDP channel, but handles the messages directly, instead of using Mongo or Meteor’s methods or pub/sub.

Streamer looks maybe a bit easier to use, custom-protocol is pretty low level

1 Like

@coagmano thank you for the clarification!

all the useful feedback i received here makes me feel i owe you a short update on the project. We successfully refactored our app to use meteor streamer which effectively eliminated our latency compensation bug. Moving the update function to a server-side method wasn’t enough to solve our problem (i don’t remember what was the new bug but i think it was simply too slow for our purpose)

we also made a throttling function on the server side : basically every time a client presses on the spacebar to run, it sends an instruction to the server, which is stored in an “instructions array”. The server empties this array every 10th of second, using its contents to calculate the position of every player and sending out the new coordinates to all the clients.

here is images of a simulated run, where we can see that :

the position of every “player” (here it’s only a simulation, not real human players) is identical on every screen (cool!)
some short freezes occur, but it might be because we’re running 6 tabs per computer (not every “player” is on a different computer).

shorter

i might post another update when i’ve tested the game in real conditions (30 players, 1 computer/player).

webRTC seems an exciting way to go but it also sounds pretty hard to implement.

#################

code excerpts :

keypress client side calls server side method

 /* client side */
 var nextEvent = function(){
      Meteor.call("requestStepServerSide", playerId)
 }

server stores the instruction in an array

/* server side */
requestStepServerSide: function(who){
  stepQueue.push(who);
},

clicking on a button to start the race causes the worker function to run every 10th of second

 /* server side */
 startTimerSteps: function(){
    timerSteps = Meteor.setInterval(function(){
       Meteor.call('stepServerSide')
  },timerStepsInterval);
}

the worker function (clears the instruction array, broadcasts message to the clients)

/* server side */
stepServerSide:function(){
  updates = 0;
  for (var i = 0; i < stepQueue.length; i++) {
    stepQueue[i]
    typeof posTable[stepQueue[i]] === 'undefined' ? posTable[stepQueue[i]] = 1 : posTable[stepQueue[i]]++;
    updates++;
  }
  stepQueue = []
  streamer.emit('message', posTable);
},

the client then executes this function every time it receives a message (streamer.on(‘message’)[…])

/* client side */
redrawPlayers=function(posTable){
    $.each(posTable, function(key, value){
       if(key == playerId && value>90){
          clearInterval(timer);
          return
       }else{
          var doesPlayerExist = document.getElementById(""+key)

     if(doesPlayerExist!==null){
          doesPlayerExist.style.transform="translateX("+value+"vw)"
     }
   }
})

thank you!
samuel

1 Like