2D Game is the Phaser-based game template on Gipity. All files are fully editable - no locked template layer. Uses Phaser 3.80.1 via CDN with arcade physics.

When to use this: When the user asks for a 2D game - platformer, side-scroller, arcade, puzzle, endless runner, top-down, shooter, or RPG. For simple games (wordle, quiz, card games), use web-simple. For 3D multiplayer apps, use 3d-engine for a blank template or 3d-world for a playable rocket-launcher starter.

Quick Start - Start Here

STRONGLY RECOMMENDED: Begin every 2D game by adding the 2d-game template with add. It sets up Phaser 3, boot/game scenes, config, settings, and favicons. Only hand-roll files if the user explicitly tells you to skip the template.

add name=2d-game title="<Game Name>"

Starting over in an existing project: If src/ already exists and the user wants a clean rebuild, call file_delete on src first, then run add normally. Or pass force=true to add to overwrite in one step - destructive, so confirm with the user first. Unrelated content (media, data, notes) is preserved either way.

Naming: Use the user's name verbatim if given. If they didn't specify, blend "Gip" or "Gipity" into the name (e.g. "Gipity Racer", "Gip Tac Toe") - be creative but don't force it.

This creates a playable game immediately - colored rectangle player, arcade physics, ground platform, and controls that work on desktop AND mobile out of the box (WASD/arrows + Space on keyboard; a floating virtual joystick, Jump button, and fullscreen toggle on touch devices). Then edit scenes/game.js and settings.js to build your game.

Project Structure

src/
  index.html            - Phaser CDN, game container, module entry
  js/
    config.js           - Phaser.Game config, scene registration
    controls.js         - Touch controls overlay (virtual joystick, buttons, fullscreen)
    settings.js         - Tunable values (canvas, colors, physics, player, gameplay)
    strings.js          - User-facing display text
    scenes/
      boot.js           - Preloader with progress bar
      game.js           - Main game scene (gameplay logic)
  css/
    styles.css          - Page layout, canvas styling
  images/
    favicon-192.png
    favicon.ico

Phaser Essentials

Scene Lifecycle

Every scene has three key methods:

Loading Assets

In boot.js preload():

this.load.image('player', './images/player.png');
this.load.spritesheet('hero', './images/hero.png', { frameWidth: 32, frameHeight: 48 });
this.load.audio('jump', './audio/jump.mp3');

Creating Game Objects

// Sprite (from loaded image)
this.player = this.physics.add.sprite(400, 300, 'player');

// Rectangle (no image needed)
this.player = this.add.rectangle(400, 300, 32, 48, 0xf26522);
this.physics.add.existing(this.player);

// Static group (platforms)
this.platforms = this.physics.add.staticGroup();
this.platforms.create(400, 568, 'ground');

Physics (Arcade)

// Gravity is set in config.js via settings.physics.gravity
body.setVelocityX(speed);
body.setVelocityY(jumpForce);  // negative = up
body.setBounce(0.2);
body.setCollideWorldBounds(true);

// Collisions
this.physics.add.collider(player, platforms);
this.physics.add.overlap(player, coins, collectCoin, null, this);

Input

// Keyboard
this.cursors = this.input.keyboard.createCursorKeys();  // up, down, left, right
if (this.cursors.left.isDown) body.setVelocityX(-speed);

// Specific keys
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

// Touch/pointer
this.input.on('pointerdown', (pointer) => { ... });

Text

this.add.text(400, 30, 'Score: 0', {
  fontSize: '24px', color: '#ffffff', fontStyle: 'bold',
}).setOrigin(0.5);

Camera

this.cameras.main.startFollow(this.player, true, 0.1, 0.1);
this.cameras.main.setBounds(0, 0, worldWidth, worldHeight);

Common Phaser 3 Pitfalls

These are the failure modes that most often turn the whole screen black. Read before writing a game scene.

Build Incrementally

For anything non-trivial, don't write the whole game in one Write call. Work in small, verified steps:

  1. Add the template (gipity add 2d-game) and deploy - confirm the starter game renders.
  2. Customize ONE element (e.g. replace the player rectangle with your sprite). Deploy, screenshot, confirm it renders.
  3. Add the next element (ground, enemies, collectibles) one at a time, deploying and verifying between each.
  4. Only after the core loop works, layer on polish (parallax, particles, HUD, touch controls).

A 500-line rewrite of scenes/game.js is very hard to debug when something breaks - a single bad API call turns the whole screen black with no useful error. Small steps keep the failure surface tiny.

Verification After Deploy

After every gipity deploy dev:

Asserting on real game state (score, lives, collisions, win/lose)

A screenshot proves the game renders; it can't prove the ball bounces, the score increments, or the win screen ever fires. Drive the live game object instead. js/config.js exports the Phaser game, and index.html loads it as a module - so a dynamic import() from gipity page eval resolves out of the browser's module cache and hands you the running instance (relative specifiers resolve against the page URL). No second game boots.

Never ship a window.game debug hook to do this. That leaves instrumentation in your production bundle and costs an extra deploy.

gipity page eval "<deploy-url>" "
  const { game } = await import('./js/config.js');
  const s = game.scene.getScene('Game');
  s.startGame();                               // call your own scene methods
  return JSON.stringify({ score: s.score, lives: s.lives, state: s.state });
"

For a longer driver (play a full round, force a win, assert the game-over overlay), put the script in a file and pass --file - no shell quoting, and the body may use await and return:

gipity page eval "<deploy-url>" --file ./tests/drive-game.js --json

The eval body has a ~20s in-page budget, so split a long sequence into one call per state you're verifying.

Never wait wall-clock time for physics/animation to play out - the headless browser can paint at ~15 fps, so setTimeout(2000) advances far less than 2s of game time and assertions report false negatives. config.js also exports advance(seconds): it pauses the browser loop and ticks Phaser's TimeStep by hand, so N simulated seconds (physics, timers, collisions) run in milliseconds, deterministically:

gipity page eval "<deploy-url>" "
  const { game, advance } = await import('./js/config.js');
  const s = game.scene.getScene('Game');
  s.startGame();
  advance(5);                                  // 5s of game time, instantly
  return JSON.stringify({ score: s.score, bricks: s.bricks.countActive() });
"

To capture a driven state visually, page screenshot --action "<js>" runs the same kind of script before the shot - same async body, same app-relative import(). If the action throws, you still get the image, plus a ⚠ --action failed: line telling you it shows the undriven page:

gipity page screenshot "<deploy-url>" -o win.png \
  --action "const { game } = await import('./js/config.js'); game.scene.getScene('Game').winGame();"

Write throwaway driver scripts and screenshots outside the project directory (e.g. /tmp) - the project auto-syncs to Gipity, so scratch files land in the user's storage. If they must live in the project, add the path to .gipityignore.

Animations

this.anims.create({
  key: 'walk',
  frames: this.anims.generateFrameNumbers('hero', { start: 0, end: 3 }),
  frameRate: 10,
  repeat: -1,
});
this.player.anims.play('walk', true);

Adding a New Scene

  1. Create src/js/scenes/myScene.js:
export class MyScene extends Phaser.Scene {
  constructor() { super('MyScene'); }
  create() { /* ... */ }
  update() { /* ... */ }
}
  1. Register in config.js:
import { MyScene } from './scenes/myScene.js';
// Add to scene array: scene: [Boot, Game, MyScene],
  1. Switch scenes: this.scene.start('MyScene');

Genre Recipes

Side-Scroller / Platformer

Top-Down (Zelda-style)

Endless Runner

Arcade / Shooter

Puzzle

Mobile / Touch

The template is mobile-ready by default via js/controls.js - a DOM overlay above the canvas that renders only on touch devices (nothing shows on desktop). It gives the standard mobile-game layout: a floating virtual joystick on the left half (the pad appears wherever the thumb lands), round action buttons bottom-right, and a fullscreen toggle top-right (auto-hidden where the Fullscreen API is unavailable, e.g. iPhone Safari - there, "Add to Home Screen" runs fullscreen via the template's PWA meta tags). Multi-touch is tracked per pointer, so joystick + buttons work simultaneously.

import { touch, initTouchControls } from '../controls.js';

// create(): declare the buttons your game needs (first = primary, biggest)
initTouchControls({ buttons: [{ id: 'jump', label: 'Jump' }, { id: 'fire', label: 'Fire' }] });
// tap-only game? disable the joystick so it doesn't swallow lower-left taps:
// initTouchControls({ joystick: false, buttons: [...] });

// update(): merge with keyboard - joystick is analog (-1..1)
const keyX = (cursors.right.isDown ? 1 : 0) - (cursors.left.isDown ? 1 : 0);
const moveX = keyX !== 0 ? keyX : touch.x;            // touch.y for vertical
body.setVelocityX(moveX * speed);
if (touch.isDown('jump')) { /* held */ }
if (touch.justPressed('fire')) { /* once per press */ }

Always keep desktop AND touch input working: keyboard = WASD + arrows (both), plus the matching touch buttons. touch.enabled tells you touch controls are active (e.g. to swap instruction text). Phaser still handles in-canvas taps via pointer events (this.input.on('pointerdown', ...)), with 3 active pointers configured.

For mobile-responsive canvas, the template uses Phaser.Scale.FIT + CENTER_BOTH by default - the game keeps its settings.canvas coordinate system and letterboxes to fit any screen or orientation. The page is hardened for games (no pinch-zoom, no overscroll, no text selection; dvh viewport).

Deploy Verification

Verify a deploy when it matters - the first deploy, structural changes (new pages, new frameworks, changed imports), or anything that might have broken. Skip it for trivial changes (copy tweaks, style values).

gipity deploy dev --inspect deploys and reports the live page in one step: console errors, failed resources, timing, layout overflow. A clean console is necessary but NOT sufficient for Canvas/WebGL - also capture gipity page screenshot <url> and look at it, because render failures are silent. A blank page, black canvas, or wrong-looking UI with a clean console is a real failure, not a pass.

Full loop - reading function logs, calling a function directly, driving the page: the app-debugging skill.