
Game Engine
- 11.7k installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
Web-based game engine and game development using HTML5, Canvas, WebGL, JavaScript, and frameworks like Phaser, Three.js, Babylon.js, A-Frame.
About
Expert skill for constructing web-based game engines and games using HTML5 Canvas, WebGL, and JavaScript. Covers game loops, physics simulation, collision detection, sprite management, input handling (keyboard, mouse, touch, gamepad), and rendering techniques for 2D and 3D experiences. Includes starter templates for platformers, breakout games, and maze games; reference documentation on core concepts like rendering, physics, and controls; and workflows for audio integration, multiplayer via WebRTC, performance optimization, and publishing across browsers and platforms.
- Game loop architecture: input processing, state updates, and rendering using requestAnimationFrame
- Collision detection: AABB, circle, SAT-based (2D) and bounding box, raycasting (3D) implementations
- Multi-input support: keyboard, mouse, touch events, gamepad API, and device orientation controls
- Framework integration: Phaser, Three.js, Babylon.js, A-Frame with 2D Canvas and WebGL rendering
- Pre-built templates for paddle games, platformers, and maze games with complete working examples
Game Engine by the numbers
- 11,719 all-time installs (skills.sh)
- +166 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #2 of 247 Game Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
game-engine capabilities & compatibility
- Capabilities
- 2d game rendering via canvas · 3d game rendering via webgl · physics and gravity simulation · collision detection (aabb, circle, sat, raycasti · multi input handling (keyboard, mouse, touch, ga · audio playback and web audio api · sprite and tilemap management · game loop implementation
- Works with
- github · vercel
- Use cases
- ui design · frontend
- Platforms
- macOS · Windows · Linux · WSL
- Runs
- Remote server
- Pricing
- Free
What game-engine says it does
Covers techniques for platformers, breakout-style games, maze games, tilemaps, audio, multiplayer via WebRTC, and publishing games.
npx skills add https://github.com/github/awesome-copilot --skill game-engineAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11.7k |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
What it does
Build 2D and 3D web-based games and game engines using HTML5 Canvas, WebGL, and JavaScript frameworks.
Who is it for?
Game developers building web-based 2D platformers, breakout games, maze games, 3D experiences, and interactive browser games.
Skip if: Native mobile game development, AAA game engines (Unreal, Unity native), or non-interactive web applications.
When should I use this skill?
Asked to create games, build game engines, implement game physics, handle collision detection, set up game loops, manage sprites, add game controls, or work with 2D/3D rendering.
What you get
A working, optimized web game or game engine with proper rendering, physics, controls, audio, and cross-browser/device compatibility.
- Working game or game engine
- Optimized assets and code
- Cross-browser testing completed
By the numbers
- 5 starter templates provided: paddle-game, 2d-maze, 2d-platform, gameBase-repo, simple-2d-engine
- 8 reference documentation files covering basics, web APIs, techniques, 3D, controls, publishing, algorithms, and termino
Files
Game Engine Skill
Build web-based games and game engines using HTML5 Canvas, WebGL, and JavaScript. This skill includes starter templates, reference documentation, and step-by-step workflows for 2D and 3D game development with frameworks such as Phaser, Three.js, Babylon.js, and A-Frame.
When to Use This Skill
- Building a game engine or game from scratch using web technologies
- Implementing game loops, physics, collision detection, or rendering
- Working with HTML5 Canvas, WebGL, or SVG for game graphics
- Adding game controls (keyboard, mouse, touch, gamepad)
- Creating 2D platformers, breakout-style games, maze games, or 3D experiences
- Working with tilemaps, sprites, or animations
- Adding audio to web games
- Implementing multiplayer features with WebRTC or WebSockets
- Optimizing game performance
- Publishing and distributing web games
Prerequisites
- Basic knowledge of HTML, CSS, and JavaScript
- A modern web browser with Canvas/WebGL support
- A text editor or IDE
- Optional: Node.js for build tooling and local development servers
Core Concepts
The following concepts form the foundation of every web-based game engine.
Game Loop
Every game engine revolves around the game loop -- a continuous cycle of:
1. Process Input - Read keyboard, mouse, touch, or gamepad input 2. Update State - Update game object positions, physics, AI, and logic 3. Render - Draw the current game state to the screen
Use requestAnimationFrame for smooth, browser-optimized rendering.
Rendering
- Canvas 2D - Best for 2D games, sprite-based rendering, and tilemaps
- WebGL - Hardware-accelerated 3D and advanced 2D rendering
- SVG - Vector-based graphics, good for UI elements
- CSS - Useful for DOM-based game elements and transitions
Physics and Collision Detection
- 2D Collision Detection - AABB, circle, and SAT-based collision
- 3D Collision Detection - Bounding box, bounding sphere, and raycasting
- Velocity and Acceleration - Basic Newtonian physics for movement
- Gravity - Constant downward acceleration for platformers
Controls
- Keyboard - Arrow keys, WASD, and custom key bindings
- Mouse - Click, move, and pointer lock for FPS-style controls
- Touch - Mobile touch events and virtual joysticks
- Gamepad - Gamepad API for controller support
Audio
- Web Audio API - Programmatic sound generation and spatial audio
- HTML5 Audio - Simple audio playback for music and sound effects
Step-by-Step Workflows
Creating a Basic 2D Game
1. Set up an HTML file with a <canvas> element 2. Get the 2D rendering context 3. Implement the game loop using requestAnimationFrame 4. Create game objects with position, velocity, and size properties 5. Handle keyboard/mouse input for player control 6. Implement collision detection between game objects 7. Add scoring, lives, and win/lose conditions 8. Add sound effects and music
Building a 3D Game
1. Choose a framework (Three.js, Babylon.js, A-Frame, or PlayCanvas) 2. Set up the scene, camera, and renderer 3. Load or create 3D models and textures 4. Implement lighting and shaders 5. Add physics and collision detection 6. Implement player controls and camera movement 7. Add audio and visual effects
Publishing a Game
1. Optimize assets (compress images, minify code) 2. Test across browsers and devices 3. Choose distribution platform (web, app stores, game portals) 4. Implement monetization if needed 5. Promote through game communities and social media
Game Templates
Starter templates are available in the assets/ folder. Each template provides a complete, working example that can be used as a starting point for a new project.
| Template | Description |
|---|---|
paddle-game-template.md | 2D Breakout-style game with pure JavaScript |
2d-maze-game.md | Maze game with device orientation controls |
2d-platform-game.md | Platformer game using Phaser framework |
gameBase-template-repo.md | Game base template repository structure |
simple-2d-engine.md | Simple 2D platformer engine with collisions |
Reference Documentation
Detailed reference material is available in the references/ folder. Consult these files for in-depth coverage of specific topics.
| Reference | Topics Covered |
|---|---|
basics.md | Game development introduction and anatomy |
web-apis.md | Canvas, WebGL, Web Audio, Gamepad, and other web APIs |
techniques.md | Collision detection, tilemaps, async scripts, audio |
3d-web-games.md | 3D theory, frameworks, shaders, WebXR |
game-control-mechanisms.md | Touch, keyboard, mouse, and gamepad controls |
game-publishing.md | Distribution, promotion, and monetization |
algorithms.md | Raycasting, collision, physics, vector math |
terminology.md | Game development glossary |
game-engine-core-principles.md | Core design principles for game engines |
Troubleshooting
| Issue | Solution |
|---|---|
| Canvas is blank | Check that you are calling drawing methods after getting the context and inside the game loop |
| Game runs at different speeds | Use delta time in update calculations instead of fixed values |
| Collision detection is inconsistent | Use continuous collision detection or reduce time steps for fast-moving objects |
| Audio does not play | Browsers require user interaction before playing audio; trigger playback from a click handler |
| Performance is poor | Profile with browser dev tools, reduce draw calls, use object pooling, and optimize asset sizes |
| Touch controls are unresponsive | Prevent default touch behavior and handle touch events separately from mouse events |
| WebGL context lost | Handle the webglcontextlost event and restore state on webglcontextrestored |
2D Maze Game Template
A mobile-optimized 2D maze game where players guide a ball through a labyrinth of obstacles to reach a target hole. The game uses the Device Orientation API for tilt-based motion controls on mobile devices and keyboard arrow keys on desktop. Built with the Phaser framework (v2.x with Arcade Physics), it features multi-level progression, collision detection, audio feedback, vibration haptics, and a timer system.
Source reference: MDN - HTML5 Gamedev Phaser Device Orientation Live demo: Cyber Orb Source code: GitHub - EnclaveGames/Cyber-Orb
---
Game Concept
The player controls a ball (the "orb") by tilting their mobile device or pressing arrow keys. The ball rolls through a maze of horizontal and vertical wall segments. The objective on each level is to navigate the ball to a hole at the top of the screen while avoiding walls. Collisions with walls trigger a bounce, a sound effect, and optional vibration. A timer tracks how long the player takes per level and across the entire game.
---
Project Structure
project/
index.html
src/
phaser-arcade-physics.2.2.2.min.js
Boot.js
Preloader.js
MainMenu.js
Howto.js
Game.js
img/
ball.png
hole.png
element-horizontal.png
element-vertical.png
button-start.png
loading-bg.png
loading-bar.png
audio/
bounce.ogg
bounce.mp3
bounce.m4a---
Phaser Setup and Initialization
HTML Entry Point
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Cyber Orb</title>
<style>
body { margin: 0; background: #333; }
</style>
<script src="src/phaser-arcade-physics.2.2.2.min.js"></script>
<script src="src/Boot.js"></script>
<script src="src/Preloader.js"></script>
<script src="src/MainMenu.js"></script>
<script src="src/Howto.js"></script>
<script src="src/Game.js"></script>
</head>
<body>
<script>
(() => {
const game = new Phaser.Game(320, 480, Phaser.CANVAS, "game");
game.state.add("Boot", Ball.Boot);
game.state.add("Preloader", Ball.Preloader);
game.state.add("MainMenu", Ball.MainMenu);
game.state.add("Howto", Ball.Howto);
game.state.add("Game", Ball.Game);
game.state.start("Boot");
})();
</script>
</body>
</html>- Canvas size:
320 x 480 - Renderer:
Phaser.CANVAS(alternatives:Phaser.WEBGL,Phaser.AUTO)
---
Game State Architecture
The game follows a linear state flow:
Boot --> Preloader --> MainMenu --> Howto --> GameBoot State
Loads minimal assets for the loading screen and configures scaling.
const Ball = {
_WIDTH: 320,
_HEIGHT: 480,
};
Ball.Boot = function (game) {};
Ball.Boot.prototype = {
preload() {
this.load.image("preloaderBg", "img/loading-bg.png");
this.load.image("preloaderBar", "img/loading-bar.png");
},
create() {
this.game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
this.game.scale.pageAlignHorizontally = true;
this.game.scale.pageAlignVertically = true;
this.game.state.start("Preloader");
},
};Preloader State
Displays a visual loading bar while loading all game assets. Audio is loaded in multiple formats for cross-browser compatibility.
Ball.Preloader = function (game) {};
Ball.Preloader.prototype = {
preload() {
this.preloadBg = this.add.sprite(
(Ball._WIDTH - 297) * 0.5,
(Ball._HEIGHT - 145) * 0.5,
"preloaderBg"
);
this.preloadBar = this.add.sprite(
(Ball._WIDTH - 158) * 0.5,
(Ball._HEIGHT - 50) * 0.5,
"preloaderBar"
);
this.load.setPreloadSprite(this.preloadBar);
this.load.image("ball", "img/ball.png");
this.load.image("hole", "img/hole.png");
this.load.image("element-w", "img/element-horizontal.png");
this.load.image("element-h", "img/element-vertical.png");
this.load.spritesheet("button-start", "img/button-start.png", 146, 51);
this.load.audio("audio-bounce", [
"audio/bounce.ogg",
"audio/bounce.mp3",
"audio/bounce.m4a",
]);
},
create() {
this.game.state.start("MainMenu");
},
};MainMenu State
Displays the title screen with a start button.
Ball.MainMenu = function (game) {};
Ball.MainMenu.prototype = {
create() {
this.add.sprite(0, 0, "screen-mainmenu");
this.gameTitle = this.add.sprite(Ball._WIDTH * 0.5, 40, "title");
this.gameTitle.anchor.set(0.5, 0);
this.startButton = this.add.button(
Ball._WIDTH * 0.5, 200, "button-start",
this.startGame, this,
2, 0, 1 // hover, out, down frames
);
this.startButton.anchor.set(0.5, 0);
this.startButton.input.useHandCursor = true;
},
startGame() {
this.game.state.start("Howto");
},
};Howto State
A single-click instruction screen before gameplay begins.
Ball.Howto = function (game) {};
Ball.Howto.prototype = {
create() {
this.buttonContinue = this.add.button(
0, 0, "screen-howtoplay",
this.startGame, this
);
},
startGame() {
this.game.state.start("Game");
},
};---
Device Orientation API Usage
The Device Orientation API provides real-time data about the physical tilt of a device. Two axes are used:
| Property | Axis | Range | Effect |
|---|---|---|---|
event.gamma | Left/right tilt | -90 to 90 degrees | Horizontal ball velocity |
event.beta | Front/back tilt | -180 to 180 degrees | Vertical ball velocity |
Registering the Listener
// In the Game state's create() method
window.addEventListener("deviceorientation", this.handleOrientation);Handling Orientation Events
handleOrientation(e) {
const x = e.gamma; // left-right tilt
const y = e.beta; // front-back tilt
Ball._player.body.velocity.x += x;
Ball._player.body.velocity.y += y;
}Tilt Behavior
- Tilt device left: negative gamma, ball rolls left
- Tilt device right: positive gamma, ball rolls right
- Tilt device forward: positive beta, ball rolls down
- Tilt device backward: negative beta, ball rolls up
The tilt angle directly maps to velocity increments -- the steeper the tilt, the greater the force applied to the ball each frame.
---
Core Game Mechanics
Game State Structure
Ball.Game = function (game) {};
Ball.Game.prototype = {
create() {},
initLevels() {},
showLevel(level) {},
updateCounter() {},
managePause() {},
manageAudio() {},
update() {},
wallCollision() {},
handleOrientation(e) {},
finishLevel() {},
};Ball Creation and Physics
// In create()
this.ball = this.add.sprite(this.ballStartPos.x, this.ballStartPos.y, "ball");
this.ball.anchor.set(0.5);
this.physics.enable(this.ball, Phaser.Physics.ARCADE);
this.ball.body.setSize(18, 18);
this.ball.body.bounce.set(0.3, 0.3);- Anchor at center
(0.5, 0.5)for rotation around midpoint - Physics body: 18x18 pixels
- Bounce coefficient: 0.3 (retains 30% velocity after wall collision)
Keyboard Controls (Desktop Fallback)
// In create()
this.keys = this.game.input.keyboard.createCursorKeys();
// In update()
if (this.keys.left.isDown) {
this.ball.body.velocity.x -= this.movementForce;
} else if (this.keys.right.isDown) {
this.ball.body.velocity.x += this.movementForce;
}
if (this.keys.up.isDown) {
this.ball.body.velocity.y -= this.movementForce;
} else if (this.keys.down.isDown) {
this.ball.body.velocity.y += this.movementForce;
}Hole (Goal) Setup
this.hole = this.add.sprite(Ball._WIDTH * 0.5, 90, "hole");
this.physics.enable(this.hole, Phaser.Physics.ARCADE);
this.hole.anchor.set(0.5);
this.hole.body.setSize(2, 2);The hole has a tiny 2x2 collision body for precise overlap detection.
---
Level System
Level Data Format
Each level is an array of wall segment objects with position and type:
this.levelData = [
[{ x: 96, y: 224, t: "w" }], // Level 1
[
{ x: 72, y: 320, t: "w" },
{ x: 200, y: 320, t: "h" },
{ x: 72, y: 150, t: "w" },
], // Level 2
// ... more levels
];x, y: Position in pixelst: Type --"w"for horizontal wall,"h"for vertical wall
Building Levels
initLevels() {
for (let i = 0; i < this.maxLevels; i++) {
const newLevel = this.add.group();
newLevel.enableBody = true;
newLevel.physicsBodyType = Phaser.Physics.ARCADE;
for (const item of this.levelData[i]) {
newLevel.create(item.x, item.y, `element-${item.t}`);
}
newLevel.setAll("body.immovable", true);
newLevel.visible = false;
this.levels.push(newLevel);
}
}Showing a Level
showLevel(level) {
const lvl = level || this.level;
if (this.levels[lvl - 2]) {
this.levels[lvl - 2].visible = false;
}
this.levels[lvl - 1].visible = true;
}---
Collision Detection
Wall Collisions (Bounce)
// In update()
this.physics.arcade.collide(
this.ball, this.borderGroup,
this.wallCollision, null, this
);
this.physics.arcade.collide(
this.ball, this.levels[this.level - 1],
this.wallCollision, null, this
);collide causes the ball to bounce off walls and triggers the callback.
Hole Overlap (Pass-Through Detection)
this.physics.arcade.overlap(
this.ball, this.hole,
this.finishLevel, null, this
);overlap detects intersection without physical collision response.
Wall Collision Callback
wallCollision() {
if (this.audioStatus) {
this.bounceSound.play();
}
if ("vibrate" in window.navigator) {
window.navigator.vibrate(100);
}
}---
Audio System
// In create()
this.bounceSound = this.game.add.audio("audio-bounce");
// Toggle
manageAudio() {
this.audioStatus = !this.audioStatus;
}---
Vibration API
if ("vibrate" in window.navigator) {
window.navigator.vibrate(100); // 100ms vibration pulse
}Feature-detect before calling. Provides tactile feedback on supported mobile devices.
---
Timer System
// In create()
this.timer = 0;
this.totalTimer = 0;
this.timerText = this.game.add.text(15, 15, "Time: 0", this.fontBig);
this.totalTimeText = this.game.add.text(120, 30, "Total time: 0", this.fontSmall);
this.time.events.loop(Phaser.Timer.SECOND, this.updateCounter, this);
// Counter callback
updateCounter() {
this.timer++;
this.timerText.setText(`Time: ${this.timer}`);
this.totalTimeText.setText(`Total time: ${this.totalTimer + this.timer}`);
}---
Level Completion
finishLevel() {
if (this.level >= this.maxLevels) {
this.totalTimer += this.timer;
alert(`Congratulations, game completed!\nTotal time: ${this.totalTimer}s`);
this.game.state.start("MainMenu");
} else {
alert(`Level ${this.level} completed!`);
this.totalTimer += this.timer;
this.timer = 0;
this.level++;
this.timerText.setText(`Time: ${this.timer}`);
this.totalTimeText.setText(`Total time: ${this.totalTimer}`);
this.levelText.setText(`Level: ${this.level} / ${this.maxLevels}`);
this.ball.body.x = this.ballStartPos.x;
this.ball.body.y = this.ballStartPos.y;
this.ball.body.velocity.x = 0;
this.ball.body.velocity.y = 0;
this.showLevel();
}
}---
Complete Update Loop
update() {
// Keyboard input
if (this.keys.left.isDown) {
this.ball.body.velocity.x -= this.movementForce;
} else if (this.keys.right.isDown) {
this.ball.body.velocity.x += this.movementForce;
}
if (this.keys.up.isDown) {
this.ball.body.velocity.y -= this.movementForce;
} else if (this.keys.down.isDown) {
this.ball.body.velocity.y += this.movementForce;
}
// Wall collisions
this.physics.arcade.collide(
this.ball, this.borderGroup, this.wallCollision, null, this
);
this.physics.arcade.collide(
this.ball, this.levels[this.level - 1], this.wallCollision, null, this
);
// Hole overlap
this.physics.arcade.overlap(
this.ball, this.hole, this.finishLevel, null, this
);
}---
Phaser API Quick Reference
| Function | Purpose |
|---|---|
this.add.sprite(x, y, key) | Create a game object |
this.add.group() | Create a container for objects |
this.add.button(x, y, key, cb, ctx, over, out, down) | Create interactive button |
this.add.text(x, y, text, style) | Create text display |
this.physics.enable(obj, system) | Enable physics on object |
this.physics.arcade.collide(a, b, cb) | Detect collision with bounce |
this.physics.arcade.overlap(a, b, cb) | Detect overlap without bounce |
this.load.image(key, path) | Load image asset |
this.load.spritesheet(key, path, w, h) | Load sprite animation sheet |
this.load.audio(key, paths[]) | Load audio with format fallbacks |
this.game.add.audio(key) | Instantiate audio object |
this.time.events.loop(interval, cb, ctx) | Create repeating timer |
2D Platform Game Template
A complete step-by-step guide for building a 2D platformer game using Phaser (v2.x / Phaser CE) with Arcade Physics. This template walks through every stage of development: setting up the project, creating platforms from JSON level data, adding a hero with physics-based movement and jumping, collectible coins, walking enemies, death and stomp mechanics, a scoreboard, sprite animations, win conditions with a door/key system, and multi-level progression.
What you will build: A classic side-scrolling platformer where a hero navigates platforms, collects coins, avoids or stomps on spider enemies, finds a key to unlock a door, and progresses through multiple levels -- with score tracking, animations, and physics.
Prerequisites: Basic to intermediate JavaScript knowledge, familiarity with HTML, and a local web server for development (e.g., browser-sync, live-server, or Python's SimpleHTTPServer).
Source: Based on the Mozilla HTML5 Games Workshop - Platformer. Project starter files available at the workshop repository.
---
Start Here
This tutorial builds a 2D platformer using the Phaser framework. Phaser handles rendering, physics, input, audio, and asset loading so you can focus on game logic.
What You Will Build
The finished game features:
- A hero character the player controls with the keyboard
- Platforms the hero can walk and jump on
- Collectible coins that increase the score
- Walking spider enemies that kill the hero on contact (but can be stomped from above)
- A key and door system: the hero must pick up a key to unlock the door and complete the level
- Multiple levels loaded from JSON data files
- A scoreboard showing collected coins
- Sprite animations for the hero (idle, running, jumping, falling)
Project Structure
project/
index.html
js/
phaser.min.js (Phaser 2.6.2 or Phaser CE)
main.js (all game code goes here)
audio/
sfx/
jump.wav
coin.wav
stomp.wav
key.wav
door.wav
images/
background.png
ground.png
grass:8x1.png (platform tile images in various sizes)
grass:6x1.png
grass:4x1.png
grass:2x1.png
grass:1x1.png
hero.png (hero spritesheet: 36x42 per frame)
hero_stopped.png (single frame for initial steps)
coin_animated.png (coin spritesheet)
spider.png (spider spritesheet)
invisible_wall.png (invisible boundary for enemy AI)
key.png (key spritesheet)
door.png (door spritesheet)
key_icon.png (HUD icon for key)
font:numbers.png (bitmap font for score)
data/
level00.json
level01.jsonLevel Data Format
Each level is defined in a JSON file. The JSON structure describes positions of every entity:
{
"hero": { "x": 21, "y": 525 },
"door": { "x": 169, "y": 546 },
"key": { "x": 750, "y": 524 },
"platforms": [
{ "image": "ground", "x": 0, "y": 546 },
{ "image": "grass:8x1", "x": 208, "y": 420 },
{ "image": "grass:4x1", "x": 420, "y": 336 },
{ "image": "grass:2x1", "x": 680, "y": 252 }
],
"coins": [
{ "x": 147, "y": 525 },
{ "x": 189, "y": 525 },
{ "x": 399, "y": 399 },
{ "x": 441, "y": 336 }
],
"spiders": [
{ "x": 121, "y": 399 }
],
"decoration": {
"grass": [
{ "x": 84, "y": 504, "frame": 0 },
{ "x": 420, "y": 504, "frame": 1 }
]
}
}Each entity type (hero, door, key, platforms, coins, spiders) has x and y coordinates. Platforms also specify which image asset to use for that platform tile.
---
Initialise Phaser
The first step is setting up the HTML file and creating the Phaser game instance.
HTML Entry Point
Create an index.html file that loads Phaser and your game script:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Platformer Game</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #000;
}
</style>
<script src="js/phaser.min.js"></script>
<script src="js/main.js"></script>
</head>
<body>
<div id="game"></div>
</body>
</html>- The
<div id="game">is the container where Phaser will insert the game canvas. - Phaser is loaded first, then your game script.
Creating the Game Instance
In js/main.js, create the Phaser game object and register a game state:
// Create a Phaser game instance
// Parameters: width, height, renderer, DOM element ID
window.onload = function () {
let game = new Phaser.Game(960, 600, Phaser.AUTO, 'game');
// Add and start the play state
game.state.add('play', PlayState);
game.state.start('play');
};960, 600sets the game canvas dimensions in pixels.Phaser.AUTOlets Phaser choose between WebGL and Canvas rendering automatically.'game'is the ID of the DOM element that will contain the canvas.
The PlayState Object
Define the game state as an object with lifecycle methods:
PlayState = {};
PlayState.init = function () {
// Called first when the state starts
};
PlayState.preload = function () {
// Load all assets here
};
PlayState.create = function () {
// Create game entities and set up the world
};
PlayState.update = function () {
// Called every frame (~60 times per second)
// Handle game logic, input, collisions here
};init-- runs first; used for configuration and receiving parameters.preload-- used to load all assets (images, audio, JSON) before the game starts.create-- called once after assets are loaded; used to create sprites, groups, and game objects.update-- called every frame at ~60fps; used for input handling, physics checks, and game logic.
At this point you should see an empty black canvas rendered on the page.
---
The Game Loop
Phaser uses a game loop architecture. Every frame, Phaser calls update(), which is where you handle input, move sprites, and check collisions. Before the loop starts, preload() loads assets and create() sets up the initial game state.
Loading and Displaying the Background
Start by loading and displaying a background image to verify the game loop is working:
PlayState.preload = function () {
this.game.load.image('background', 'images/background.png');
};
PlayState.create = function () {
// Add the background image at position (0, 0)
this.game.add.image(0, 0, 'background');
};this.game.load.image(key, path)loads an image and assigns it a key for later reference.this.game.add.image(x, y, key)creates a static image at the given position.
You should now see the background image rendered in the game canvas.
Understanding the Frame Cycle
preload() -> [assets loaded] -> create() -> update() -> update() -> update() -> ...Each call to update() represents one frame. The game targets 60 frames per second. All movement, input reading, and collision detection happen inside update().
---
Creating Platforms
Platforms are the surfaces the hero walks and jumps on. They are loaded from the level JSON data and created as physics-enabled sprites arranged in a group.
Loading Platform Assets
Load the level JSON data and all platform tile images in preload:
PlayState.preload = function () {
this.game.load.image('background', 'images/background.png');
// Load level data
this.game.load.json('level:1', 'data/level01.json');
// Load platform images
this.game.load.image('ground', 'images/ground.png');
this.game.load.image('grass:8x1', 'images/grass_8x1.png');
this.game.load.image('grass:6x1', 'images/grass_6x1.png');
this.game.load.image('grass:4x1', 'images/grass_4x1.png');
this.game.load.image('grass:2x1', 'images/grass_2x1.png');
this.game.load.image('grass:1x1', 'images/grass_1x1.png');
};Spawning Platforms from Level Data
Create a method to load the level and spawn each platform as a sprite inside a physics group:
PlayState.create = function () {
// Add the background
this.game.add.image(0, 0, 'background');
// Load level data and spawn entities
this._loadLevel(this.game.cache.getJSON('level:1'));
};
PlayState._loadLevel = function (data) {
// Create a group for platforms
this.platforms = this.game.add.group();
// Spawn each platform from the level data
data.platforms.forEach(this._spawnPlatform, this);
};
PlayState._spawnPlatform = function (platform) {
// Add a sprite at the platform's position using the specified image
let sprite = this.platforms.create(platform.x, platform.y, platform.image);
// Enable physics on this platform
this.game.physics.enable(sprite);
// Make platform immovable so it doesn't get pushed by the hero
sprite.body.allowGravity = false;
sprite.body.immovable = true;
};this.game.add.group()creates a Phaser group -- a container for related sprites that enables batch operations and collision detection.this.platforms.create(x, y, key)creates a sprite inside the group.sprite.body.immovable = trueprevents the platform from being pushed by other physics bodies.sprite.body.allowGravity = falseprevents platforms from falling due to gravity.
You should now see the ground and grass platform tiles rendered on the screen.
---
The Main Character Sprite
Now add the hero character that the player will control.
Loading the Hero Image
Add the hero image to preload. Initially we use a single static image; we will switch to a spritesheet later for animations:
// In PlayState.preload:
this.game.load.image('hero', 'images/hero_stopped.png');Spawning the Hero
Add the hero to _loadLevel and create a spawn method:
PlayState._loadLevel = function (data) {
this.platforms = this.game.add.group();
data.platforms.forEach(this._spawnPlatform, this);
// Spawn the hero at the position defined in level data
this._spawnCharacters({ hero: data.hero });
};
PlayState._spawnCharacters = function (data) {
// Create the hero sprite
this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');
// Set the anchor to the bottom-center for easier positioning
this.hero.anchor.set(0.5, 1);
};anchor.set(0.5, 1)sets the sprite's origin point to the horizontal center and vertical bottom. This makes it easier to position the hero on top of platforms, since theyposition refers to the hero's feet rather than the top-left corner.
---
Keyboard Controls
Capture keyboard input so the player can move the hero left, right, and jump.
Setting Up Input Keys
In init, configure the keyboard controls:
PlayState.init = function () {
// Force integer rendering for pixel-art crispness
this.game.renderer.renderSession.roundPixels = true;
// Capture arrow keys
this.keys = this.game.input.keyboard.addKeys({
left: Phaser.KeyCode.LEFT,
right: Phaser.KeyCode.RIGHT,
up: Phaser.KeyCode.UP
});
};addKeys()captures the specified keys and returns an object with key state references.Phaser.KeyCode.LEFT,RIGHT,UPcorrespond to the arrow keys.renderSession.roundPixels = trueprevents pixel-art sprites from appearing blurry due to sub-pixel rendering.
Reading Input in Update
Handle the key states in update. For now, just log the direction; the next step adds physics-based movement:
PlayState.update = function () {
this._handleInput();
};
PlayState._handleInput = function () {
if (this.keys.left.isDown) {
// Move hero left
} else if (this.keys.right.isDown) {
// Move hero right
} else {
// Stop (no key held)
}
};this.keys.left.isDownreturnstruewhile the left arrow key is held down.- The
elseclause handles the case where neither left nor right is pressed (the hero should stop).
---
Moving Sprites with Physics
Enable Arcade Physics so the hero can move with velocity and interact with platforms through collisions.
Enabling the Physics Engine
Enable Arcade Physics in init:
PlayState.init = function () {
this.game.renderer.renderSession.roundPixels = true;
this.keys = this.game.input.keyboard.addKeys({
left: Phaser.KeyCode.LEFT,
right: Phaser.KeyCode.RIGHT,
up: Phaser.KeyCode.UP
});
// Enable Arcade Physics
this.game.physics.startSystem(Phaser.Physics.ARCADE);
};Adding a Physics Body to the Hero
Enable physics on the hero sprite in _spawnCharacters:
PlayState._spawnCharacters = function (data) {
this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');
this.hero.anchor.set(0.5, 1);
// Enable physics body on the hero
this.game.physics.enable(this.hero);
};Moving with Velocity
Now update _handleInput to set the hero's velocity based on key presses:
const SPEED = 200; // pixels per second
PlayState._handleInput = function () {
if (this.keys.left.isDown) {
this.hero.body.velocity.x = -SPEED;
} else if (this.keys.right.isDown) {
this.hero.body.velocity.x = SPEED;
} else {
this.hero.body.velocity.x = 0;
}
};body.velocity.xsets the horizontal speed in pixels per second.- A negative value moves the sprite left; positive moves it right.
- Setting velocity to
0when no keys are pressed makes the hero stop immediately.
The hero can now move left and right, but will fall through platforms and off the screen because there is no gravity or collision handling yet.
---
Gravity
Add gravity so the hero falls downward and collides with platforms.
Setting Global Gravity
Enable gravity for the entire physics world in init:
PlayState.init = function () {
this.game.renderer.renderSession.roundPixels = true;
this.keys = this.game.input.keyboard.addKeys({
left: Phaser.KeyCode.LEFT,
right: Phaser.KeyCode.RIGHT,
up: Phaser.KeyCode.UP
});
this.game.physics.startSystem(Phaser.Physics.ARCADE);
// Set global gravity
this.game.physics.arcade.gravity.y = 1200;
};gravity.y = 1200applies a downward acceleration of 1200 pixels per second squared to all physics-enabled sprites (unless they opt out withallowGravity = false).
Collision Detection Between Hero and Platforms
Add collision detection in update so the hero lands on platforms instead of falling through:
PlayState.update = function () {
this._handleCollisions();
this._handleInput();
};
PlayState._handleCollisions = function () {
// Make the hero collide with the platform group
this.game.physics.arcade.collide(this.hero, this.platforms);
};arcade.collide(spriteA, groupB)checks for physics collisions between the hero and every sprite in the platforms group. When the hero lands on a platform, the physics engine prevents it from passing through and resolves the overlap.- It is important to call
_handleCollisions()before_handleInput()so collision data (like whether the hero is touching the ground) is up to date when we process input.
The hero now falls due to gravity and lands on the platforms. You can walk left and right on the platforms.
---
Jumps
Allow the hero to jump when the up arrow key is pressed -- but only when standing on a platform (no mid-air jumps).
Implementing the Jump Mechanic
Add a jump constant and update _handleInput:
const SPEED = 200;
const JUMP_SPEED = 600;
PlayState._handleInput = function () {
if (this.keys.left.isDown) {
this.hero.body.velocity.x = -SPEED;
} else if (this.keys.right.isDown) {
this.hero.body.velocity.x = SPEED;
} else {
this.hero.body.velocity.x = 0;
}
// Handle jumping
if (this.keys.up.isDown) {
this._jump();
}
};
PlayState._jump = function () {
let canJump = this.hero.body.touching.down;
if (canJump) {
this.hero.body.velocity.y = -JUMP_SPEED;
}
return canJump;
};this.hero.body.touching.downistruewhen the hero's physics body is touching another body on its underside -- meaning the hero is standing on something.- Setting
velocity.yto a negative value launches the hero upward (the y-axis points downward in screen coordinates). - The
canJumpcheck prevents the hero from jumping while already in the air, enforcing single-jump behavior. - The method returns whether the jump was performed, which is useful later for playing sound effects.
Adding a Jump Sound Effect
Load a jump sound and play it on successful jumps:
// In PlayState.preload:
this.game.load.audio('sfx:jump', 'audio/sfx/jump.wav');
// In PlayState.create:
this.sfx = {
jump: this.game.add.audio('sfx:jump')
};
// In PlayState._jump, after setting velocity:
PlayState._jump = function () {
let canJump = this.hero.body.touching.down;
if (canJump) {
this.hero.body.velocity.y = -JUMP_SPEED;
this.sfx.jump.play();
}
return canJump;
};---
Pickable Coins
Add collectible coins that the player can pick up to increase their score.
Loading Coin Assets
Load the coin spritesheet and coin sound effect in preload:
// In PlayState.preload:
this.game.load.spritesheet('coin', 'images/coin_animated.png', 22, 22);
this.game.load.audio('sfx:coin', 'audio/sfx/coin.wav');load.spritesheet(key, path, frameWidth, frameHeight)loads a spritesheet and slices it into individual frames of 22x22 pixels for animation.
Spawning Coins from Level Data
Update _loadLevel to create a coins group and spawn each coin:
PlayState._loadLevel = function (data) {
this.platforms = this.game.add.group();
this.coins = this.game.add.group();
data.platforms.forEach(this._spawnPlatform, this);
data.coins.forEach(this._spawnCoin, this);
this._spawnCharacters({ hero: data.hero });
};
PlayState._spawnCoin = function (coin) {
let sprite = this.coins.create(coin.x, coin.y, 'coin');
sprite.anchor.set(0.5, 0.5);
// Add a tween animation to make the coin bob up and down
this.game.physics.enable(sprite);
sprite.body.allowGravity = false;
// Coin bobbing animation with a tween
sprite.animations.add('rotate', [0, 1, 2, 1], 6, true); // 6fps, looping
sprite.animations.play('rotate');
};- Each coin is created inside the
coinsgroup for easy collision detection. allowGravity = falseprevents coins from falling.- The
animations.addcreates a frame animation using the spritesheet frames 0, 1, 2, 1 at 6fps, looping continuously.
Collecting Coins
Add the coin sound to the sfx object and detect overlap between the hero and coins:
// In PlayState.create, add to the sfx object:
this.sfx = {
jump: this.game.add.audio('sfx:jump'),
coin: this.game.add.audio('sfx:coin')
};
// In PlayState._handleCollisions:
PlayState._handleCollisions = function () {
this.game.physics.arcade.collide(this.hero, this.platforms);
// Detect overlap between hero and coins (no physical collision, just overlap)
this.game.physics.arcade.overlap(
this.hero, this.coins, this._onHeroVsCoin, null, this
);
};
PlayState._onHeroVsCoin = function (hero, coin) {
this.sfx.coin.play();
coin.kill(); // Remove the coin from the game
this.coinPickupCount++;
};arcade.overlap()checks if two sprites/groups overlap without resolving collisions physically. When an overlap is detected, it calls the callback function (_onHeroVsCoin).coin.kill()removes the coin sprite from the game world.this.coinPickupCounttracks the number of coins collected (initialize it in_loadLevel).
Initializing the Coin Counter
PlayState._loadLevel = function (data) {
this.platforms = this.game.add.group();
this.coins = this.game.add.group();
data.platforms.forEach(this._spawnPlatform, this);
data.coins.forEach(this._spawnCoin, this);
this._spawnCharacters({ hero: data.hero });
// Initialize coin counter
this.coinPickupCount = 0;
};---
Walking Enemies
Add spider enemies that walk back and forth on platforms. The hero can stomp on them from above but dies if touching them from the side.
Loading Enemy Assets
// In PlayState.preload:
this.game.load.spritesheet('spider', 'images/spider.png', 42, 32);
this.game.load.image('invisible-wall', 'images/invisible_wall.png');
this.game.load.audio('sfx:stomp', 'audio/sfx/stomp.wav');- The spider spritsheet has frames for a crawling animation.
- Invisible walls are placed at platform edges to keep spiders from walking off -- they are not rendered visually but have physics bodies.
Spawning Enemies
Update _loadLevel and add a spawn method for spiders:
PlayState._loadLevel = function (data) {
this.platforms = this.game.add.group();
this.coins = this.game.add.group();
this.spiders = this.game.add.group();
this.enemyWalls = this.game.add.group();
data.platforms.forEach(this._spawnPlatform, this);
data.coins.forEach(this._spawnCoin, this);
data.spiders.forEach(this._spawnSpider, this);
this._spawnCharacters({ hero: data.hero });
// Make enemy walls invisible
this.enemyWalls.visible = false;
this.coinPickupCount = 0;
};Creating Invisible Walls on Platforms
Modify _spawnPlatform to add invisible walls at both edges of each platform:
PlayState._spawnPlatform = function (platform) {
let sprite = this.platforms.create(platform.x, platform.y, platform.image);
this.game.physics.enable(sprite);
sprite.body.allowGravity = false;
sprite.body.immovable = true;
// Spawn invisible walls at the left and right edges of this platform
this._spawnEnemyWall(platform.x, platform.y, 'left');
this._spawnEnemyWall(platform.x + sprite.width, platform.y, 'right');
};
PlayState._spawnEnemyWall = function (x, y, side) {
let sprite = this.enemyWalls.create(x, y, 'invisible-wall');
// Anchor to the bottom of the wall and adjust position based on side
sprite.anchor.set(side === 'left' ? 1 : 0, 1);
this.game.physics.enable(sprite);
sprite.body.immovable = true;
sprite.body.allowGravity = false;
};- Each platform gets two invisible walls, one at each edge.
- The walls act as barriers that prevent spiders from walking off the edge.
- The anchor is set so the wall aligns to the correct side of the platform.
Spawning and Animating Spiders
PlayState._spawnSpider = function (spider) {
let sprite = this.spiders.create(spider.x, spider.y, 'spider');
sprite.anchor.set(0.5, 1);
// Add the crawl animation
sprite.animations.add('crawl', [0, 1, 2], 8, true);
sprite.animations.add('die', [0, 4, 0, 4, 0, 4, 3, 3, 3, 3, 3, 3], 12);
sprite.animations.play('crawl');
// Enable physics
this.game.physics.enable(sprite);
// Set initial movement speed
sprite.body.velocity.x = Spider.SPEED;
};
// Spider speed constant
const Spider = { SPEED: 100 };- Spiders have two animations:
crawl(looping) anddie(played once on death). velocity.x = 100starts the spider moving to the right at 100 pixels per second.
Making Spiders Bounce Off Walls
Add collision handling so spiders reverse direction when hitting invisible walls or platform edges:
// In PlayState._handleCollisions:
PlayState._handleCollisions = function () {
this.game.physics.arcade.collide(this.hero, this.platforms);
this.game.physics.arcade.collide(this.spiders, this.platforms);
this.game.physics.arcade.collide(this.spiders, this.enemyWalls);
this.game.physics.arcade.overlap(
this.hero, this.coins, this._onHeroVsCoin, null, this
);
this.game.physics.arcade.overlap(
this.hero, this.spiders, this._onHeroVsEnemy, null, this
);
};To make spiders reverse direction when colliding with walls, check their velocity each frame and flip them:
// In PlayState.update, after collision handling, update spider directions:
PlayState.update = function () {
this._handleCollisions();
this._handleInput();
// Update spider facing direction based on velocity
this.spiders.forEach(function (spider) {
if (spider.body.touching.right || spider.body.blocked.right) {
spider.body.velocity.x = -Spider.SPEED; // Turn left
} else if (spider.body.touching.left || spider.body.blocked.left) {
spider.body.velocity.x = Spider.SPEED; // Turn right
}
}, this);
};- When a spider touches a wall on its right side, it reverses to move left, and vice versa.
body.touchingis set by Phaser after collision resolution.
---
Death
Implement hero death when touching enemies and the stomp mechanic for killing enemies.
Hero vs Enemy: Stomp or Die
When the hero overlaps with a spider, check if the hero is falling (stomping) or not:
PlayState._onHeroVsEnemy = function (hero, enemy) {
if (hero.body.velocity.y > 0) {
// Hero is falling -> stomp the enemy
enemy.body.velocity.x = 0; // Stop enemy movement
enemy.body.enable = false; // Disable enemy physics
// Play die animation then remove the enemy
enemy.animations.play('die');
enemy.events.onAnimationComplete.addOnce(function () {
enemy.kill();
});
// Bounce the hero up after stomping
hero.body.velocity.y = -JUMP_SPEED / 2;
this.sfx.stomp.play();
} else {
// Hero touched enemy from side or below -> die
this._killHero();
}
};
PlayState._killHero = function () {
this.hero.kill();
// Restart the level after a short delay
this.game.time.events.add(500, function () {
this.game.state.restart(true, false, { level: this.level });
}, this);
};- If
hero.body.velocity.y > 0, the hero is moving downward (falling), indicating a stomp. - On stomp: the enemy stops, plays its death animation, and is removed. The hero bounces up.
- If the hero is not falling, the hero dies.
this.hero.kill()removes the hero from the game. - After 500ms, the entire state is restarted, effectively reloading the level.
Add Stomp Sound
// In PlayState.create, add to sfx:
this.sfx = {
jump: this.game.add.audio('sfx:jump'),
coin: this.game.add.audio('sfx:coin'),
stomp: this.game.add.audio('sfx:stomp')
};Adding a Death Animation for the Hero
Make the hero flash and fall off the screen when dying:
PlayState._killHero = function () {
this.hero.alive = false;
// Play a "dying" visual: the hero jumps up and falls off screen
this.hero.body.velocity.y = -JUMP_SPEED / 2;
this.hero.body.velocity.x = 0;
this.hero.body.allowGravity = true;
// Disable collisions so the hero falls through platforms
this.hero.body.collideWorldBounds = false;
// Restart after a delay
this.game.time.events.add(1000, function () {
this.game.state.restart(true, false, { level: this.level });
}, this);
};Guarding Input When Dead
Prevent input from controlling the hero after death:
PlayState._handleInput = function () {
if (!this.hero.alive) { return; }
if (this.keys.left.isDown) {
this.hero.body.velocity.x = -SPEED;
} else if (this.keys.right.isDown) {
this.hero.body.velocity.x = SPEED;
} else {
this.hero.body.velocity.x = 0;
}
if (this.keys.up.isDown) {
this._jump();
}
};this.hero.aliveis set tofalsein_killHero, so input is ignored after death and the hero falls off screen naturally.
---
Scoreboard
Display the number of collected coins on screen using a bitmap font.
Loading the Bitmap Font
// In PlayState.preload:
this.game.load.image('font:numbers', 'images/numbers.png');
this.game.load.image('icon:coin', 'images/coin_icon.png');Creating the HUD
Create a fixed HUD (heads-up display) that shows the coin icon and count:
PlayState._createHud = function () {
let coinIcon = this.game.make.image(0, 0, 'icon:coin');
// Create a dynamic text label for the coin count
this.hud = this.game.add.group();
// Use a retroFont or a regular text object for the score
let scoreStyle = {
font: '30px monospace',
fill: '#fff'
};
this.coinFont = this.game.add.text(
coinIcon.width + 7, 0, 'x0', scoreStyle
);
this.hud.add(coinIcon);
this.hud.add(this.coinFont);
this.hud.position.set(10, 10);
this.hud.fixedToCamera = true;
};Alternatively, using Phaser's RetroFont for pixel-art number rendering:
PlayState._createHud = function () {
// Bitmap-based number rendering using RetroFont
this.coinFont = this.game.add.retroFont(
'font:numbers', 20, 26,
'0123456789X ', 6
);
let coinIcon = this.game.make.image(0, 0, 'icon:coin');
let coinScoreImg = this.game.make.image(
coinIcon.x + coinIcon.width + 7, 0, this.coinFont
);
this.hud = this.game.add.group();
this.hud.add(coinIcon);
this.hud.add(coinScoreImg);
this.hud.position.set(10, 10);
this.hud.fixedToCamera = true;
};retroFontcreates a bitmap font from a spritesheet containing character glyphs.- Parameters: image key, character width, character height, character set string, number of characters per row.
Calling createHud in create
PlayState.create = function () {
this.game.add.image(0, 0, 'background');
this._loadLevel(this.game.cache.getJSON('level:1'));
// Create the HUD
this._createHud();
};Updating the Score Display
Update the score text whenever a coin is collected:
PlayState._onHeroVsCoin = function (hero, coin) {
this.sfx.coin.play();
coin.kill();
this.coinPickupCount++;
// Update the HUD
this.coinFont.text = 'x' + this.coinPickupCount;
};---
Animations for the Main Character
Replace the static hero image with a spritesheet and add animations for different states: idle (stopped), running, jumping, and falling.
Loading the Hero Spritesheet
Replace the single image load with a spritesheet in preload:
// Replace: this.game.load.image('hero', 'images/hero_stopped.png');
// With:
this.game.load.spritesheet('hero', 'images/hero.png', 36, 42);- The hero spritesheet is 36 pixels wide and 42 pixels tall per frame.
- Frames include idle, walk cycle, jump, and fall poses.
Defining Animations
In _spawnCharacters, add animation definitions after creating the hero sprite:
PlayState._spawnCharacters = function (data) {
this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');
this.hero.anchor.set(0.5, 1);
this.game.physics.enable(this.hero);
// Define animations
this.hero.animations.add('stop', [0]); // Single frame: idle
this.hero.animations.add('run', [1, 2], 8, true); // 2 frames at 8fps, looping
this.hero.animations.add('jump', [3]); // Single frame: jumping up
this.hero.animations.add('fall', [4]); // Single frame: falling down
};animations.add(name, frames, fps, loop)registers an animation with the given name.- Single-frame animations like
stop,jump, andfalleffectively set a static pose. - The
runanimation alternates between frames 1 and 2 at 8fps.
Playing the Correct Animation
Add a method to determine and play the right animation based on the hero's current state:
PlayState._getAnimationName = function () {
let name = 'stop'; // Default: standing still
if (!this.hero.alive) {
name = 'stop'; // Use idle frame when dead
} else if (this.hero.body.velocity.y < 0) {
name = 'jump'; // Moving upward
} else if (this.hero.body.velocity.y > 0 && !this.hero.body.touching.down) {
name = 'fall'; // Moving downward and not on ground
} else if (this.hero.body.velocity.x !== 0 && this.hero.body.touching.down) {
name = 'run'; // Moving horizontally on the ground
}
return name;
};Flipping the Sprite Based on Direction
Update the hero's facing direction and play the animation in update:
PlayState.update = function () {
this._handleCollisions();
this._handleInput();
// Flip sprite based on movement direction
if (this.hero.body.velocity.x < 0) {
this.hero.scale.x = -1; // Face left
} else if (this.hero.body.velocity.x > 0) {
this.hero.scale.x = 1; // Face right
}
// Play the appropriate animation
this.hero.animations.play(this._getAnimationName());
// Update spider directions
this.spiders.forEach(function (spider) {
if (spider.body.touching.right || spider.body.blocked.right) {
spider.body.velocity.x = -Spider.SPEED;
} else if (spider.body.touching.left || spider.body.blocked.left) {
spider.body.velocity.x = Spider.SPEED;
}
}, this);
};this.hero.scale.x = -1flips the sprite horizontally to face left. Setting it to1faces right. Because the anchor is at(0.5, 1), the flip looks natural.animations.play()only restarts the animation if the name changes, so calling it every frame is safe and efficient.
---
Win Condition
Add a door and key mechanic: the hero must collect a key, then reach the door to complete the level.
Loading Door and Key Assets
// In PlayState.preload:
this.game.load.spritesheet('door', 'images/door.png', 42, 66);
this.game.load.spritesheet('key', 'images/key.png', 20, 22); // Key bobbing animation
this.game.load.image('icon:key', 'images/key_icon.png');
this.game.load.audio('sfx:key', 'audio/sfx/key.wav');
this.game.load.audio('sfx:door', 'audio/sfx/door.wav');Spawning the Door and Key
Update _loadLevel and _spawnCharacters:
PlayState._loadLevel = function (data) {
this.platforms = this.game.add.group();
this.coins = this.game.add.group();
this.spiders = this.game.add.group();
this.enemyWalls = this.game.add.group();
this.bgDecoration = this.game.add.group();
// Must spawn decorations first (background layer)
// Spawn door before hero so it renders behind the hero
data.platforms.forEach(this._spawnPlatform, this);
data.coins.forEach(this._spawnCoin, this);
data.spiders.forEach(this._spawnSpider, this);
this._spawnDoor(data.door.x, data.door.y);
this._spawnKey(data.key.x, data.key.y);
this._spawnCharacters({ hero: data.hero });
this.enemyWalls.visible = false;
this.coinPickupCount = 0;
this.hasKey = false;
};
PlayState._spawnDoor = function (x, y) {
this.door = this.bgDecoration.create(x, y, 'door');
this.door.anchor.setTo(0.5, 1);
this.game.physics.enable(this.door);
this.door.body.allowGravity = false;
};
PlayState._spawnKey = function (x, y) {
this.key = this.bgDecoration.create(x, y, 'key');
this.key.anchor.set(0.5, 0.5);
this.game.physics.enable(this.key);
this.key.body.allowGravity = false;
// Add a bobbing up-and-down tween to the key
this.key.y -= 3;
this.game.add.tween(this.key)
.to({ y: this.key.y + 6 }, 800, Phaser.Easing.Sinusoidal.InOut)
.yoyo(true)
.loop()
.start();
};- The door is placed in a background decoration group so it renders behind the hero.
- The key has a sinusoidal bobbing tween that moves it 6 pixels up and down over 800ms, looping forever.
Collecting the Key and Opening the Door
Add key and door sound effects to the sfx object:
// In PlayState.create sfx:
this.sfx = {
jump: this.game.add.audio('sfx:jump'),
coin: this.game.add.audio('sfx:coin'),
stomp: this.game.add.audio('sfx:stomp'),
key: this.game.add.audio('sfx:key'),
door: this.game.add.audio('sfx:door')
};Add overlap detection for the key and door in _handleCollisions:
PlayState._handleCollisions = function () {
this.game.physics.arcade.collide(this.hero, this.platforms);
this.game.physics.arcade.collide(this.spiders, this.platforms);
this.game.physics.arcade.collide(this.spiders, this.enemyWalls);
this.game.physics.arcade.overlap(
this.hero, this.coins, this._onHeroVsCoin, null, this
);
this.game.physics.arcade.overlap(
this.hero, this.spiders, this._onHeroVsEnemy, null, this
);
this.game.physics.arcade.overlap(
this.hero, this.key, this._onHeroVsKey, null, this
);
this.game.physics.arcade.overlap(
this.hero, this.door, this._onHeroVsDoor,
// Only trigger if the hero has the key
function (hero, door) {
return this.hasKey && hero.body.touching.down;
}, this
);
};- The door overlap has a process callback (the fourth argument) that only triggers the overlap callback when
this.hasKeyis true and the hero is standing on something. This prevents the hero from entering the door while falling or without the key.
Key and Door Callbacks
PlayState._onHeroVsKey = function (hero, key) {
this.sfx.key.play();
key.kill();
this.hasKey = true;
};
PlayState._onHeroVsDoor = function (hero, door) {
this.sfx.door.play();
// Freeze the hero and play the door opening animation
hero.body.velocity.x = 0;
hero.body.velocity.y = 0;
hero.body.enable = false;
// Play door open animation (transition from closed to open frame)
door.frame = 1; // Switch to "open" frame
// Advance to the next level after a short delay
this.game.time.events.add(500, this._goToNextLevel, this);
};
PlayState._goToNextLevel = function () {
this.camera.fade('#000');
this.camera.onFadeComplete.addOnce(function () {
this.game.state.restart(true, false, {
level: this.level + 1
});
}, this);
};- When the hero touches the key, the key is removed and
hasKeyis set totrue. - When the hero reaches the door (with the key), the hero freezes, the door opens, and after a delay the game transitions to the next level.
camera.fade()creates a fade-to-black transition for a polished level switch.
Showing the Key Icon in the HUD
Update _createHud to show whether the hero has collected the key:
PlayState._createHud = function () {
this.keyIcon = this.game.make.image(0, 19, 'icon:key');
this.keyIcon.anchor.set(0, 0.5);
// ... existing coin HUD code ...
this.hud.add(this.keyIcon);
this.hud.add(coinIcon);
this.hud.add(coinScoreImg);
this.hud.position.set(10, 10);
this.hud.fixedToCamera = true;
};Update the key icon appearance each frame in update:
// In PlayState.update, add:
this.keyIcon.frame = this.hasKey ? 1 : 0;- Frame 0 shows a grayed-out key icon; frame 1 shows the collected key icon.
---
Switching Levels
Support multiple levels by loading different JSON files based on a level index.
Passing Level Number Through init
Modify init to accept a level parameter:
PlayState.init = function (data) {
this.game.renderer.renderSession.roundPixels = true;
this.keys = this.game.input.keyboard.addKeys({
left: Phaser.KeyCode.LEFT,
right: Phaser.KeyCode.RIGHT,
up: Phaser.KeyCode.UP
});
this.game.physics.startSystem(Phaser.Physics.ARCADE);
this.game.physics.arcade.gravity.y = 1200;
// Store the current level number (default to 0)
this.level = (data.level || 0) % LEVEL_COUNT;
};
const LEVEL_COUNT = 2; // Total number of levelsdatais an object passed fromgame.state.start()orgame.state.restart().- The modulo operation (
% LEVEL_COUNT) wraps around to level 0 after the last level, creating an infinite loop of levels.
Loading Level Data Dynamically
Update preload to load the correct level based on this.level:
PlayState.preload = function () {
this.game.load.image('background', 'images/background.png');
// Load the current level's JSON data
this.game.load.json('level:0', 'data/level00.json');
this.game.load.json('level:1', 'data/level01.json');
// ... load all other assets ...
};Update create to use the correct level data:
PlayState.create = function () {
this.sfx = {
jump: this.game.add.audio('sfx:jump'),
coin: this.game.add.audio('sfx:coin'),
stomp: this.game.add.audio('sfx:stomp'),
key: this.game.add.audio('sfx:key'),
door: this.game.add.audio('sfx:door')
};
this.game.add.image(0, 0, 'background');
// Load level data based on current level number
this._loadLevel(this.game.cache.getJSON('level:' + this.level));
this._createHud();
};Starting the Game at Level 0
Update the initial state start to pass level 0:
window.onload = function () {
let game = new Phaser.Game(960, 600, Phaser.AUTO, 'game');
game.state.add('play', PlayState);
game.state.start('play', true, false, { level: 0 });
};- The third and fourth
startarguments control world/cache clearing.true, falsekeeps the cache between restarts (so assets do not need to be reloaded) but clears the world. { level: 0 }is passed toinitas thedataparameter.
Level Transition Flow
The complete level flow is:
1. Hero collects key -> hasKey = true 2. Hero reaches door -> _onHeroVsDoor fires 3. Camera fades to black -> _goToNextLevel fires 4. State restarts with { level: this.level + 1 } 5. init receives the new level number 6. The correct level JSON is loaded and the game continues
---
Moving Forward
Congratulations -- you have built a complete 2D platformer. Here are ideas for extending the game further:
Suggested Improvements
- Mobile / touch controls: Add on-screen buttons or swipe gestures using
game.input.onDownfor touch-enabled devices. - More levels: Create additional JSON level files with new platform layouts, coin placements, and enemy configurations.
- Menu screen: Add a
MenuStatewith a title screen and start button before enteringPlayState. - Game over screen: Instead of instantly restarting, show a "Game Over" screen with the score.
- Lives system: Give the hero multiple lives instead of instant restart.
- Power-ups: Add items like speed boosts, double jump, or invincibility.
- Moving platforms: Create platforms that travel along a path using tweens.
- Different enemy types: Add flying enemies, enemies that shoot projectiles, or enemies with different movement patterns.
- Parallax scrolling: Add multiple background layers that scroll at different speeds for depth.
- Camera scrolling: For levels wider than the screen, use
game.camera.follow(this.hero)to scroll with the hero. - Sound and music: Add background music and additional sound effects for a more polished experience.
- Particle effects: Use Phaser's particle emitter for coin collection sparkles, enemy death effects, or dust when landing.
Full Game Source Reference
Below is the complete main.js file combining all steps for reference. This represents the final state of the game with all features:
// =============================================================================
// Constants
// =============================================================================
const SPEED = 200;
const JUMP_SPEED = 600;
const LEVEL_COUNT = 2;
const Spider = { SPEED: 100 };
// =============================================================================
// Game State: PlayState
// =============================================================================
PlayState = {};
// -----------------------------------------------------------------------------
// init
// -----------------------------------------------------------------------------
PlayState.init = function (data) {
this.game.renderer.renderSession.roundPixels = true;
this.keys = this.game.input.keyboard.addKeys({
left: Phaser.KeyCode.LEFT,
right: Phaser.KeyCode.RIGHT,
up: Phaser.KeyCode.UP
});
this.game.physics.startSystem(Phaser.Physics.ARCADE);
this.game.physics.arcade.gravity.y = 1200;
this.level = (data.level || 0) % LEVEL_COUNT;
};
// -----------------------------------------------------------------------------
// preload
// -----------------------------------------------------------------------------
PlayState.preload = function () {
// Background
this.game.load.image('background', 'images/background.png');
// Level data
this.game.load.json('level:0', 'data/level00.json');
this.game.load.json('level:1', 'data/level01.json');
// Platform tiles
this.game.load.image('ground', 'images/ground.png');
this.game.load.image('grass:8x1', 'images/grass_8x1.png');
this.game.load.image('grass:6x1', 'images/grass_6x1.png');
this.game.load.image('grass:4x1', 'images/grass_4x1.png');
this.game.load.image('grass:2x1', 'images/grass_2x1.png');
this.game.load.image('grass:1x1', 'images/grass_1x1.png');
// Characters
this.game.load.spritesheet('hero', 'images/hero.png', 36, 42);
this.game.load.spritesheet('spider', 'images/spider.png', 42, 32);
this.game.load.image('invisible-wall', 'images/invisible_wall.png');
// Collectibles
this.game.load.spritesheet('coin', 'images/coin_animated.png', 22, 22);
this.game.load.spritesheet('key', 'images/key.png', 20, 22);
this.game.load.spritesheet('door', 'images/door.png', 42, 66);
// HUD
this.game.load.image('icon:coin', 'images/coin_icon.png');
this.game.load.image('icon:key', 'images/key_icon.png');
this.game.load.image('font:numbers', 'images/numbers.png');
// Audio
this.game.load.audio('sfx:jump', 'audio/sfx/jump.wav');
this.game.load.audio('sfx:coin', 'audio/sfx/coin.wav');
this.game.load.audio('sfx:stomp', 'audio/sfx/stomp.wav');
this.game.load.audio('sfx:key', 'audio/sfx/key.wav');
this.game.load.audio('sfx:door', 'audio/sfx/door.wav');
};
// -----------------------------------------------------------------------------
// create
// -----------------------------------------------------------------------------
PlayState.create = function () {
// Sound effects
this.sfx = {
jump: this.game.add.audio('sfx:jump'),
coin: this.game.add.audio('sfx:coin'),
stomp: this.game.add.audio('sfx:stomp'),
key: this.game.add.audio('sfx:key'),
door: this.game.add.audio('sfx:door')
};
// Background
this.game.add.image(0, 0, 'background');
// Load level
this._loadLevel(this.game.cache.getJSON('level:' + this.level));
// HUD
this._createHud();
};
// -----------------------------------------------------------------------------
// update
// -----------------------------------------------------------------------------
PlayState.update = function () {
this._handleCollisions();
this._handleInput();
// Update hero sprite direction and animation
if (this.hero.body.velocity.x < 0) {
this.hero.scale.x = -1;
} else if (this.hero.body.velocity.x > 0) {
this.hero.scale.x = 1;
}
this.hero.animations.play(this._getAnimationName());
// Update spider directions when hitting walls
this.spiders.forEach(function (spider) {
if (spider.body.touching.right || spider.body.blocked.right) {
spider.body.velocity.x = -Spider.SPEED;
} else if (spider.body.touching.left || spider.body.blocked.left) {
spider.body.velocity.x = Spider.SPEED;
}
}, this);
// Update key icon in HUD
this.keyIcon.frame = this.hasKey ? 1 : 0;
};
// -----------------------------------------------------------------------------
// Level Loading
// -----------------------------------------------------------------------------
PlayState._loadLevel = function (data) {
// Create groups (order matters for rendering layers)
this.bgDecoration = this.game.add.group();
this.platforms = this.game.add.group();
this.coins = this.game.add.group();
this.spiders = this.game.add.group();
this.enemyWalls = this.game.add.group();
// Spawn entities from level data
data.platforms.forEach(this._spawnPlatform, this);
data.coins.forEach(this._spawnCoin, this);
data.spiders.forEach(this._spawnSpider, this);
this._spawnDoor(data.door.x, data.door.y);
this._spawnKey(data.key.x, data.key.y);
this._spawnCharacters({ hero: data.hero });
// Hide invisible walls
this.enemyWalls.visible = false;
// Initialize game state
this.coinPickupCount = 0;
this.hasKey = false;
};
// -----------------------------------------------------------------------------
// Spawn Methods
// -----------------------------------------------------------------------------
PlayState._spawnPlatform = function (platform) {
let sprite = this.platforms.create(platform.x, platform.y, platform.image);
this.game.physics.enable(sprite);
sprite.body.allowGravity = false;
sprite.body.immovable = true;
// Add invisible walls at both edges for enemy AI
this._spawnEnemyWall(platform.x, platform.y, 'left');
this._spawnEnemyWall(platform.x + sprite.width, platform.y, 'right');
};
PlayState._spawnEnemyWall = function (x, y, side) {
let sprite = this.enemyWalls.create(x, y, 'invisible-wall');
sprite.anchor.set(side === 'left' ? 1 : 0, 1);
this.game.physics.enable(sprite);
sprite.body.immovable = true;
sprite.body.allowGravity = false;
};
PlayState._spawnCharacters = function (data) {
this.hero = this.game.add.sprite(data.hero.x, data.hero.y, 'hero');
this.hero.anchor.set(0.5, 1);
this.game.physics.enable(this.hero);
this.hero.body.collideWorldBounds = true;
// Hero animations
this.hero.animations.add('stop', [0]);
this.hero.animations.add('run', [1, 2], 8, true);
this.hero.animations.add('jump', [3]);
this.hero.animations.add('fall', [4]);
};
PlayState._spawnCoin = function (coin) {
let sprite = this.coins.create(coin.x, coin.y, 'coin');
sprite.anchor.set(0.5, 0.5);
this.game.physics.enable(sprite);
sprite.body.allowGravity = false;
sprite.animations.add('rotate', [0, 1, 2, 1], 6, true);
sprite.animations.play('rotate');
};
PlayState._spawnSpider = function (spider) {
let sprite = this.spiders.create(spider.x, spider.y, 'spider');
sprite.anchor.set(0.5, 1);
this.game.physics.enable(sprite);
sprite.animations.add('crawl', [0, 1, 2], 8, true);
sprite.animations.add('die', [0, 4, 0, 4, 0, 4, 3, 3, 3, 3, 3, 3], 12);
sprite.animations.play('crawl');
sprite.body.velocity.x = Spider.SPEED;
};
PlayState._spawnDoor = function (x, y) {
this.door = this.bgDecoration.create(x, y, 'door');
this.door.anchor.setTo(0.5, 1);
this.game.physics.enable(this.door);
this.door.body.allowGravity = false;
};
PlayState._spawnKey = function (x, y) {
this.key = this.bgDecoration.create(x, y, 'key');
this.key.anchor.set(0.5, 0.5);
this.game.physics.enable(this.key);
this.key.body.allowGravity = false;
// Bobbing tween
this.key.y -= 3;
this.game.add.tween(this.key)
.to({ y: this.key.y + 6 }, 800, Phaser.Easing.Sinusoidal.InOut)
.yoyo(true)
.loop()
.start();
};
// -----------------------------------------------------------------------------
// Input
// -----------------------------------------------------------------------------
PlayState._handleInput = function () {
if (!this.hero.alive) { return; }
if (this.keys.left.isDown) {
this.hero.body.velocity.x = -SPEED;
} else if (this.keys.right.isDown) {
this.hero.body.velocity.x = SPEED;
} else {
this.hero.body.velocity.x = 0;
}
if (this.keys.up.isDown) {
this._jump();
}
};
PlayState._jump = function () {
let canJump = this.hero.body.touching.down;
if (canJump) {
this.hero.body.velocity.y = -JUMP_SPEED;
this.sfx.jump.play();
}
return canJump;
};
// -----------------------------------------------------------------------------
// Collisions
// -----------------------------------------------------------------------------
PlayState._handleCollisions = function () {
// Physical collisions
this.game.physics.arcade.collide(this.hero, this.platforms);
this.game.physics.arcade.collide(this.spiders, this.platforms);
this.game.physics.arcade.collide(this.spiders, this.enemyWalls);
// Overlap detection (no physical push)
this.game.physics.arcade.overlap(
this.hero, this.coins, this._onHeroVsCoin, null, this
);
this.game.physics.arcade.overlap(
this.hero, this.spiders, this._onHeroVsEnemy, null, this
);
this.game.physics.arcade.overlap(
this.hero, this.key, this._onHeroVsKey, null, this
);
this.game.physics.arcade.overlap(
this.hero, this.door, this._onHeroVsDoor,
function (hero, door) {
return this.hasKey && hero.body.touching.down;
}, this
);
};
// -----------------------------------------------------------------------------
// Collision Callbacks
// -----------------------------------------------------------------------------
PlayState._onHeroVsCoin = function (hero, coin) {
this.sfx.coin.play();
coin.kill();
this.coinPickupCount++;
this.coinFont.text = 'x' + this.coinPickupCount;
};
PlayState._onHeroVsEnemy = function (hero, enemy) {
if (hero.body.velocity.y > 0) {
// Stomp: hero is falling onto the enemy
enemy.body.velocity.x = 0;
enemy.body.enable = false;
enemy.animations.play('die');
enemy.events.onAnimationComplete.addOnce(function () {
enemy.kill();
});
hero.body.velocity.y = -JUMP_SPEED / 2;
this.sfx.stomp.play();
} else {
// Hero dies
this._killHero();
}
};
PlayState._onHeroVsKey = function (hero, key) {
this.sfx.key.play();
key.kill();
this.hasKey = true;
};
PlayState._onHeroVsDoor = function (hero, door) {
this.sfx.door.play();
hero.body.velocity.x = 0;
hero.body.velocity.y = 0;
hero.body.enable = false;
door.frame = 1; // Open door
this.game.time.events.add(500, this._goToNextLevel, this);
};
// -----------------------------------------------------------------------------
// Death and Level Transitions
// -----------------------------------------------------------------------------
PlayState._killHero = function () {
this.hero.alive = false;
this.hero.body.velocity.y = -JUMP_SPEED / 2;
this.hero.body.velocity.x = 0;
this.hero.body.allowGravity = true;
this.hero.body.collideWorldBounds = false;
this.game.time.events.add(1000, function () {
this.game.state.restart(true, false, { level: this.level });
}, this);
};
PlayState._goToNextLevel = function () {
this.camera.fade('#000');
this.camera.onFadeComplete.addOnce(function () {
this.game.state.restart(true, false, {
level: this.level + 1
});
}, this);
};
// -----------------------------------------------------------------------------
// Animations
// -----------------------------------------------------------------------------
PlayState._getAnimationName = function () {
let name = 'stop';
if (!this.hero.alive) {
name = 'stop';
} else if (this.hero.body.velocity.y < 0) {
name = 'jump';
} else if (this.hero.body.velocity.y > 0 && !this.hero.body.touching.down) {
name = 'fall';
} else if (this.hero.body.velocity.x !== 0 && this.hero.body.touching.down) {
name = 'run';
}
return name;
};
// -----------------------------------------------------------------------------
// HUD
// -----------------------------------------------------------------------------
PlayState._createHud = function () {
this.keyIcon = this.game.make.image(0, 19, 'icon:key');
this.keyIcon.anchor.set(0, 0.5);
let coinIcon = this.game.make.image(
this.keyIcon.width + 7, 0, 'icon:coin'
);
let scoreStyle = { font: '24px monospace', fill: '#fff' };
this.coinFont = this.game.add.text(
coinIcon.x + coinIcon.width + 7, 0, 'x0', scoreStyle
);
this.hud = this.game.add.group();
this.hud.add(this.keyIcon);
this.hud.add(coinIcon);
this.hud.add(this.coinFont);
this.hud.position.set(10, 10);
this.hud.fixedToCamera = true;
};
// =============================================================================
// Entry Point
// =============================================================================
window.onload = function () {
let game = new Phaser.Game(960, 600, Phaser.AUTO, 'game');
game.state.add('play', PlayState);
game.state.start('play', true, false, { level: 0 });
};Key Concepts Summary
| Concept | Phaser API | Purpose |
|---|---|---|
| Game instance | new Phaser.Game(w, h, renderer, container) | Creates the game canvas and engine |
| Game states | game.state.add() / game.state.start() | Organizes code into init/preload/create/update lifecycle |
| Loading images | game.load.image(key, path) | Loads a static image asset |
| Loading spritesheets | game.load.spritesheet(key, path, fw, fh) | Loads an animated spritesheet |
| Loading JSON | game.load.json(key, path) | Loads JSON data (level definitions) |
| Loading audio | game.load.audio(key, path) | Loads a sound effect |
| Sprite groups | game.add.group() | Container for related sprites; enables batch collision detection |
| Physics bodies | game.physics.enable(sprite) | Adds an Arcade Physics body to a sprite |
| Gravity | game.physics.arcade.gravity.y | Global downward acceleration |
| Collision | arcade.collide(a, b) | Physical collision resolution (sprites push each other) |
| Overlap | arcade.overlap(a, b, callback) | Detection without physical push (for pickups) |
| Velocity | sprite.body.velocity.x/y | Movement speed in pixels per second |
| Immovable | sprite.body.immovable = true | Prevents sprite from being pushed by collisions |
| Animations | sprite.animations.add(name, frames, fps, loop) | Defines a frame animation |
| Tweens | game.add.tween(target).to(props, duration, easing) | Smooth property animation |
| Keyboard input | game.input.keyboard.addKeys({...}) | Captures specific keyboard keys |
| Camera | this.camera.fade() | Screen transition effects |
| Anchor | sprite.anchor.set(x, y) | Sets the origin point for positioning and rotation |
| Sprite flipping | sprite.scale.x = -1 | Horizontally mirrors the sprite |
GameBase Template Repository
A feature-rich, opinionated starter template for 2D game projects built with Haxe and the Heaps game engine. Created and maintained by Sebastien Benard (deepnight), the lead developer behind Dead Cells. GameBase provides a production-tested foundation with entity management, level integration via LDtk, rendering pipeline, and a game loop architecture -- all designed to let developers skip boilerplate and jump straight into game-specific logic.
Repository: github.com/deepnight/gameBase Author: Sebastien Benard / deepnight Technology: Haxe + Heaps (HashLink or JS targets) Level editor integration: LDtk
---
Purpose
GameBase exists to solve the "blank project" problem. Instead of setting up rendering, entity systems, camera controls, debug overlays, and level loading from scratch, developers clone this repository and begin implementing game-specific mechanics immediately. It reflects patterns refined through commercial game development, particularly from the development of Dead Cells.
Key benefits:
- Pre-built entity system with grid-based positioning and sub-pixel precision
- LDtk level editor integration for visual level design
- Built-in debug tools and overlays
- Frame-rate independent game loop with fixed-step updates
- Camera system with follow, shake, zoom, and clamp
- Configurable Controller/input management
- Scalable rendering pipeline with Heaps
---
Repository Structure
gameBase/
src/
game/
App.hx -- Application entry point and initialization
Game.hx -- Main game process, holds level and entities
Entity.hx -- Base entity class with grid coords, velocity, animation
Level.hx -- Level loading and collision map from LDtk
Camera.hx -- Camera follow, shake, zoom, clamping
Fx.hx -- Visual effects (particles, flashes, etc.)
Types.hx -- Enums, typedefs, and constants
en/
Hero.hx -- Player entity (example implementation)
Mob.hx -- Enemy entity (example implementation)
import.hx -- Global imports (available everywhere)
res/
atlas/ -- Sprite sheets and texture atlases
levels/ -- LDtk level project files
fonts/ -- Bitmap fonts
.ldtk -- LDtk project file (root)
build.hxml -- Haxe compiler configuration
Makefile -- Build/run shortcuts
README.md---
Key Files and Their Roles
src/game/App.hx -- Application Entry Point
The main application class that extends dn.Process. Handles:
- Window/display initialization
- Scene management (root scene graph)
- Global input controller setup
- Debug toggle and console
class App extends dn.Process {
public static var ME : App;
override function init() {
ME = this;
// Initialize rendering, controller, assets
new Game();
}
}src/game/Game.hx -- Game Process
Manages the active game session:
- Holds reference to the current
Level - Manages all active
Entityinstances (via a global linked list) - Handles pause, game-over, and restart logic
- Coordinates camera and effects
class Game extends dn.Process {
public var level : Level;
public var hero : en.Hero;
public var fx : Fx;
public var camera : Camera;
public function new() {
super(App.ME);
level = new Level();
fx = new Fx();
camera = new Camera();
hero = new en.Hero();
}
}src/game/Entity.hx -- Base Entity
The core entity class featuring:
- Grid-based positioning:
cx,cy(integer cell coordinates) plusxr,yr(sub-cell ratio 0.0 to 1.0) for smooth sub-pixel movement - Velocity and friction:
dx,dy(velocity) with configurablefrictX,frictY - Gravity: Optional per-entity gravity
- Sprite management: Animated sprite via Heaps
h2d.Animordn.heaps.HSprite - Lifecycle:
update(),fixedUpdate(),postUpdate(),dispose() - Collision helpers:
hasCollision(cx, cy)check against the level collision map
class Entity {
// Grid position
public var cx : Int = 0; // Cell X
public var cy : Int = 0; // Cell Y
public var xr : Float = 0.5; // X ratio within cell (0..1)
public var yr : Float = 1.0; // Y ratio within cell (0..1)
// Velocity
public var dx : Float = 0;
public var dy : Float = 0;
// Pixel position (computed)
public var attachX(get,never) : Float;
inline function get_attachX() return (cx + xr) * Const.GRID;
public var attachY(get,never) : Float;
inline function get_attachY() return (cy + yr) * Const.GRID;
// Physics step
public function fixedUpdate() {
xr += dx;
dx *= frictX;
// X collision
if (xr > 1) { cx++; xr--; }
if (xr < 0) { cx--; xr++; }
yr += dy;
dy *= frictY;
// Y collision
if (yr > 1) { cy++; yr--; }
if (yr < 0) { cy--; yr++; }
}
}src/game/Level.hx -- Level Management
Loads and manages level data from LDtk project files:
- Parses tile layers, entity layers, and int grid layers
- Builds a collision grid (
hasCollision(cx, cy)) - Provides helper methods to query the level structure
class Level {
var data : ldtk.Level;
var collisions : Map<Int, Bool>;
public function new(ldtkLevel) {
data = ldtkLevel;
// Parse IntGrid layer for collision marks
for (cy in 0...data.l_Collisions.cHei)
for (cx in 0...data.l_Collisions.cWid)
if (data.l_Collisions.getInt(cx, cy) == 1)
collisions.set(coordId(cx, cy), true);
}
public inline function hasCollision(cx:Int, cy:Int) : Bool {
return collisions.exists(coordId(cx, cy));
}
}src/game/Camera.hx -- Camera System
Provides:
- Target tracking: Follow an entity smoothly with configurable dead zones
- Shake: Screen shake with decay
- Zoom: Dynamic zoom in/out
- Clamping: Keep the camera within level bounds
src/game/Fx.hx -- Effects System
Particle and visual effect management:
- Particle pools
- Screen flash
- Slow-motion helpers
- Color overlay effects
---
Technology Stack
Haxe
A cross-platform, high-level programming language that compiles to multiple targets:
- HashLink (HL): Native bytecode VM for desktop (primary dev target)
- JavaScript (JS): Browser/web target
- C/C++: Via HXCPP for native builds
Heaps (Heaps.io)
A high-performance, cross-platform 2D/3D game engine:
- GPU-accelerated rendering via OpenGL/DirectX/WebGL
- Scene graph architecture with
h2d.Objecthierarchy - Sprite batching and texture atlases
- Bitmap font rendering
- Input abstraction
LDtk
A modern, open-source 2D level editor created by Sebastien Benard:
- Visual, tile-based level design
- IntGrid layers for collision and metadata
- Entity layers for game object placement
- Auto-tiling rules
- Haxe API auto-generated from the project file
---
Setup Instructions
Prerequisites
1. Install Haxe (4.0+): haxe.org 2. Install HashLink (for desktop target): hashlink.haxe.org 3. Install LDtk (for level editing): ldtk.io
Getting Started
# Clone the repository
git clone https://github.com/deepnight/gameBase.git my-game
cd my-game
# Install Haxe dependencies
haxelib install heaps
haxelib install deepnightLibs
haxelib install ldtk-haxe-api
# Build and run (HashLink target)
haxe build.hxml
hl bin/client.hl
# Or use the Makefile (if available)
make runUsing as a Starting Point
1. Clone or use the template -- Do not fork; clone into a new directory with your game's name. 2. Rename the package -- Update src/game/ package declarations and project references to match your game. 3. Edit `build.hxml` -- Adjust the main class, output path, and target as needed. 4. Design levels in LDtk -- Open the .ldtk file, define your layers and entities, and export. 5. Implement entities -- Create new entity classes in src/game/en/ extending Entity. 6. Iterate -- Use the debug console (toggle in-game) for live inspection and tuning.
---
Build Targets
| Target | Command | Output | Use Case |
|---|---|---|---|
| HashLink | haxe build.hxml | bin/client.hl | Development, desktop release |
| JavaScript | haxe build.js.hxml | bin/client.js | Web/browser builds |
| DirectX/OpenGL | Via HL native | Native executable | Production desktop release |
---
Debug Features
GameBase includes built-in debug tooling:
- Debug overlay: Toggle with a key to show entity bounds, grid, velocities, collision map
- Console: In-game command console for toggling flags, teleporting, spawning entities
- FPS counter: Visible frame-rate and update-rate monitor
- Process inspector: View active processes and their hierarchy
---
Game Loop Architecture
GameBase uses a fixed-timestep game loop pattern:
Each frame:
1. preUpdate() -- Input polling, pre-frame logic
2. fixedUpdate() -- Physics, movement, collisions (fixed timestep)
- May run 0-N times per frame to catch up
3. update() -- General per-frame logic
4. postUpdate() -- Sprite position sync, camera update, rendering prepThis ensures physics behavior is consistent regardless of frame rate, while rendering and visual updates remain smooth.
---
Entity Lifecycle
Constructor --> init() --> [game loop: fixedUpdate/update/postUpdate] --> dispose()- Constructor: Set initial position, create sprite, register in global entity list
- fixedUpdate(): Physics step (velocity, friction, gravity, collision)
- update(): AI, state machine, animation triggers
- postUpdate(): Sync sprite position to grid coordinates, apply visual effects
- dispose(): Remove from entity list, destroy sprite, clean up references
Paddle Game Template (2D Breakout)
A complete step-by-step guide for building a 2D Breakout game with pure JavaScript and the HTML5 Canvas API. This template walks through every stage of development, from setting up the canvas to implementing a lives system and polished game loop.
What you will build: A classic breakout/paddle game where the player controls a paddle to bounce a ball and destroy a field of bricks, with score tracking, win/lose conditions, keyboard and mouse controls, and a lives system.
Prerequisites: Basic to intermediate JavaScript knowledge and familiarity with HTML.
Source: Based on the MDN 2D Breakout Game Tutorial.
---
Step 1: Create the Canvas and Draw on It
The first step is setting up the HTML document with a <canvas> element and learning to draw basic shapes using the 2D rendering context.
HTML Structure
Create your base HTML file with an embedded canvas element:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>Gamedev Canvas Workshop</title>
<style>
* {
padding: 0;
margin: 0;
}
canvas {
background: #eeeeee;
display: block;
margin: 0 auto;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="480" height="320"></canvas>
<script>
// JavaScript code goes here
</script>
</body>
</html>Getting the Canvas Reference and 2D Context
The canvas element provides a drawing surface. You access it through a 2D rendering context:
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");canvasis a reference to the HTML<canvas>element.ctxis the 2D rendering context object, which provides all drawing methods.
Drawing a Filled Rectangle
Use rect() to define a rectangle and fill() to render it:
ctx.beginPath();
ctx.rect(20, 40, 50, 50);
ctx.fillStyle = "red";
ctx.fill();
ctx.closePath();- The first two parameters (
20, 40) set the top-left corner coordinates. - The second two parameters (
50, 50) set the width and height. fillStylesets the fill color.fill()renders the shape as a solid fill.
Drawing a Circle
Use arc() to define a circle:
ctx.beginPath();
ctx.arc(240, 160, 20, 0, Math.PI * 2, false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath();240, 160-- center x, y coordinates.20-- radius.0-- start angle (radians).Math.PI * 2-- end angle (full circle).false-- draw clockwise.
Drawing a Stroked Rectangle (Outline Only)
Use stroke() instead of fill() for outlines, and strokeStyle for outline color:
ctx.beginPath();
ctx.rect(160, 10, 100, 40);
ctx.strokeStyle = "rgb(0 0 255 / 50%)";
ctx.stroke();
ctx.closePath();- Uses an RGB color with 50% alpha transparency.
stroke()draws only the outline, not a solid fill.
Key Methods Reference
| Method | Purpose |
|---|---|
beginPath() | Start a new drawing path |
closePath() | Close the current path |
rect(x, y, width, height) | Define a rectangle |
arc(x, y, radius, startAngle, endAngle, counterclockwise) | Define a circle or arc |
fillStyle | Set the fill color |
fill() | Fill the shape with the fill color |
strokeStyle | Set the stroke (outline) color |
stroke() | Draw an outline of the shape |
Complete Code for Step 1
<canvas id="myCanvas" width="480" height="320"></canvas>
<style>
* { padding: 0; margin: 0; }
canvas { background: #eeeeee; display: block; margin: 0 auto; }
</style>
<script>
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
// Filled red square
ctx.beginPath();
ctx.rect(20, 40, 50, 50);
ctx.fillStyle = "red";
ctx.fill();
ctx.closePath();
// Filled green circle
ctx.beginPath();
ctx.arc(240, 160, 20, 0, Math.PI * 2, false);
ctx.fillStyle = "green";
ctx.fill();
ctx.closePath();
// Stroked blue rectangle (semi-transparent)
ctx.beginPath();
ctx.rect(160, 10, 100, 40);
ctx.strokeStyle = "rgb(0 0 255 / 50%)";
ctx.stroke();
ctx.closePath();
</script>---
Step 2: Move the Ball
Now we animate the ball by creating a game loop that redraws the canvas on each frame and updates the ball position using velocity variables.
Creating the Draw Loop
Define a draw() function that executes repeatedly using setInterval:
function draw() {
// drawing code
}
setInterval(draw, 10);setInterval(draw, 10) calls the draw function every 10 milliseconds, creating approximately 100 frames per second.
Drawing the Ball
Inside the draw() function, draw a ball (circle) at a fixed position:
ctx.beginPath();
ctx.arc(50, 50, 10, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();Adding Position Variables
Instead of hardcoded positions, use variables so we can update them each frame. Place these above the draw() function:
let x = canvas.width / 2;
let y = canvas.height - 30;This starts the ball at the horizontal center, near the bottom of the canvas.
Adding Velocity Variables
Define speed and direction for horizontal (dx) and vertical (dy) movement:
let dx = 2;
let dy = -2;dx = 2moves the ball 2 pixels right per frame.dy = -2moves the ball 2 pixels up per frame (negative y is upward on canvas).
Updating Position Each Frame
Add position updates at the end of the draw() function:
x += dx;
y += dy;Clearing the Canvas
Without clearing, the ball leaves a trail. Add clearRect() at the start of each frame:
ctx.clearRect(0, 0, canvas.width, canvas.height);Refactoring Into a Separate drawBall() Function
For clean, maintainable code, separate the ball-drawing logic:
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}Complete Code for Step 2
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
x += dx;
y += dy;
}
setInterval(draw, 10);Key concepts:
- Animation loop:
setInterval(draw, 10)continuously redraws the scene. - Position variables:
xandytrack the ball's current location. - Velocity variables:
dxanddydetermine movement per frame. - Canvas clearing:
clearRect()removes the previous frame before drawing the new one.
---
Step 3: Bounce Off the Walls
We add collision detection so the ball bounces off the canvas edges instead of disappearing.
Defining the Ball Radius
Extract the ball radius into a named constant for reuse in collision calculations:
const ballRadius = 10;Update drawBall() to use this variable:
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}Basic Wall Collision (Without Radius Adjustment)
The simplest approach checks if the next ball position goes beyond the canvas boundaries:
// Left and right walls
if (x + dx > canvas.width || x + dx < 0) {
dx = -dx;
}
// Top and bottom walls
if (y + dy > canvas.height || y + dy < 0) {
dy = -dy;
}Reversing dx or dy (multiplying by -1) changes the ball's direction.
Improved Collision (Accounting for Ball Radius)
The basic version lets the ball sink halfway into the wall before bouncing. To fix this, account for the ball's radius:
// Left and right walls
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
// Top and bottom walls
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
dy = -dy;
}Collision Detection Conditions
| Wall | Condition | Action |
|---|---|---|
| Left | x + dx < ballRadius | dx = -dx |
| Right | x + dx > canvas.width - ballRadius | dx = -dx |
| Top | y + dy < ballRadius | dy = -dy |
| Bottom | y + dy > canvas.height - ballRadius | dy = -dy |
Complete Code for Step 3
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
// Collision detection - left and right walls
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
// Collision detection - top and bottom walls
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
dy = -dy;
}
x += dx;
y += dy;
}
setInterval(draw, 10);---
Step 4: Paddle and Keyboard Controls
Now we add a player-controlled paddle at the bottom of the screen and wire up keyboard input (left/right arrow keys).
Defining Paddle Variables
const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;paddleHeightandpaddleWidthdefine the paddle dimensions.paddleXstarts the paddle centered horizontally. It is aletbecause it will change as the player moves it.
Drawing the Paddle
Create a drawPaddle() function. The paddle sits at the very bottom of the canvas:
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}- The y-position is
canvas.height - paddleHeight, placing it flush with the bottom edge.
Keyboard State Variables
Track whether arrow keys are currently pressed:
let rightPressed = false;
let leftPressed = false;Event Listeners for Key Presses
Register handlers for keydown (key pressed) and keyup (key released):
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);Key Handler Functions
Set the boolean flags based on which key is pressed or released:
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}Both "ArrowRight" (modern browsers) and "Right" (legacy IE/Edge) are checked for compatibility.
Paddle Movement Logic (With Boundary Checking)
Add this inside the draw() function to move the paddle based on key state, while keeping it within canvas bounds:
if (rightPressed) {
paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
} else if (leftPressed) {
paddleX = Math.max(paddleX - 7, 0);
}- The paddle moves 7 pixels per frame.
Math.minprevents the paddle from going past the right edge.Math.maxprevents it from going past the left edge.
Complete Code for Step 4
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;
let rightPressed = false;
let leftPressed = false;
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
drawPaddle();
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) {
dy = -dy;
}
if (rightPressed) {
paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
} else if (leftPressed) {
paddleX = Math.max(paddleX - 7, 0);
}
x += dx;
y += dy;
}
setInterval(draw, 10);---
Step 5: Game Over
We replace the bottom-wall bounce with actual game logic: the ball should bounce off the paddle, but if it misses, it is game over.
Storing the Interval Reference
To stop the game loop on game over, store the interval ID:
let interval = 0;Then assign the return value of setInterval:
interval = setInterval(draw, 10);Implementing Game Over and Paddle Collision
Replace the bottom-wall collision check. Instead of bouncing off the bottom edge, we now check whether the ball hits the paddle or misses it:
if (y + dy < ballRadius) {
// Ball hits top wall -- bounce
dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
// Ball reaches bottom edge
if (x > paddleX && x < paddleX + paddleWidth) {
// Ball hits paddle -- bounce
dy = -dy;
} else {
// Ball missed the paddle -- game over
alert("GAME OVER");
document.location.reload();
clearInterval(interval);
}
}How paddle collision works:
x > paddleX-- the ball is past the paddle's left edge.x < paddleX + paddleWidth-- the ball is before the paddle's right edge.- If both are true, the ball is above the paddle, so it bounces.
- If the ball reaches the bottom without hitting the paddle, the game ends.
Complete Code for Step 5
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;
let rightPressed = false;
let leftPressed = false;
let interval = 0;
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBall();
drawPaddle();
// Left and right wall collision
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
// Top wall collision
if (y + dy < ballRadius) {
dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
// Bottom edge: paddle collision or game over
if (x > paddleX && x < paddleX + paddleWidth) {
dy = -dy;
} else {
alert("GAME OVER");
document.location.reload();
clearInterval(interval);
}
}
// Paddle movement
if (rightPressed) {
paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
} else if (leftPressed) {
paddleX = Math.max(paddleX - 7, 0);
}
x += dx;
y += dy;
}
interval = setInterval(draw, 10);---
Step 6: Build the Brick Field
Now we create the grid of bricks that the ball will destroy. The bricks are stored in a 2D array and drawn in rows and columns.
Brick Configuration Variables
Define constants that control the layout of the brick field:
const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;brickRowCount/brickColumnCount-- how many rows and columns of bricks.brickWidth/brickHeight-- dimensions of each individual brick.brickPadding-- space between bricks.brickOffsetTop/brickOffsetLeft-- distance from the top and left canvas edges to the first brick.
Creating the Bricks 2D Array
Use nested loops to create a 2D array. Each brick stores its x and y position (initially 0, calculated during drawing):
const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0 };
}
}The drawBricks() Function
Loop through every brick, calculate its position, store it, and draw it:
function drawBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}Position calculation formula:
brickX = column * (brickWidth + brickPadding) + brickOffsetLeftbrickY = row * (brickHeight + brickPadding) + brickOffsetTop
This creates an evenly-spaced grid with consistent padding and margins.
Calling drawBricks() in the Game Loop
Add the call at the beginning of your draw() function, after clearing the canvas:
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
// ... rest of draw function
}Complete Code for Step 6
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;
let rightPressed = false;
let leftPressed = false;
let interval = 0;
const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;
const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0 };
}
}
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (y + dy < ballRadius) {
dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
if (x > paddleX && x < paddleX + paddleWidth) {
dy = -dy;
} else {
alert("GAME OVER");
document.location.reload();
clearInterval(interval);
}
}
if (rightPressed) {
paddleX = Math.min(paddleX + 7, canvas.width - paddleWidth);
} else if (leftPressed) {
paddleX = Math.max(paddleX - 7, 0);
}
x += dx;
y += dy;
}
interval = setInterval(draw, 10);---
Step 7: Collision Detection
With bricks on screen, we need to detect when the ball hits one and make it disappear. Each brick gets a status property: 1 means visible, 0 means destroyed.
Adding the Status Property to Bricks
Update the brick initialization to include a status flag:
const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}The collisionDetection() Function
Loop through every brick and check if the ball's center is within the brick's bounding box:
function collisionDetection() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const b = bricks[c][r];
if (b.status === 1) {
if (
x > b.x &&
x < b.x + brickWidth &&
y > b.y &&
y < b.y + brickHeight
) {
dy = -dy;
b.status = 0;
}
}
}
}
}Collision conditions (all four must be true simultaneously):
x > b.x-- ball center is to the right of the brick's left edge.x < b.x + brickWidth-- ball center is to the left of the brick's right edge.y > b.y-- ball center is below the brick's top edge.y < b.y + brickHeight-- ball center is above the brick's bottom edge.
When a collision is detected:
dy = -dyreverses the ball's vertical direction (bounce).b.status = 0marks the brick as destroyed.
Updating drawBricks() to Respect Status
Only draw bricks that are still active (status === 1):
function drawBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status === 1) {
const brickX = c * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY = r * (brickHeight + brickPadding) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
}Calling collisionDetection() in the Game Loop
Add the call in your draw() function, after drawing all elements:
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
collisionDetection();
// ... rest of draw function
}---
Step 8: Track the Score and Win
We add a score counter that increments each time a brick is destroyed, and a win condition that triggers when all bricks are gone.
Initializing the Score
let score = 0;The drawScore() Function
Display the current score on the canvas using text rendering:
function drawScore() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText(`Score: ${score}`, 8, 20);
}ctx.fontsets the font size and family (like CSS).ctx.fillText(text, x, y)renders text at the given coordinates.- Position
(8, 20)places the score in the top-left corner.
Incrementing the Score
In the collisionDetection() function, increment the score when a brick is hit:
dy = -dy;
b.status = 0;
score++;Adding the Win Condition
After incrementing the score, check if the player has destroyed all bricks:
score++;
if (score === brickRowCount * brickColumnCount) {
alert("YOU WIN, CONGRATULATIONS!");
document.location.reload();
clearInterval(interval);
}The total number of bricks is brickRowCount * brickColumnCount. When the score reaches that number, every brick has been destroyed.
Complete collisionDetection() with Score and Win
function collisionDetection() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const b = bricks[c][r];
if (b.status === 1) {
if (
x > b.x &&
x < b.x + brickWidth &&
y > b.y &&
y < b.y + brickHeight
) {
dy = -dy;
b.status = 0;
score++;
if (score === brickRowCount * brickColumnCount) {
alert("YOU WIN, CONGRATULATIONS!");
document.location.reload();
clearInterval(interval);
}
}
}
}
}
}Calling drawScore() in the Game Loop
Add the call in your draw() function:
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
drawScore();
collisionDetection();
// ... rest of draw function
}Canvas Text Methods Reference
| Method/Property | Purpose |
|---|---|
ctx.font | Set font size and family |
ctx.fillStyle | Set text color |
ctx.fillText(text, x, y) | Draw filled text at coordinates |
---
Step 9: Mouse Controls
In addition to keyboard controls, we add mouse support so the player can move the paddle by moving the mouse.
Adding the mousemove Event Listener
Register the handler alongside the existing keyboard listeners:
document.addEventListener("mousemove", mouseMoveHandler);The mouseMoveHandler Function
Calculate the mouse's horizontal position relative to the canvas and update the paddle position:
function mouseMoveHandler(e) {
const relativeX = e.clientX - canvas.offsetLeft;
if (relativeX > 0 && relativeX < canvas.width) {
paddleX = relativeX - paddleWidth / 2;
}
}How it works:
e.clientX-- the mouse's horizontal position in the browser viewport.canvas.offsetLeft-- the distance from the canvas's left edge to the viewport's left edge.relativeX-- the mouse position relative to the canvas (not the viewport).- The boundary check (
relativeX > 0 && relativeX < canvas.width) ensures the paddle only moves when the mouse is over the canvas. paddleX = relativeX - paddleWidth / 2centers the paddle under the mouse cursor by subtracting half the paddle width.
Complete Event Listener Setup (Keyboard + Mouse)
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
document.addEventListener("mousemove", mouseMoveHandler);Both control methods work simultaneously. The player can use arrow keys or mouse -- or switch between them at any time.
---
Step 10: Finishing Up
The final step adds a lives system (so the player gets multiple chances) and upgrades the game loop from setInterval to requestAnimationFrame for smoother rendering.
Adding the Lives Variable
let lives = 3;The drawLives() Function
Display the remaining lives in the top-right corner:
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
}Implementing the Lives System
Replace the immediate game-over logic with a lives-based system. When the ball misses the paddle:
if (y + dy < ballRadius) {
dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
if (x > paddleX && x < paddleX + paddleWidth) {
dy = -dy;
} else {
lives--;
if (!lives) {
alert("GAME OVER");
document.location.reload();
} else {
// Reset ball and paddle positions
x = canvas.width / 2;
y = canvas.height - 30;
dx = 2;
dy = -2;
paddleX = (canvas.width - paddleWidth) / 2;
}
}
}What happens when a life is lost:
lives--decrements the lives counter.- If
livesreaches0, the game ends with an alert and page reload. - Otherwise, the ball resets to center-bottom, velocity resets, and the paddle resets to center.
Upgrading to requestAnimationFrame
Replace setInterval with requestAnimationFrame for a smoother, browser-optimized game loop:
Old approach (remove):
interval = setInterval(draw, 10);New approach: Add requestAnimationFrame(draw) at the end of the draw() function:
function draw() {
// ... all drawing and logic ...
requestAnimationFrame(draw);
}
// Start the game by calling draw() once:
draw();requestAnimationFrame lets the browser schedule rendering at the optimal frame rate (typically 60fps), which is more efficient than a fixed 10ms interval.
Calling drawLives() in the Game Loop
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
drawScore();
drawLives();
collisionDetection();
// ... rest of logic ...
requestAnimationFrame(draw);
}---
Complete Final Game Code
Below is the entire game in a single, self-contained HTML file. This is the final product of all 10 steps combined.
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>2D Breakout Game</title>
<style>
* {
padding: 0;
margin: 0;
}
canvas {
background: #eeeeee;
display: block;
margin: 0 auto;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="480" height="320"></canvas>
<script>
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
// --- Ball ---
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
// --- Paddle ---
const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;
// --- Controls ---
let rightPressed = false;
let leftPressed = false;
// --- Bricks ---
const brickRowCount = 3;
const brickColumnCount = 5;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;
const bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
// --- Score and Lives ---
let score = 0;
let lives = 3;
// =====================
// Event Listeners
// =====================
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
document.addEventListener("mousemove", mouseMoveHandler);
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
function mouseMoveHandler(e) {
const relativeX = e.clientX - canvas.offsetLeft;
if (relativeX > 0 && relativeX < canvas.width) {
paddleX = relativeX - paddleWidth / 2;
}
}
// =====================
// Collision Detection
// =====================
function collisionDetection() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const b = bricks[c][r];
if (b.status === 1) {
if (
x > b.x &&
x < b.x + brickWidth &&
y > b.y &&
y < b.y + brickHeight
) {
dy = -dy;
b.status = 0;
score++;
if (score === brickRowCount * brickColumnCount) {
alert("YOU WIN, CONGRATULATIONS!");
document.location.reload();
}
}
}
}
}
}
// =====================
// Drawing Functions
// =====================
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(
paddleX,
canvas.height - paddleHeight,
paddleWidth,
paddleHeight
);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status === 1) {
const brickX =
c * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY =
r * (brickHeight + brickPadding) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
}
function drawScore() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText(`Score: ${score}`, 8, 20);
}
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
}
// =====================
// Main Game Loop
// =====================
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
drawScore();
drawLives();
collisionDetection();
// Left and right wall collision
if (
x + dx > canvas.width - ballRadius ||
x + dx < ballRadius
) {
dx = -dx;
}
// Top wall collision
if (y + dy < ballRadius) {
dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
// Bottom edge: paddle collision or lose a life
if (x > paddleX && x < paddleX + paddleWidth) {
dy = -dy;
} else {
lives--;
if (!lives) {
alert("GAME OVER");
document.location.reload();
} else {
x = canvas.width / 2;
y = canvas.height - 30;
dx = 2;
dy = -2;
paddleX = (canvas.width - paddleWidth) / 2;
}
}
}
// Paddle movement (keyboard)
if (rightPressed) {
paddleX = Math.min(
paddleX + 7,
canvas.width - paddleWidth
);
} else if (leftPressed) {
paddleX = Math.max(paddleX - 7, 0);
}
x += dx;
y += dy;
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>---
Quick Reference: All Game Variables
| Variable | Type | Purpose |
|---|---|---|
canvas | const | Reference to the HTML canvas element |
ctx | const | 2D rendering context |
ballRadius | const | Radius of the ball (10) |
x, y | let | Current ball position |
dx, dy | let | Ball velocity (pixels per frame) |
paddleHeight | const | Height of the paddle (10) |
paddleWidth | const | Width of the paddle (75) |
paddleX | let | Current horizontal position of the paddle |
rightPressed | let | Whether the right arrow key is held down |
leftPressed | let | Whether the left arrow key is held down |
brickRowCount | const | Number of brick rows (3) |
brickColumnCount | const | Number of brick columns (5) |
brickWidth | const | Width of each brick (75) |
brickHeight | const | Height of each brick (20) |
brickPadding | const | Space between bricks (10) |
brickOffsetTop | const | Distance from top of canvas to first brick row (30) |
brickOffsetLeft | const | Distance from left of canvas to first brick column (30) |
bricks | const | 2D array holding all brick objects |
score | let | Current player score |
lives | let | Remaining lives (starts at 3) |
Quick Reference: All Functions
| Function | Purpose |
|---|---|
keyDownHandler(e) | Sets rightPressed or leftPressed to true on key press |
keyUpHandler(e) | Sets rightPressed or leftPressed to false on key release |
mouseMoveHandler(e) | Moves paddle to follow mouse horizontal position |
collisionDetection() | Checks ball against all active bricks; destroys hit bricks, increments score, checks win |
drawBall() | Renders the ball at current (x, y) position |
drawPaddle() | Renders the paddle at current paddleX position |
drawBricks() | Renders all bricks with status === 1 |
drawScore() | Renders the score text in the top-left corner |
drawLives() | Renders the lives text in the top-right corner |
draw() | Main game loop: clears canvas, draws everything, handles collisions, updates positions |
Related skills
How it compares
Pick game-engine over generic frontend skills when you need a complete Phaser maze prototype with tilt controls rather than UI components.
FAQ
What is the game loop and why is it essential?
The game loop is a continuous cycle of: (1) Process Input, (2) Update State, (3) Render. Use requestAnimationFrame for smooth, browser-optimized rendering. Every game engine revolves around this loop.
How do I handle collision detection in 2D and 3D games?
2D uses AABB, circle, or SAT-based collision. 3D uses bounding box, bounding sphere, or raycasting. For fast-moving objects, use continuous collision detection or reduce time steps.
Why does audio not play in my web game?
Browsers require user interaction before playing audio. Trigger playback from a click handler, not immediately on page load.
Is Game Engine safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.