Codédex | Add Time Travel to Games with Phaser

Add Time Travel to Games with Phaser

Julien Kris

·

45 min read

·

Dec 16, 2025

Introduction

For December 2025’s Game Jam, we're challenging you to develop games within the theme of "The Changing of Time"! This can mean so many different things, and we figured it's a perfect opportunity to give y'all some ideas and inspiration for how to mess with time in Phaser!

There are tons of games out there that you can check out for inspo, including Braid, Superhot, Prince of Persia: The Sands of Time, and sooo many more.

Braid (2008) is a puzzle game where the player controls the flow of time in order to solve puzzles.

In this project tutorial, we will learn how to implement some cool time-based game mechanics into a simple platformer made with Phaser.

The Changing of Time

So how do we add a time element in our game? Here are a few that come to mind:

Phaser's Arcade Physics Engine

In order to access most of the options we're about to discuss, you'll need to make sure to set phaser's default physics engine to "arcade" in the config object.

"arcade" is Phaser's lightweight physics system. It handles gravity, collisions, and other physics properties for two different types of physics shapes: rectangles and circles. It's made for arcade/retro-style games like platformers, top-down games, or puzzle games.

Here's what that'll look like:

const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  backgroundColor: "#87CEFA",
  // Physics engine set up here
  physics: {
    default: "arcade",
    arcade: {
      debug: true
    }
  },
  scene: { preload, create, update }
};

Time Slowing Down & Speeding Up

Phaser contains a property called .timeScale that allows us to slow down, speed up, and even freeze time. It takes in values like 0.0001 (this makes time feel frozen without setting .timeScale to 0, which would break the physics engine), 0.5 (half-speed), 1 (normal speed), and 2 (speed up).

// freeze
this.physics.world.timeScale = 0.0001;

// slow motion
this.physics.world.timeScale = 0.5;

// normal speed
this.physics.world.timeScale = 1;

// speed up
this.physics.world.timeScale = 2;

.timeScale is a property from Phaser's arcade physics engine. As long as Phaser's physics mode is set to "arcade" in the config object, you'll be able to access .timeScale.

Time Moves Only When You Move

In the game Superhot (2016), time only moves when you move. Wild, right?

Here's how we could achieve that with Phaser:

if (!cursors.left.isDown && !cursors.right.isDown && !cursors.up.isDown) {
  // if NONE of the keys are pressed, freeze time
  this.physics.world.timeScale = .0001;
} else {
  // if AT LEAST one key is pressed, time passes as normal
  this.physics.world.timeScale = 1;
}

Here, we're using the same .timeScale property as before. This time, we're checking to see if any of the keys are being pressed to determine whether time freezes or moves.

Time Works Differently in Different Environments

.timeScale is just one way of achieving the effect that time is moving more slowly or quickly. We can also manipulate gravity to make movement in the world feel slower or faster.

For example, time might work differently in different environments, or in different coordinates in a 2D platformer game.

if (player.y < 300) {
  this.physics.world.gravity.y = 200;   // floaty room
} else {
  this.physics.world.gravity.y = 600;   // normal room
}

Here, the player's movement feels floaty and slow if their y-position is less than 300, and otherwise their movement is at a normal gravity and speed.

Building a Platformer Game with Time Travel

Now that we've gone over some examples for manipulating time in games, let's build a simple platformer game from scratch, incorporating the concept of time travel, as well as the ability to slow down and speed up the player's movement.

As with any Phaser scene, we'll need to set up our config object.

const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  backgroundColor: "#87CEFA",
  physics: {
    default: "arcade",
    arcade: { debug: false }
  },
  scene: { preload, create, update }
};
new Phaser.Game(config);

Phaser.AUTO picks WebGL or Canvas automatically, based on what the browser or device can handle. It tells Phaser to use WebGL if possible since it's faster, GPU-powered and can render more complex elements. It'll fall back to Canvas if on an older device, since it's slower, but compatible with a wider range of devices.

Some other properties of the config object include:

Next, we'll declare our global variables:

let player;
let cursors;
let periodText;
let speedText;
let platforms = [];
let currentPeriodIndex = 1;
let timePeriods = ["Past", "Present", "Future"];

We created an empty platforms array to keep references to the rectangular platform GameObjects so the code can add colliders and later change their colors.

currentPeriodIndex = 1 is so that the game starts in "Present" (we'll map this later in the code).

periodText and speedText will display the current time period we're in and whether the character is moving slow, fast, or normal.

preload()

In the preload() function, we'll load our character image.

function preload() {
  this.load.image("character", "https://i.imgur.com/TTQA8lp.gif");
}

create()

Next comes the create() function, which gets a bit more complex.

function create() {
  this.physics.world.gravity.y = 500;
}

This sets a global gravity in the physics world, which pulls objects downward on the y-axis.

function create() {
 this.physics.world.gravity.y = 500;

// New code
 player = this.physics.add.sprite(400, 200, "character").setScale(0.4);
 player.setCollideWorldBounds(true);
 player.body.setSize(player.width * 0.85, player.height * 0.85);
 player.body.setOffset(player.width * 0.1, player.height * 0.1);
}

This adds a physics-enabled sprite at (400, 200) and scales it down to 0.4, or 40%.

function create() {
  this.physics.world.gravity.y = 500;
  player = this.physics.add.sprite(400, 200, "character").setScale(0.4);
  player.setCollideWorldBounds(true);
  player.body.setSize(player.width * 0.85, player.height * 0.85);
  player.body.setOffset(player.width * 0.1, player.height * 0.1);

// New code
  const makePlatform = (x, y, width, height) => {
    const p = this.add.rectangle(x, y, width, height, 0x00aa00);
    this.physics.add.existing(p, true);
    platforms.push(p);
    return p;
  };

makePlatform(400, 580, 800, 40);
  makePlatform(200, 250, 200, 20);
  makePlatform(600, 350, 200, 20);
  makePlatform(350, 450, 200, 20);

platforms.forEach(p => this.physics.add.collider(player, p));
}

Next, we'll set up the arrow key input and the text display.

const makePlatform = (x, y, width, height) => { const p = this.add.rectangle(x, y, width, height, 0x00aa00); this.physics.add.existing(p, true); platforms.push(p); return p; }; makePlatform(400, 580, 800, 40); makePlatform(200, 250, 200, 20); makePlatform(600, 350, 200, 20); makePlatform(350, 450, 200, 20);

platforms.forEach(p => this.physics.add.collider(player, p));

// New code cursors = this.input.keyboard.createCursorKeys(); cursors.speedUp = this.input.keyboard.addKey('S'); // speed up cursors.slow = this.input.keyboard.addKey('D'); // slow down cursors.timeTravel = this.input.keyboard.addKey('T'); // time travel

periodText = this.add.text(10, 10, Time Period: ${timePeriods[currentPeriodIndex]}, { font: "20px Arial", fill: "#000" }); speedText = this.add.text(10, 40, Speed: Normal, { font: "20px Arial", fill: "#000" }); }


- `createCursorKeys()` provides input for the left, right, up, down, and custom keys. S, D, and T are added as custom keys for additional inputs.
- `periodText` and `speedText` show the current time period and speed label.

### update()

In the `update` function, we'll add the functionality for movement and jumping, time speed controls, and time travel.

```javascript
function update() {
  let speed = 160;

if (cursors.left.isDown) player.setVelocityX(-speed);
  else if (cursors.right.isDown) player.setVelocityX(speed);
  else player.setVelocityX(0);

if (cursors.up.isDown && player.body.blocked.down) {
    player.setVelocityY(-400);
  }
}

setVelocityX moves the player to the left and right when they press the left and right arrows, at the rate of speed, which is set to 160.

setVelocityY(-400) makes the player jump when they press the up arrow and when the player is standing on something (when player.body.blocked.down is true).

Next, we can add the time speed controls.

function update() {
  let speed = 160;

if (cursors.left.isDown) player.setVelocityX(-speed);
  else if (cursors.right.isDown) player.setVelocityX(speed);
  else player.setVelocityX(0);

if (cursors.up.isDown && player.body.blocked.down) {
    player.setVelocityY(-400);
  }

// New code
  if (cursors.slow.isDown) {
    this.physics.world.timeScale = 2;
    speedText.setText("Speed: Slow");
  } else if (cursors.speedUp.isDown) {
    this.physics.world.timeScale = 0.5;
    speedText.setText("Speed: Fast");
  } else {
    this.physics.world.timeScale = 1;
    speedText.setText("Speed: Normal");
  }
}

This section changes the value of this.physics.world.timeScale based on which key is pressed. The corresponding keys, which were set up in the create() function, change how quickly the physics world simulates speed.

When timeScale = 2 the text shows "Speed: Slow". When timeScale = 0.5 the text shows "Speed: Fast".

Setting Up Time Travel Logic

function update() {
  let speed = 160;

if (cursors.left.isDown) player.setVelocityX(-speed);
  else if (cursors.right.isDown) player.setVelocityX(speed);
  else player.setVelocityX(0);

if (cursors.up.isDown && player.body.blocked.down) {
    player.setVelocityY(-400);
  }

// Additional code for time travel
  if (Phaser.Input.Keyboard.JustDown(cursors.timeTravel)) {
    currentPeriodIndex = (currentPeriodIndex + 1) % timePeriods.length;
    const period = timePeriods[currentPeriodIndex];

if (period === "Past") {
      this.cameras.main.setBackgroundColor("#b8a88a");
      platforms.forEach(p => p.fillColor = 0x847250);
    } else if (period === "Present") {
      this.cameras.main.setBackgroundColor("#87CEFA");
      platforms.forEach(p => p.fillColor = 0x00aa00);
    } else if (period === "Future") {
      this.cameras.main.setBackgroundColor("#4B0082");
      platforms.forEach(p => p.fillColor = 0x39FF14);
    }

periodText.setText(`Time Period: ${period}`);
  }
}

Conclusion

Congrats! You've learned some strategies to incorporate The Changing of Time into your Phaser games!

Now you know: