Jump to content

seperating update loop of game from update loop of rendering


aylictal
 Share

Recommended Posts

Hello-

This might seem basic, but when I look at boilerplate phaser projects i typically see 3 functions (either standalone, or a part of the newer es6 classes):

 

preload(){
}

create(){
}

update(){
}

 

Most game logic I've worked with does not tie in it's rendering calls with its physics calls such as player positional data, etc.  Is there a good standard for seperating these two concerns with phaser?

 

Thanks.

Link to comment
Share on other sites

The reason for this post is because (as of my understanding) update triggers request animationframe under the hood which is both good and bad.

RAF will render stuff as fast as the machine can possibly render it.

The issue is if you tie physics in with that update (such as positioning, or player speed, stuff like this) then it will update much faster on faster machines than on slower machines, which is why you don't really want to tie these two things together (otherwise known as framelocking).  This is so that while the renderer can be rendering many many many times, the updates to physics can be on a separate loop altogether.

Link to comment
Share on other sites

I think there is no way to separate game physics from game render updates, because they are bound together ... or what do the others think?

Quote

The issue is if you tie physics in with that update (such as positioning, or player speed, stuff like this) then it will update much faster on faster machines than on slower machines, which is why you don't really want to tie these two things together (otherwise known as framelocking). 

Is that true for Phaser 3?

Link to comment
Share on other sites

RAF doesn't run as fast as possible, it runs at a stable rate (usually 60Hz) when the thread is free or less when it's busy.

Physics systems receive the time delta from the update loop, so they won't necessarily run any faster or slower. 

Link to comment
Share on other sites

58 minutes ago, samme said:

RAF doesn't run as fast as possible, it runs at a stable rate (usually 60Hz) when the thread is free or less when it's busy.

 

It runs on several factors, but monitor refresh rate is one of them, so if a player is using a 144hz monitor, it is going to try to match that, which is drastically different than 60hz.

However, your answer about physics systems receiving time delta from the update loop is very helpful.  Is there a way to set up a rudimentary physics system in it's own method ie

preload(){
}
create(){
}
update(){
}
physics(){  //??
}
 

Link to comment
Share on other sites

1 hour ago, samme said:

RAF doesn't run as fast as possible, it runs at a stable rate (usually 60Hz) when the thread is free or less when it's busy.

RAF will fire as fast as it can, synced to display refresh rate. So on 60 Hz display, you get 60 Hz or less. On 144 Hz display it will run 144 Hz or less. On g-sync/freesync/hdmi 2.1 vrr, it can be anything.

What needs to be understood here is that phaser doesn't provide you with proper game update loop. It gives you RAF loop (or fallbacks). It's on your to handle whatever you need, e.g. fixed timestep physics with interpolation. V3 had recently added automatic fixed timestep and manual update options to matter.js physics. I'm not sure if the automatic fixed timestep is still running off RAF (which makes the option kinda pointless), but you can use the manual option in any case.

In my case, if the game is mostly async tween actions (think match 3 sliding stuff), I just use tweens and don't care about deltatime. As soon as I have any kind of physics or time based logic (even simple speed += acceleration), I switch to fixed timestep with interpolation. I did describe my approach in at least one topic here :)

Link to comment
Share on other sites

But yes, the problem is that player locations are going to be stored on a server, which is async by default, and the client then needs to get updated from messages sent, thus relying upon update() to interpret those will fail

Link to comment
Share on other sites

When it comes to syncing with server, it gets much more complicated. Depending on the approach you take, from simple interpolation from past data with a buffer through basic prediction and correction to converging prediction. All these, even though work similarly to rendering interpolation with fixed timestep physics, are there to solve different kind of problem though.
I wrote an article about Entity Interpolation for multiplayer games, but it's not really aimed at total beginners. That said I do link to other articles that describe the basics, so all this should get your started.

Link to comment
Share on other sites

Maybe we just have different ideas of "as fast as possible" here.

I mean RAF doesn't run continuously. It's synchronized, and it's stable as long the thread isn't busy. I said usually 60Hz because that's the most common refresh rate. If it's not 60 it will be come other stable rate.

More importantly, you can use the time delta in update: http://labs.phaser.io/edit.html?src=src\timestep\variable smooth step.js

 

Link to comment
Share on other sites

Yes, alright. Except that it might not be stable on gpu-synced displays.

Using provided delta time is of course the way to do anything, but you can't always apply it raw like that. Thus the need for fixed timestep for physics and basically any kind of numerical integration. As I mentioned before, even simple `speed += acceleration * dt` will behave differently at different dt.

Link to comment
Share on other sites

10 hours ago, Antriel said:

Using provided delta time is of course the way to do anything, but you can't always apply it raw like that. Thus the need for fixed timestep for physics and basically any kind of numerical integration. As I mentioned before, even simple `speed += acceleration * dt` will behave differently at different dt.

I don't understand what you mean by "behave differently" in this context.

Isn't the whole point of that formula that the physics will behave the same at different dt? If dt is 20, the object will move twice as far as if dt was 10, because it's been twice as long since the last update - hence making the physics behave correctly regardless of how often Update() is called.

Link to comment
Share on other sites

I'm not very versed in terms as far as math and physics goes, but basically physics engines use only approximations to calculate what happens. There's a few approaches and each have their pros and cons. If you take basic Euler:

position += velocity * dt;
velocity += acceleration * dt;

You will find out that different `dt` will give you different result within the same time. Semi-implicit Euler (just swap those 2 lines) is lot better, but still wrong. You can read pretty good article about it here.

So basically, to have predictable and stable physics, you want to run your `physics.update(dt)` with fixed `dt`. That means it's the same every time. But that also means you can't just call it once every frame, because time between frames won't be exactly `dt` every time. So you do something like:

var accumulator = 0;
var physicsTimestep = 1/60;

function renderUpdate(timeSinceLastFrame) {
  accumulator += timeSinceLastFrame;
  while(accumulator >= physicsTimestep) {
    accumulator -= physicsTimestep;
    physics.update(physicsTimestep);
  }
  render(timeSinceLastFrame);
}

 

Link to comment
Share on other sites

Yes I have figured out how to cater the native update loop to accomodate this, but also had to write my own render method:

This is what I'm working with to customize for this behavior.  I could probably use a code review to make sure my ducks are in line (it seems to work for me, but this heavy math involved).

 

class scene1 extends Phaser.Scene {
    constructor(){
        super({key: "scene1"});
        this.lag = 0;
        this.fps = 60; //physics checks 60 times/s
        this.frameduration = 1000/ this.fps;
    }
    phys(delta) {
        //phys checks and server IO events update state of entities here
    }
    render(delta){
        //rendering stuff here
    }
    update(timestamp, elapsed){
        if (elapsed > 1000) elapsed = this.frameduration;
        this.lag += elapsed;
        while(this.lag >= this.frameduration){
            this.phys(elapsed);
            this.lag -= this.frameduration;
        }
        this.render(this.lag/this.frameduration);
    }
}

 

Edited by aylictal
code comments to explain this madness
Link to comment
Share on other sites

Few notes:

  • Phaser 3 smooths out deltaTime so I can't see it ever being over 1000. That check isn't really needed.
  • You pass the raw elapsed time to `phys()`. That's not fixed timestep, you should pass `frameduration` (which is really bad name imo, it's not duration of the frame, it's your physics timestep).
  • What you pass into render isn't `delta`, it's progress towards next update that is to be used as interpolation value. I usually call it `alpha`, and although the name doesn't matter, I would avoid calling it `delta` as that's not what it is.
Link to comment
Share on other sites

Is interpolation/alpha really needed for the renderer?  Can't that be conducted in phys() checks where phys is updating the state based upon io sent from server, or evaluating state of current objects of the game (ie client side physics updates)?

Based upon your suggestions, review the following please if this makes the most sense:
 

class scene1 extends Phaser.Scene {
    constructor(){
        super({key: "scene1"});
        this.lag = 0;
        this.fps = 60; //physics checks 60 times/frame
        this.frameduration = 1000/ this.fps;
    }
    phys(currentframe) {
        //phys checks and server IO events update state of entities here
    }
    render(){
        //rendering stuff here
    }
    update(timestamp, elapsed){
        this.lag += elapsed;
        while(this.lag >= this.frameduration){
            this.phys(this.frameduration);
            this.lag -= this.frameduration;
        }
        this.render();
    }
}

 

Link to comment
Share on other sites

Looking good now :)

7 hours ago, aylictal said:

Is interpolation/alpha really needed for the renderer?

Technically not. It's there to improve the smoothness and avoid frame skips. Even if the display has the same refresh rate as physics, it will eventually go out of sync (frame skips, etc.). What happens then is that you could possibly be juuuust not enough to update physics, so you render the same frame again. That will cause visible stuttering.

Alternatively, if your user has higher refresh rate display (120/144, even more Hz displays are pretty common now), you essentially limit them to whatever your physics update is.

By providing alpha or however you call it. That is, a relative value of how close we are to next update, and then use that for interpolation, you essentially remove the stutter and enable smoother movement than your physics update rate is. The interpolation itself looks like this: `obj.pos = (obj.next_pos - obj.old_pos) * alpha + obj.old_pos;`
Where `obj` is whatever you update in physics and `pos` is whatever property that is (x, y, rotation usually). The `old_pos` and `next_pos` are previous and current values respectively. So in your physics update, first thing you do (before running physics) is `obj.old_pos = obj.next_pos` for all objects. And then instead of updating `pos` directly in your physics update, you update `next_pos`.

At a cost of at most one physics step additional input latency (which is really negligible in the whole picture), you get smooth movement at any display refresh rate (if it's high enough for smoothness that is :D).

Alternatively, you could run your physics using this approach at much much higher rate, say 200 Hz. And then interpolation stops being that useful. This is usually done if you need very stable physics and can afford the computational time (as you would be running multiple physics update per frame).

Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...