Understanding frame based Vs time based games

Normally, when you build a game, there is a routine which is called at each frame which updates the content to display. Think about a platformer, with characters moving at each frame. Those characters have a speed, and they move towards a direction by a certain amount of pixels every frame. Let’s see a simplified version of the circular endless runner I showed you during the last weeks:
var game;
var gameOptions = {
    bigCircleRadius: 250,
    playerRadius: 25,
    playerSpeed: 1
}
window.onload = function() {
    var gameConfig = {
        thpe: Phaser.CANVAS,
        width: 800,
        height: 800,
        scene: [playGame]
    }
    game = new Phaser.Game(gameConfig);
    window.focus()
    resize();
    window.addEventListener("resize", resize, false);
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.image("bigcircle", "bigcircle.png");
        this.load.image("player", "player.png");
    }
    create(){
        this.bigCircle = this.add.sprite(game.config.width / 2, game.config.height / 2, "bigcircle");
        this.bigCircle.displayWidth = gameOptions.bigCircleRadius * 2;
        this.bigCircle.displayHeight = gameOptions.bigCircleRadius * 2;
        this.player = this.add.sprite(game.config.width / 2, game.config.height / 2 - gameOptions.bigCircleRadius - gameOptions.playerRadius, "player");
        this.player.displayWidth = gameOptions.playerRadius * 2;
        this.player.displayHeight = gameOptions.playerRadius * 2;
        this.player.currentAngle = -90;
    }
    update(){
        this.player.currentAngle = Phaser.Math.Angle.WrapDegrees(this.player.currentAngle + gameOptions.playerSpeed);
        var radians = Phaser.Math.DegToRad(this.player.currentAngle);
        var distanceFromCenter = gameOptions.bigCircleRadius + gameOptions.playerRadius;
        this.player.x = this.bigCircle.x + distanceFromCenter * Math.cos(radians);
        this.player.y = this.bigCircle.y + distanceFromCenter * Math.sin(radians);
        var revolutions = gameOptions.bigCircleRadius / gameOptions.playerRadius + 1;
        this.player.angle = this.player.currentAngle * revolutions;
    }
}
// pure javascript to scale the game
function resize() {
    var canvas = document.querySelector("canvas");
    var windowWidth = window.innerWidth;
    var windowHeight = window.innerHeight;
    var windowRatio = windowWidth / windowHeight;
    var gameRatio = game.config.width / game.config.height;
    if(windowRatio < gameRatio){
        canvas.style.width = windowWidth + "px";
        canvas.style.height = (windowWidth / gameRatio) + "px";
    }
    else{
        canvas.style.width = (windowHeight * gameRatio) + "px";
        canvas.style.height = windowHeight + "px";
    }
}

Player speed is set in angles per frame at line 5. Being player speed 1, we will need 360 frames to travel all the way around the big circle.
What do you see here? If your browser runs at 60 frames per second, you should see the small circle moving all the way around the big circle in 360 frames / 60 frames per second = 6 seconds. So let’s say this is the speed you want, and you will code the entire gameplay having in mind the small circle will move all the way around the big circle in 6 seconds, assuming your browser will always render the game at 60 frames per second. What happens if the game for some reason lags, or your game renders faster than 60fps? Look at this script:
var game;
var gameOptions = {
    bigCircleRadius: 250,
    playerRadius: 25,
    playerSpeed: 1
}
window.onload = function() {
    var gameConfig = {
        thpe: Phaser.CANVAS,
        width: 800,
        height: 800,
        scene: [playGame]
    }
    game = new Phaser.Game(gameConfig);
    window.focus()
    resize();
    window.addEventListener("resize", resize, false);
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.image("bigcircle", "bigcircle.png");
        this.load.image("player", "player.png");
    }
    create(){
        this.bigCircle = this.add.sprite(game.config.width / 2, game.config.height / 2, "bigcircle");
        this.bigCircle.displayWidth = gameOptions.bigCircleRadius * 2;
        this.bigCircle.displayHeight = gameOptions.bigCircleRadius * 2;
        this.player = this.add.sprite(game.config.width / 2, game.config.height / 2 - gameOptions.bigCircleRadius - gameOptions.playerRadius, "player");
        this.player.displayWidth = gameOptions.playerRadius * 2;
        this.player.displayHeight = gameOptions.playerRadius * 2;
        this.player.currentAngle = -90;
    }
    update(){
        if(Phaser.Math.Between(0, 3) == 0){
            this.player.currentAngle = Phaser.Math.Angle.WrapDegrees(this.player.currentAngle + gameOptions.playerSpeed);
            var radians = Phaser.Math.DegToRad(this.player.currentAngle);
            var distanceFromCenter = gameOptions.bigCircleRadius + gameOptions.playerRadius;
            this.player.x = this.bigCircle.x + distanceFromCenter * Math.cos(radians);
            this.player.y = this.bigCircle.y + distanceFromCenter * Math.sin(radians);
            var revolutions = gameOptions.bigCircleRadius / gameOptions.playerRadius + 1;
            this.player.angle = this.player.currentAngle * revolutions;
        }
    }
}
// pure javascript to scale the game
function resize() {
    var canvas = document.querySelector("canvas");
    var windowWidth = window.innerWidth;
    var windowHeight = window.innerHeight;
    var windowRatio = windowWidth / windowHeight;
    var gameRatio = game.config.width / game.config.height;
    if(windowRatio < gameRatio){
        canvas.style.width = windowWidth + "px";
        canvas.style.height = (windowWidth / gameRatio) + "px";
    }
    else{
        canvas.style.width = (windowHeight * gameRatio) + "px";
        canvas.style.height = windowHeight + "px";
    }
}
If you look at the highlighted code, you’ll see we are simulating a lag by skipping frames on a random basis. Look at the game now:
How long does the small circle take to move all the way around the big circle? Still six seconds? No, way more. This happens because we are updating the game at each frame, and if the frame rate reduces, the game gets slower. Well, then just don’t reduce the frame rate, it’s simple! Yes, in a perfect world each device renders the game with the same amount of frames per second, which never changes no matter what happens. But we are living in the actual world. That’s why we are going to handle game movement using time. Look at this script:
var game;
var gameOptions = {
    bigCircleRadius: 250,
    playerRadius: 25,
    playerSpeed: 6000
}
window.onload = function() {
    var gameConfig = {
        thpe: Phaser.CANVAS,
        width: 800,
        height: 800,
        scene: [playGame]
    }
    game = new Phaser.Game(gameConfig);
    window.focus()
    resize();
    window.addEventListener("resize", resize, false);
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.image("bigcircle", "bigcircle.png");
        this.load.image("player", "player.png");
    }
    create(){
        this.bigCircle = this.add.sprite(game.config.width / 2, game.config.height / 2, "bigcircle");
        this.bigCircle.displayWidth = gameOptions.bigCircleRadius * 2;
        this.bigCircle.displayHeight = gameOptions.bigCircleRadius * 2;
        this.player = this.add.sprite(game.config.width / 2, game.config.height / 2 - gameOptions.bigCircleRadius - gameOptions.playerRadius, "player");
        this.player.displayWidth = gameOptions.playerRadius * 2;
        this.player.displayHeight = gameOptions.playerRadius * 2;
        this.player.currentAngle = -90;
    }
    update(t, dt){
        var deltaAngle = 360 * (dt / gameOptions.playerSpeed);
        this.player.currentAngle = Phaser.Math.Angle.WrapDegrees(this.player.currentAngle + deltaAngle);
        var radians = Phaser.Math.DegToRad(this.player.currentAngle);
        var distanceFromCenter = gameOptions.bigCircleRadius + gameOptions.playerRadius;
        this.player.x = this.bigCircle.x + distanceFromCenter * Math.cos(radians);
        this.player.y = this.bigCircle.y + distanceFromCenter * Math.sin(radians);
        var revolutions = gameOptions.bigCircleRadius / gameOptions.playerRadius + 1;
        this.player.angle = this.player.currentAngle * revolutions;
    }
}
// pure javascript to scale the game
function resize() {
    var canvas = document.querySelector("canvas");
    var windowWidth = window.innerWidth;
    var windowHeight = window.innerHeight;
    var windowRatio = windowWidth / windowHeight;
    var gameRatio = game.config.width / game.config.height;
    if(windowRatio < gameRatio){
        canvas.style.width = windowWidth + "px";
        canvas.style.height = (windowWidth / gameRatio) + "px";
    }
    else{
        canvas.style.width = (windowHeight * gameRatio) + "px";
        canvas.style.height = windowHeight + "px";
    }
}
At line 5 now the speed is no longer defined in degrees per frame, but in milliseconds needed to move all the way around the big circle. Still 6 seconds. Then at line 36 we have two arguments, representing respectively the amount in milliseconds since the game started, and the amount in milliseconds since last frame was rendered. Line 37 determines the amount of degrees the small circle should move according to elapsed time and circle speed. Look at the result:
The small circle moves again all the way around the big circle in 6 seconds. Let’s add some fake lag:
var game;
var gameOptions = {
    bigCircleRadius: 250,
    playerRadius: 25,
    playerSpeed: 6000
}
window.onload = function() {
    var gameConfig = {
        thpe: Phaser.CANVAS,
        width: 800,
        height: 800,
        scene: [playGame]
    }
    game = new Phaser.Game(gameConfig);
    window.focus()
    resize();
    window.addEventListener("resize", resize, false);
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.image("bigcircle", "bigcircle.png");
        this.load.image("player", "player.png");
    }
    create(){
        this.bigCircle = this.add.sprite(game.config.width / 2, game.config.height / 2, "bigcircle");
        this.bigCircle.displayWidth = gameOptions.bigCircleRadius * 2;
        this.bigCircle.displayHeight = gameOptions.bigCircleRadius * 2;
        this.player = this.add.sprite(game.config.width / 2, game.config.height / 2 - gameOptions.bigCircleRadius - gameOptions.playerRadius, "player");
        this.player.displayWidth = gameOptions.playerRadius * 2;
        this.player.displayHeight = gameOptions.playerRadius * 2;
        this.player.currentAngle = -90;
        this.lastUpdate = 0;
    }
    update(t){
        if(Phaser.Math.Between(0, 3) == 0){
            var dt = t - this.lastUpdate;
            this.lastUpdate = t;
            var deltaAngle = 360 * (dt / gameOptions.playerSpeed);
            this.player.currentAngle = Phaser.Math.Angle.WrapDegrees(this.player.currentAngle + deltaAngle);
            var radians = Phaser.Math.DegToRad(this.player.currentAngle);
            var distanceFromCenter = gameOptions.bigCircleRadius + gameOptions.playerRadius;
            this.player.x = this.bigCircle.x + distanceFromCenter * Math.cos(radians);
            this.player.y = this.bigCircle.y + distanceFromCenter * Math.sin(radians);
            var revolutions = gameOptions.bigCircleRadius / gameOptions.playerRadius + 1;
            this.player.angle = this.player.currentAngle * revolutions;
        }
    }
}
// pure javascript to scale the game
function resize() {
    var canvas = document.querySelector("canvas");
    var windowWidth = window.innerWidth;
    var windowHeight = window.innerHeight;
    var windowRatio = windowWidth / windowHeight;
    var gameRatio = game.config.width / game.config.height;
    if(windowRatio < gameRatio){
        canvas.style.width = windowWidth + "px";
        canvas.style.height = (windowWidth / gameRatio) + "px";
    }
    else{
        canvas.style.width = (windowHeight * gameRatio) + "px";
        canvas.style.height = windowHeight + "px";
    }
}
At line 35 we need a property to save the last update since the beginning of the game, in milliseconds. Line 37 now features only the amount of milliseconds passed since the game started. We are faking a lag, so there’s no point in getting the amount of milliseconds passed since last frame update. Lines 39 and 40 calculate the amount of milliseconds passed since last active frame and update the property to save the current time. Look at the result:
Yes, the game still lags, but the small circle still moves all the way around the big circle in 6 seconds. The game is not slower. It just runs in at a slower amount of frames per seconds. And this is the way you should move your characters. Download all the examples featured in this post and play with them.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers