Game Engine
GitHubOfficialFirst committed by jhaugaDistributed by GitHub
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The Game Engine skill provides a structured approach to developing interactive 2D and 3D games for the web. It covers the entire lifecycle of game development, from core engine architecture—such as the game loop and rendering pipelines—to specific implementations of physics, collision detection, and input handling. The skill includes reference documentation on web APIs, algorithms, and 3D theory, alongside practical templates for common game types like platformers and maze games.
What it does
- Provides workflows for building game engines and games from scratch.
- Explains core concepts like game loops, rendering, and physics.
- Offers reference material for WebGL, Canvas, and Web Audio APIs.
- Includes starter templates for 2D platformers and breakout-style games.
- Covers advanced topics like raycasting, collision detection, and multiplayer networking.
- Guides users through asset optimization and game publishing.
When to use it
- When you need to build a web-based game or a custom game engine.
- When implementing game mechanics like physics, collision, or input controls.
- When you need to understand 3D rendering pipelines or WebGL fundamentals.
- When you are looking for starter code for 2D or 3D web game projects.
- When you need to optimize game performance or handle cross-browser audio issues.
Advantages
- Centralizes essential game development knowledge for the web.
- Provides ready-to-use templates to accelerate project setup.
- Offers clear, step-by-step procedures for common game development tasks.
- Includes troubleshooting guides for common rendering and performance issues.
- Supports both 2D (Canvas) and 3D (WebGL/Three.js) development workflows.
Usage examples
- “How do I set up a basic game loop using requestAnimationFrame?”
- The agent provides a code structure that initializes a canvas, defines an update function for game logic, and a render function for drawing, tied together by a recursive requestAnimationFrame call.
- “Can you help me implement AABB collision detection for my 2D platformer?”
- The agent explains the Axis-Aligned Bounding Box algorithm and provides a JavaScript function to check if two rectangular objects overlap.
- “What are the steps to publish my web game?”
- The agent outlines a checklist including asset compression, code minification, cross-browser testing, and choosing a distribution platform.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/github/game-engine/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip game-engine.zip -d .agents/skills
Documentation by jhauga
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:
Process Input - Read keyboard, mouse, touch, or gamepad input
Update State - Update game object positions, physics, AI, and logic
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
Set up an HTML file with a
<canvas>elementGet the 2D rendering context
Implement the game loop using
requestAnimationFrameCreate game objects with position, velocity, and size properties
Handle keyboard/mouse input for player control
Implement collision detection between game objects
Add scoring, lives, and win/lose conditions
Add sound effects and music
Building a 3D Game
Choose a framework (Three.js, Babylon.js, A-Frame, or PlayCanvas)
Set up the scene, camera, and renderer
Load or create 3D models and textures
Implement lighting and shaders
Add physics and collision detection
Implement player controls and camera movement
Add audio and visual effects
Publishing a Game
Optimize assets (compress images, minify code)
Test across browsers and devices
Choose distribution platform (web, app stores, game portals)
Implement monetization if needed
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 |
Files
- ATTRIBUTION.mdAdded by AutoGineer851 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md6.2 KiB
- assets/2d-maze-game.md13.0 KiB
- assets/2d-platform-game.md58.2 KiB
- assets/gameBase-template-repo.md9.9 KiB
- assets/paddle-game-template.md38.4 KiB
- assets/simple-2d-engine.md14.0 KiB
- references/3d-web-games.md23.9 KiB
- references/algorithms.md24.5 KiB
- references/basics.md10.9 KiB
- references/game-control-mechanisms.md19.1 KiB
- references/game-engine-core-principles.md21.6 KiB
- references/game-publishing.md13.7 KiB
- references/techniques.md27.4 KiB
- references/terminology.md19.4 KiB
- references/web-apis.md51.4 KiB
assets/paddle-game-template.md
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 |
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
jhauga
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/game-engine · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- jhauga
AutoGineer is not affiliated with or endorsed by jhauga or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download game-engine.zip