If you’ve ever wanted to make a small game that runs in the browser, Phaser.js is a great place to start. It’s a simple JavaScript library that helps you build interactive 2-D games that you can play in the browser.

In this guide, you’ll learn what Phaser is and then use it to build the popular Snake game. The snake moves on a grid. It eats food to grow. It dies if it hits a wall or itself. The full project fits in two files, and the code is split into small blocks so it’s easy to follow.

By the end, you’ll be able to understand and copy the code, run it, and tweak it. You’ll also learn why each part exists and how it fits into the Phaser way of doing things.

Play the game here to get a feel for what you’ll be building.

Table of Contents

What is Phaser.js?

Phaser is a free JavaScript library for 2D games. You write plain JavaScript and let Phaser do the heavy lifting. You don’t need a build system or a game engine installer. You can start with a single HTML file and one JavaScript file.

Phaser organizes code into scenes. A scene has three common steps. You load assets in preload, you set up images and variables in create, and you update your game each frame in update. That small loop is the core of most arcade games.

Now let’s setup the project.

Project Setup

Create a folder with two files named index.html and main.js. The HTML page loads Phaser from a CDN and then loads your script.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Phaser Snake</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>body { margin: 0; background: #111; }</style>
  </head>
  <body>
    <div id="game"></div>
    <script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>

This page adds a container div and includes Phaser 3 from jsDelivr. Your main.js file will create the game and attach it to that div.

Grid, Colors, and Game Config

Snake is easiest on a grid. You choose a tile size and a number of tiles wide and high. You also define colors for the background, the snake, and the food. Then you create a Phaser game config that points to your scene functions.

// main.js (part 1)

// Size of one grid tile in pixels
const TILE = 16;

// Number of tiles across (columns) and down (rows)
// Game area = 40 * 16px wide (640px) and 30 * 16px tall (480px)
const COLS = 40;                 
const ROWS = 30;                 

// Total pixel width and height of the game canvas
const WIDTH = COLS * TILE;
const HEIGHT = ROWS * TILE;

// Colors for background, snake head, snake body, and food
const COLORS = {
  bg: 0x1d1d1d,   // dark gray background
  head: 0x30c452, // bright green head
  body: 0x2aa04a, // darker green body
  food: 0xe94f37, // red food
};

// Directions represented as x and y offsets on the grid
// For example, moving left means x decreases by 1, y stays the same
const DIR = {
  left:  { x: -1, y:  0, name: 'left'  },
  right: { x:  1, y:  0, name: 'right' },
  up:    { x:  0, y: -1, name: 'up'    },
  down:  { x:  0, y:  1, name: 'down'  },
};

// Phaser game configuration
// - type: Phaser will use WebGL if possible, otherwise Canvas
// - parent: attach game canvas to <div id="game">
// - width/height: set canvas size
// - backgroundColor: dark background from COLORS
// - scene: defines which functions run during preload, create, and update
const config = {
  type: Phaser.AUTO,
  parent: 'game',
  width: WIDTH,
  height: HEIGHT,
  backgroundColor: COLORS.bg,
  scene: { preload, create, update }
};

// Create a new Phaser game with the config
new Phaser.Game(config);

The config tells Phaser to create a canvas, set its size, and use your scene functions. Phaser.AUTO selects WebGL if possible and falls back to Canvas.

Scene State and Helper Functions

You need to store the snake’s body as grid cells, the rectangles that draw those cells, the direction of travel, the queued input, the food cell, the score, and the movement timer. A few helper functions keep the math clean.

// main.js (part 2)

// Snake state
let snake;           // Array of grid cells [{x, y}, ...]; index 0 = head
let snakeRects;      // Array of Phaser rectangles drawn at snake cell positions
let direction;       // Current direction of snake movement (object from DIR)
let nextDirection;   // Next direction chosen by player input (applied on step)
let food;            // Current food cell {x, y}
let score = 0;       // Current score count
let scoreText;       // Phaser text object that displays the score
let moveEvent;       // Phaser timer event to move snake at fixed intervals
let speedMs = 130;   // Delay in milliseconds between moves (lower = faster)

// Input state
let cursors;         // Phaser helper object for arrow keys
let spaceKey;        // Phaser Key object for Space bar (restart the game)

/**
 * Convert a grid cell (x,y) to its pixel center (px,py) on the canvas.
 * Example: (0,0) -> (8,8) if TILE=16. Ensures rectangles are centered.
 */
function gridToPixelCenter(x, y) {
  return { px: x * TILE + TILE / 2, py: y * TILE + TILE / 2 };
}

/**
 * Pick a random grid cell that is not occupied by any cell in excludeCells.
 * - Creates a Set of occupied cells as "x,y" strings for fast lookup.
 * - Keeps generating random cells until it finds a free one.
 * Used to place food so it never spawns on the snake.
 */
function randomFreeCell(excludeCells) {
  const occupied = new Set(excludeCells.map(c => `${c.x},${c.y}`));
  while (true) {
    const x = Math.floor(Math.random() * COLS);
    const y = Math.floor(Math.random() * ROWS);
    if (!occupied.has(`${x},${y}`)) return { x, y };
  }
}

/**
 * Check if direction 'a' is exactly the opposite of direction 'b'.
 * Example: left vs right, or up vs down.
 * This prevents the snake from instantly turning 180° into itself.
 */
function isOpposite(a, b) {
  return a.x === -b.x && a.y === -b.y;
}

gridToPixelCenter converts a grid cell to the center point in pixels so rectangles line up. randomFreeCell finds a cell not used by the snake. isOpposite helps block instant reversals that would cause a crash.

Preload and Create

This game uses simple vector rectangles, so there are no images to load. You still define the scene functions since Phaser calls them by name from your config.

// main.js (part 3)

/**
 * preload()
 * Runs once before the game starts.
 * Used for loading images, sounds, and other assets.
 * In this version, we use simple colored rectangles (no assets needed).
 */
function preload() {
  // No assets to load in this version.
}

/**
 * create()
 * Runs once after preload. Sets up the game scene.
 * - Prepares keyboard input
 * - Calls initGame() to build the snake, food, score UI, and start movement
 */
function create() {
  // Phaser helper that gives us arrow key input (up, down, left, right)
  cursors = this.input.keyboard.createCursorKeys();

  // Register the Space bar key to restart the game later
  spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

  // Initialize the game state (snake, food, score, timer)
  // Using call(this) so that initGame runs in the context of the scene
  initGame.call(this);
}

The create step sets up keyboard input. It then calls initGame to build the first snake, place food, and start the timer. The call(this) ensures the helper function can use the scene’s this.

Initialize the Game

On a fresh start or a restart, you need to clear old timers and shapes, reset the score, build a short snake in the middle, draw it, place food, and start the movement loop.

// main.js (part 4)

/**
 * initGame()
 * Called when the game first starts or after pressing Space to restart.
 * - Clears old state (snake, food, timers)
 * - Resets score
 * - Creates a new snake in the center of the grid
 * - Spawns the first food
 * - Sets up score text
 * - Starts the timed loop that moves the snake
 */
function initGame() {
  // If an old movement timer exists, stop it (avoid multiple timers running)
  if (moveEvent) moveEvent.remove(false);

  // If old snake rectangles exist, destroy them to clean the screen
  if (snakeRects) snakeRects.forEach(r => r.destroy());

  // Reset the score and snake direction
  score = 0;
  direction = DIR.right;     // Snake starts moving to the right
  nextDirection = DIR.right; // Player input queue also points right

  // Find the starting position near the center of the grid
  const startX = Math.floor(COLS / 2);
  const startY = Math.floor(ROWS / 2);

  // Snake starts with 3 segments, head + 2 body pieces
  snake = [
    { x: startX,     y: startY },     // head (middle)
    { x: startX - 1, y: startY },     // body segment left of head
    { x: startX - 2, y: startY },     // tail further left
  ];

  // Create rectangle objects in Phaser to visually draw the snake
  snakeRects = snake.map((cell, i) => {
    const { px, py } = gridToPixelCenter(cell.x, cell.y); // convert grid to pixel center
    const color = i === 0 ? COLORS.head : COLORS.body;    // head is a brighter green
    const rect = this.add.rectangle(px, py, TILE - 2, TILE - 2, color); // slightly smaller for spacing
    rect.setOrigin(0.5, 0.5);                             // center anchor point
    return rect;
  });

  // Spawn food at a random free cell (not overlapping snake)
  food = randomFreeCell(snake);
  const { px, py } = gridToPixelCenter(food.x, food.y);

  // If food already exists from a previous run, remove it first
  if (this.foodRect) this.foodRect.destroy();

  // Draw the new food as a red rectangle
  this.foodRect = this.add.rectangle(px, py, TILE - 2, TILE - 2, COLORS.food);

  // If score text does not exist yet, create it
  // Otherwise (on restart), just reset its value
  if (!scoreText) {
    scoreText = this.add.text(8, 6, 'Score: 0', { fontFamily: 'monospace', fontSize: 18, color: '#fff' });
    this.add.text(8, 28, 'Arrows to move. Space to restart.', { fontFamily: 'monospace', fontSize: 14, color: '#aaa' });
  } else {
    scoreText.setText('Score: 0');
  }

  // Reset speed and create a repeating timer
  // Every "speedMs" milliseconds, stepSnake() will run to move the snake
  speedMs = 130;
  moveEvent = this.time.addEvent({
    delay: speedMs,
    loop: true,
    callback: () => stepSnake.call(this) // .call(this) keeps Phaser scene context
  });

  // If a "Game Over" message exists from the last run, remove it
  if (this.gameOverText) {
    this.gameOverText.destroy();
    this.gameOverText = null;
  }
}

The rectangles are one or two pixels smaller than the tile so you get a neat gap between cells. The time event calls stepSnake on a fixed rhythm. This rhythm is separate from the frame rate, which keeps movement stable across machines.

Reading Input Each Frame

You will read the arrow keys in update. You don’t move the snake here. You only set the next direction. Movement happens on the timer so the game has a steady pace.

// main.js (part 5)

/**
 * update()
 * This runs every frame (Phaser’s game loop).
 * - Reads player input from arrow keys
 * - Updates "nextDirection" so the snake will turn on the next step
 * - Listens for Space bar press to restart the game if it’s over
 */
function update() {
  // Check if LEFT arrow is pressed AND it’s not the opposite of current direction
  if (cursors.left.isDown && !isOpposite(DIR.left, direction)) {
    nextDirection = DIR.left;

  // Check if RIGHT arrow is pressed
  } else if (cursors.right.isDown && !isOpposite(DIR.right, direction)) {
    nextDirection = DIR.right;

  // Check if UP arrow is pressed
  } else if (cursors.up.isDown && !isOpposite(DIR.up, direction)) {
    nextDirection = DIR.up;

  // Check if DOWN arrow is pressed
  } else if (cursors.down.isDown && !isOpposite(DIR.down, direction)) {
    nextDirection = DIR.down;
  }

  // If the game is over (a "Game Over" text exists)
  // AND the Space bar was just pressed → restart the game
  if (this.gameOverText && Phaser.Input.Keyboard.JustDown(spaceKey)) {
    initGame.call(this); // Reset everything (snake, food, score, timer)
  }
}

This pattern prevents the snake from skipping cells if the frame rate spikes. It also makes the game feel fair. Your key presses get picked up, but the body moves on the beat set by the timer.

Stepping the Snake, Eating, and Drawing

This function is the heart of the game. It picks up the queued input, computes the next head cell, checks collisions, moves or grows the snake, updates the score, and refreshes colors.

// main.js (part 6)

/**
 * stepSnake()
 * This function runs every "tick" (based on the timer).
 * - Moves the snake forward by one cell
 * - Checks for collisions (wall or self)
 * - Handles eating food (grow + score)
 * - Updates the snake's rectangles on the screen
 */
function stepSnake() {
  // Apply the direction chosen in update() (queued by player input)
  direction = nextDirection;

  // Get the current head of the snake
  const head = snake[0];

  // Create a new head position by moving one cell in the current direction
  const newHead = { x: head.x + direction.x, y: head.y + direction.y };

  // === Collision Check #1: Wall ===
  // If the new head is outside the grid, the game ends
  if (newHead.x < 0 || newHead.x >= COLS || newHead.y < 0 || newHead.y >= ROWS) {
    return endGame.call(this);
  }

  // === Collision Check #2: Self ===
  // If the new head overlaps any snake cell, the game ends
  for (let i = 0; i < snake.length; i++) {
    if (snake[i].x === newHead.x && snake[i].y === newHead.y) {
      return endGame.call(this);
    }
  }

  // === Check if food was eaten ===
  const ate = newHead.x === food.x && newHead.y === food.y;

  // Add the new head cell to the front of the snake array
  snake.unshift(newHead);

  if (!ate) {
    // Case: Snake did NOT eat food → keep length the same
    // Remove last cell from snake array (tail)
    snake.pop();

    // Reuse the last rectangle object for performance
    const tailRect = snakeRects.pop();
    const { px, py } = gridToPixelCenter(newHead.x, newHead.y);
    tailRect.setPosition(px, py);       // Move it to the new head position
    snakeRects.unshift(tailRect);       // Put it at the front of the rectangle list
  } else {
    // Case: Snake DID eat food → grow longer
    // Create a new rectangle for the new head (since length increases)
    const { px, py } = gridToPixelCenter(newHead.x, newHead.y);
    const headRect = this.add.rectangle(px, py, TILE - 2, TILE - 2, COLORS.head);
    snakeRects.unshift(headRect);

    // Increase score and update text
    score += 10;
    scoreText.setText(`Score: ${score}`);

    // Place new food somewhere else
    placeFood.call(this);

    // Speed up slightly as difficulty curve
    maybeSpeedUp.call(this);
  }

  // === Update Colors ===
  // Ensure only index 0 is drawn as the "head" (bright green),
  // and the next segment becomes part of the "body"
  if (snakeRects[1]) snakeRects[1].setFillStyle(COLORS.body);
  snakeRects[0].setFillStyle(COLORS.head);
}

The movement rule is simple. Add a head in the current direction. If you did not eat, remove the tail. If you ate, keep the tail to grow by one. To draw this, you reuse the last rectangle when you only move. You move it to the head’s new pixel location and put it at the front of the list. When you grow, you create a new rectangle for the new head.

Spawning Food and Increasing Speed

After eating, you need to place food in a new free cell. You can also nudge the speed so the game gets harder over time.

// main.js (part 7)

/**
 * placeFood()
 * Spawns food at a new random cell that is NOT part of the snake.
 * - Uses randomFreeCell() to avoid collisions with the snake
 * - Moves the existing red rectangle (foodRect) to the new spot
 */
function placeFood() {
  // Pick a random free cell on the grid (not occupied by the snake)
  food = randomFreeCell(snake);

  // Convert that cell into pixel coordinates
  const { px, py } = gridToPixelCenter(food.x, food.y);

  // Move the existing food rectangle to the new position
  this.foodRect.setPosition(px, py);
}

/**
 * maybeSpeedUp()
 * Makes the game a little harder each time food is eaten.
 * - Decreases the move delay (snake moves faster)
 * - Restarts the timer with the new speed
 * - Stops speeding up once a lower bound (70ms) is reached
 */
function maybeSpeedUp() {
  // Only speed up if current speed is above the minimum threshold
  if (speedMs > 70) {
    // Make the snake faster by reducing the delay
    speedMs -= 3;

    // Remove the old movement timer
    moveEvent.remove(false);

    // Create a new timer with the updated speed
    moveEvent = this.time.addEvent({
      delay: speedMs,            // shorter delay = faster movement
      loop: true,                // repeat forever until game over
      callback: () => stepSnake.call(this) // keep "this" as the scene
    });
  }
}

There is a lower bound for speed so the game does not become unreadable. You can tune these numbers to match the feel you want.

Game Over and Restart

When the snake hits a wall or itself, the run ends. You stop the timer and show a message. Pressing space restarts the game.

// main.js (part 8)

/**
 * endGame()
 * Called when the snake hits a wall or itself.
 * - Stops the movement timer (snake no longer moves)
 * - Displays a "Game Over" message with the final score
 * - Waits for the player to press Space (handled in update()) to restart
 */
function endGame() {
  // Stop the movement timer so the snake no longer steps forward
  moveEvent.remove(false);

  // Define text style for the "Game Over" message
  const style = {
    fontFamily: 'monospace',
    fontSize: 28,
    color: '#fff',
    align: 'center'
  };

  // Message shows "Game Over", final score, and restart instructions
  const msg = `Game Over\nScore: ${score}\nPress Space to Restart`;

  // Add text to the center of the screen
  // .setOrigin(0.5, 0.5) makes the text anchor at its center
  this.gameOverText = this.add.text(WIDTH / 2, HEIGHT / 2, msg, style).setOrigin(0.5, 0.5);
}

The update function you wrote earlier listens for space using JustDown, so the restart happens only once per key press.

How the Whole Thing Fits Together

You now have the full loop of a Phaser scene. The preload is empty in this version because rectangles do not need assets. The create step connects keyboard input and calls initGame. The timer created in initGame keeps time for movement.

The update step reads keys every frame and sets a future direction. The stepSnake function runs on the timer. It moves the head, checks for a crash, handles growth, reuses shapes for performance, and updates the score. When the run ends, endGame stops the timer and shows a clear message. A single key press calls initGame to start fresh.

This style maps well to many other small games. If you can express your game state as data and move it on a schedule, you can draw it with simple shapes or sprites. Phaser’s time events give you a clean heartbeat. Its input system gives you easy key handling. Its drawing API makes it quick to show rectangles, text, or images.

Useful Next Steps

There are many small features you can add without changing the core. You can store a high score in localStorage. You can add simple sounds when you eat or when the game ends by loading audio in preload and calling this.sound.play in the right places.

You can make the world wrap at the edges by replacing the wall check with modulo math so the snake appears on the opposite side. You can theme the game by swapping rectangle colors or replacing rectangles with images.

Each of these additions builds on the same base. Keep the grid logic simple. Keep the state in clear arrays and objects. Move on a fixed timer. Draw based on the state. That’s the pattern.

Final Thoughts

You started with a blank page and ended with a working browser game. You set up Phaser, built a scene, and used a timer to drive movement. You learned how to handle input in a safe way, how to grow the snake, how to detect collisions, and how to draw with rectangles. You kept the code tidy with small helper functions and clear names.

From here, you can branch out to other grid games like Tetris or Minesweeper, or you can try a different style like Pong or Breakout. The structure will be similar. A scene to set things up, a timer or physics step to move things along, and a few rules that define the fun. That’s the beauty of Phaser for beginners.

If you’re into online gaming, GameBoost is the place to be. Discover GTA Modded Accounts packed with exclusive upgrades. You’ll also find accounts for other fan-favorite titles like Fortnite, Grow a Garden, Clash of Clans, and more – all in one trusted marketplace.