phaserjs / phaser

Phaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.
https://phaser.io
MIT License
36.94k stars 7.08k forks source link

Real-time vs. Game time vs. Physics time, various timing issues #798

Closed draklaw closed 9 years ago

draklaw commented 10 years ago

There are various issues with timings that may lead to game-breaking bugs, particularly on slow systems. Parts of these problems are due to the fact there is no clear distinction between real-time, game-time and "physics-time" in Phaser.

I have made a small test program to illustrate this:

BasicGame.Game.prototype = {

    preload: function () {
        this.load.image('realTime', 'target.png');
        this.load.image('green', 'green.png');
        this.load.image('timer', 'timer.png');
        this.load.image('physic', 'cursor.png');
    },

    create: function () {

        // Choosing a deltaCap > 1/60 allow to emulate slow systems that are
        // not able to run at 1/deltaCap fps. 
        this.time.deltaCap = 1/120.;

        // The speed of the sprites, in px/s.
        this.speed = 100;

        // The refresh delay of timerSprite.
        this.timerTime = 15;

        // Position of realTimeSprite is computed from Time.totalElapsedSeconds().
        this.realTimeSprite = this.add.sprite(50, this.game.height / 2, 'realTime');
        this.realTimeSprite.anchor.set(.5, .5);

        // Position of accumSprite is updated according to Time.elapsed.
        this.accumSprite = this.add.sprite(50, this.game.height / 2, 'green');
        this.accumSprite.anchor.set(.5, .5);
        this.accum = 0;

        // Position of timerSprite is updated regularily by a timer.
        this.timerSprite = this.add.sprite(50, this.game.height / 2, 'timer');
        this.timerSprite.anchor.set(.5, .5);
        this.time.events.loop(this.timerTime, function() {
            this.timerSprite.x += this.speed * this.timerTime / 1000.;
        }, this);

        // Position of physicSprite is updated by Arcade with a constant speed.
        this.physicSprite = this.add.sprite(50, this.game.height / 2, 'physic');
        this.physicSprite.anchor.set(.5, .5);
        this.game.physics.arcade.enable(this.physicSprite);
        this.physicSprite.body.velocity.x = this.speed;

        console.log("plop");

    },

    update: function () {

        this.realTimeSprite.x = 50 + this.time.totalElapsedSeconds() * this.speed;

        this.accum += this.time.elapsed;
        this.accumSprite.x = 50 + this.accum/1000. * this.speed;

    },

};

You can test it there: http://www.draklia.net/phaser-bug-test/ . In this example, 4 originally overlapping sprites are moving right at a constant speed (here, 100px/s). They differ by the way their position are updated:

All these methods lead to different results. Let's take a closer look:

In the remaining, I assume that not using deltaCap is not an option in most games, because in case of small freezes characters may easily go through walls or encounter some other bugs (0.5/1s freezes do occur in web browser).

There are 3 problems:

  1. The main problem is that "game-time" is not well-defined in phaser. Is it real-time without the pauses ? Is it the "physics" time that is slower than real-time when a frame takes more time than deltaCap ? I admit I am a bit confused about the difference between Time.elapsed and Time.physicsElapsed. Everything except physics (e.g. timers) seems to follow Time.elapsed, but I hardly see a case where one would expect to have physics going at a different pace than the rest of the game.
  2. Timers can only be triggered once per frame, which may be a problem in case of slowdowns. Yet, users may not expect that a timer trigger several times per frame, so it may be useless or worst, lead to bugs. It may still be useful as an option (disabled by default) to allow a timer to be triggered several time in one frame when required.
  3. I think it is a good thing that we can access both real-time and game-time. However it is not always clear in the documentation which function reference which time. For instance, it is not clear that totalElapsedSeconds measure real-time. (By the way, I wonder about the other 'elapsed...' methods.)

I believe that time methods should fall in two categories: real-time (time goes by at a steady speed, including during pauses) and game-time (can be slower than real-time, "skip" pauses, may be scaled for funny effects like slow-motion). Given the current state of Phaser, it would make sense to me that game-time is the actual "physics-time".

A solution would be to split time-related stuff in two sets of objects, those working in real-time and the others. This way, users could access to game.realtime or game.gametime depending on their needs, and still have game.time for backward compatibility. Practically, gametime and realtime may share the same class but would not be 'fed' the same way, thus reducing code duplication. It would make the whole timing thing more consistent and explicit.

A more long-term goal may be to allow the game loop to use a fixed time step (with interpolation for rendering). It would be anyway useful for advanced physics engines like P2, which typically require a fixed time step for stability. The solution exposed above is a first step in this direction.

draklaw commented 10 years ago

I just pushed a suggestion of fix. Feedback welcome.

There are the new things:

class Clock:

class Time:

class Timer:

And some notes / things to do to clean the interface when backward-compatibility can be broke:

class Clock:

class Time:

class Timer:

Other things:

woutercommandeur commented 10 years ago

Interesting approach and if it works as advertised would possibly fix a lot of the timing. Do you have a copy of your demo running where the issues are fixed with this new clock solution?

photonstorm commented 10 years ago

Agreed, I too would like to see some test cases showing the improvements. I did take a look at your fork but I couldn't see anything that would change the way in which the physics delta timer was calculated at all (so surely the issues you outline above would remain in that one respect?), but that may have been one of your TODO items.

There is only one concept of Time in Phaser: real world time (i.e. the time specified by the device) and everything is based on this (Timers, tween durations, sound markers, animation frame rates, etc). The physics delta timer is simply a real-time based delta with optional cap, nothing more.

In order for swapping to Game Time to be an effective change all of these things would need to be modified to allow them to use custom Clocks. This isn't a problem as such, it's just a huge amount of API modifying work. Major point release level changes imho.

Here are some issues that I'd be curious to know how you'd handle (I'm not trying to catch you out or anything, I'd just like to know how you'd deal with them):

Unless the physics delta was linked to a Game Clock I can't see how you'd ever avoid your "lava" situation? I think the issue is deeper than this though, I think what you'd need is to have Game Clock level control over the physics step itself, otherwise surely you've got the same penetration issues as with the current set-up? (just in a slightly different time frame). Seeing as Arcade Physics doesn't work on a step basis I reckon you're dealing with a fundamental change to how its values are derived (and/or implementing a step), which may not be worth the effort involved given how primitive it is anyway?

draklaw commented 10 years ago

I did not change the way physics time is calculated. The purpose of my modifications is to provide a consistent timing system that allow to work easily with various time references. The main clock (game.time.clock) follows the "physic time" computed using 'deltaCap'. Working with it ensure that everything is synchronized on this reference (objects updated with physics, timers and the clock time). As user can make it's own clocks, or we can add state-wise clocks (see below), it effectively adds to phaser different concepts of time.

There is an updated version of the demo of my previous post: http://www.draklia.net/phaser-timing-test/ . The top line is the same as the previous demo, with the same problems (because I tried to keep backward-compatibility). The bottom line is the same, but using the new 'game.time.clock' instead of 'game.time'. As it is a consistent time reference everything stay in sync. The only remaining problem occur if a timer as a delay smaller than 'deltaCap' because event can be triggered only once per frame.

Updating Phaser to use custom clock anywhere time is required would indeed require major API refactoring and I agree this should wait for a major point release. However, the modification I made are (hopefully) backward-compatible and can already be used in games to solve some timing issues, like the lava bridge problem (see demo).

About the points you mention:

photonstorm commented 10 years ago

To clarify the point about callbacks happening at any time: I meant DOM based events, keyboard, mouse, touch, etc, not Timer events. DOM level events happen out of sync and can impact a running Timer (destroy it, add new ones, etc). It's here that I see the most weird stuff happen.

hansvn commented 9 years ago

I love your comprehensive request!

I was wondering if it is also possible to access the Phaser Timer class with physicsElapsed seconds. for example: game.time.events.addAtPhysicsTime() insead of game.time.events.add()

If you are on a slow machine, the Timer class only acepts realTime (milli)seconds and therefore these events can get called before you intend it to get called (similar to you lava crumbling the tiles problem).

photonstorm commented 9 years ago

Closing this request off as we have now fully implemented a proper physics timer internally and custom game specific fps rates. There is still no concept of a "Game clock" but that isn't going to be part of Phaser 2.