Search code examples
javascriptthree.jsgame-enginegame-physics

How to implement a deterministic/tick-based game loop?


First of all, I'm trying to make a "simple" 3D game using Three.js and in the future some network framework to make it multiplayer, since I plan on doing the network part in the future I searched a little and discovered that most "action" game use a "tick" based game loop to make it possible to sync the clients and the server, and then interpolate between the ticks to make it smooth.

I already have some "working" code of the tick (handle input, update, draw) function, what I want to know is if my implementation is right, and how this "deterministic" loop should work, supposing that my implementation is working, when I increase the "tick rate" the game gets faster (the update function is running more times), is this right?

this.loops = 0;
this.tick_rate = 20;
this.skip_ticks = 1000 / this.tick_rate;
this.max_frame_skip = 10;
this.next_game_tick = performance.now();

This first part of the code is inside the constructor of the Game class

Game.prototype.run = function () {
    this.handle_input();

    this.loops = 0;

    while (performance.now() > this.next_game_tick && this.loops < this.max_frame_skip){
        this.up_stats.update();
        this.update();
        this.next_game_tick += this.skip_ticks;
        this.loops++;
    }

    this.draw();
    //monitor performance
    this.stats.update();

    //next update
    requestAnimationFrame(this.run.bind(this));
};

Full code at: https://github.com/derezzedex/first_three_js/blob/master/js/game/main.js


Solution

  • This looks pretty reasonable to me and I've used similar patterns in the past.

    Structuring synchronized simulations is a huge topic, but what you have is a good starting point and might be enough depending on the complexity of your game.

    edit: A bit more detail...

    Yes it works the same, except that this.dt is always the same. i.e. 1000 / your desired FPS for the game loop.

    If you want to do the smoothing/interpolation in between frames... you'll have to record the previous state of your object as well.. and you probably won't want to use the Euler rotations, since eulers don't interpolate well. because an angle of 360 degrees, flips back to 0, so the interpolation logic gets weird...

    But instead.. you can record the state before and after the update...

    and interpolate the .quaternion instead.. which for small changes in rotation works fine just linear interpolating .. If the changes are too big, you can use quaternion.slerp() which can handle interpolating over big distances.

    So you've got lastTickTime, and currentTime, and nextTickTime ....

    each frame.. you're doing something like:

    To interpolate you do something like:

    var alpha= (currentTime-lastTickTime) / (nextTickTime-lastTickTime);//nextTickTime-lastTickTime = your framerate delta so for 60fps = 1000/60 = 16.666666666666668
    
    var recip = 1.0 - alpha;
    
    object.position.x = (object.lastPosition.x * recip)+(object.nextPosition.x*alpha)
    object.position.y = (object.lastPosition.y * recip)+(object.nextPosition.y*alpha)
    object.position.z = (object.lastPosition.z * recip)+(object.nextPosition.z*alpha)
    
    object.scale.x = (object.lastScale.x * recip)+(object.nextScale.x*alpha)
    object.scale.y = (object.lastScale.y * recip)+(object.nextScale.y*alpha)
    object.scale.z = (object.lastScale.z * recip)+(object.nextScale.z*alpha)
    
    object.quaternion.x = (object.lastQuaternion.x * recip)+(object.nextQuaternion.x*alpha)
    object.quaternion.y = (object.lastQuaternion.y * recip)+(object.nextQuaternion.y*alpha)
    object.quaternion.z = (object.lastQuaternion.z * recip)+(object.nextQuaternion.z*alpha)
    object.quaternion.w = (object.lastQuaternion.w * recip)+(object.nextQuaternion.w*alpha)
    

    In a proper three app, you probably shouldn't store the lastPosition and nextPosition directly on the object, and instead put it in the object.userData, but whatever.. it will probably still work..