DEV Community

Cover image for 🐦 Build Flappy Bird with TCJSgame v3 β€” Step-by-Step Tutorial
Kehinde Owolabi
Kehinde Owolabi

Posted on

🐦 Build Flappy Bird with TCJSgame v3 β€” Step-by-Step Tutorial

🐦 Build Flappy Bird with TCJSgame v3 β€” Step-by-Step Tutorial

This tutorial shows how to create a simple Flappy Bird clone using TCJSgame v3. The game uses a rectangle β€œbird”, procedurally generated pipes, collision detection with crashWith(), simple gravity & flap mechanics, scoring, and restart logic.

Assumes tcjsgame-v3.js is loaded locally or from your site (and optionally the tcjsgame-perf.js extension if you want requestAnimationFrame + delta-time behavior).


What you’ll learn

  • Set up a TCJSgame display and components
  • Implement gravity and flap controls
  • Spawn moving pipes with a randomized gap
  • Detect collisions and restart the game
  • Track and display score

1. Project structure (single file)

All code shown below is contained in one HTML file for easy testing. Save as flappy.html and open in a browser.


2. The idea (quick)

  • The Bird is a Component with physics = true and gravity set.
  • Clicking or pressing Space makes the bird "flap" (upward impulse).
  • Pipes are Component rectangles that move left and are periodically spawned; each pair has a gap.
  • If the bird crashWith() any pipe or hits the top/bottom, game over.
  • Score increments when the bird passes a pipe.

3. Full working code (copy & paste)

<!doctype html> <html> <head> <meta charset="utf-8" /> <title>Flappy Bird β€” TCJSgame v3</title> <style> /* minimal page styling */ body { margin:0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; background:#111; color:#eee; display:flex; align-items:center; justify-content:center; height:100vh; } .wrap { width: 900px; max-width: 100%; } #hud { display:flex; justify-content:space-between; margin-bottom:8px; } #score { font-weight:700; font-size:18px; } #message { font-size:14px; opacity:0.9; } canvas { display:block; border-radius:8px; box-shadow:0 8px 30px rgba(0,0,0,0.6); border:6px solid rgba(255,255,255,0.02); background: linear-gradient(#87CEEB,#cfeefe); } .btn { background:#1e88e5; color:white; padding:8px 12px; border-radius:6px; cursor:pointer; border:0; } </style> </head> <body> <div class="wrap"> <div id="hud"> <div id="score">Score: 0</div> <div id="message">Press Space or click/tap to flap β€” avoid pipes</div> </div> <!-- include the engine --> <script src="tcjsgame-v3.js"></script> <!-- Optional: include performance extension to use requestAnimationFrame + delta time --> <!-- <script src="https://tcjsgame.vercel.app/mat/tcjsgame-perf.js"></script> --> <script> // ---------- Game constants ---------- const CANVAS_W = 900; const CANVAS_H = 600; const PIPE_WIDTH = 80; const PIPE_GAP = 180; // vertical gap between top and bottom pipe const PIPE_SPACING = 1500; // ms between spawns const PIPE_SPEED = 3; // pixels per frame (increase with difficulty) const GRAVITY = 0.35; const FLAP_STRENGTH = -6.5; // ---------- Globals ---------- const display = new Display(); display.start(CANVAS_W, CANVAS_H); // If you included the perf extension, enable delta-time: // enableTCJSPerf(display, { useDelta:false, cacheTiles:false, cullMargin: 32 }); let scoreEl = document.getElementById('score'); let msgEl = document.getElementById('message'); // Bird component let bird = new Component(36, 26, "orange", 140, 200, "rect"); bird.physics = true; bird.gravity = GRAVITY; bird.speedX = 0; bird.speedY = 0; bird.bounce = 0.2; display.add(bird); // Pipes container (we'll keep track ourselves so we can remove) let pipes = []; // each pipe is an object { top:Component, bottom:Component, scored:false } // Utility: remove a component from global comm so it stops being drawn/updated function removeComponentFromComm(comp) { for (let i = comm.length - 1; i >= 0; i--) { if (comm[i].x === comp) { comm.splice(i, 1); break; } } } // Spawn a pair of pipes (top and bottom) with randomized gap position function spawnPipes() { const gapTop = 80 + Math.random() * (CANVAS_H - 240 - PIPE_GAP); // top position of gap const xStart = display.canvas.width + 40; // top pipe (height = gapTop) let topPipe = new Component(PIPE_WIDTH, gapTop, "green", xStart, 0, "rect"); topPipe.speedX = -PIPE_SPEED; topPipe.physics = false; display.add(topPipe); // bottom pipe (y = gapTop + PIPE_GAP) let bottomPipeY = gapTop + PIPE_GAP; let bottomH = CANVAS_H - bottomPipeY; let bottomPipe = new Component(PIPE_WIDTH, bottomH, "green", xStart, bottomPipeY, "rect"); bottomPipe.speedX = -PIPE_SPEED; bottomPipe.physics = false; display.add(bottomPipe); pipes.push({ top: topPipe, bottom: bottomPipe, scored: false }); } // Clear all pipes function clearPipes() { pipes.forEach(p => { removeComponentFromComm(p.top); removeComponentFromComm(p.bottom); }); pipes = []; } // Reset game state let score = 0; let lastSpawn = performance.now(); let running = true; function resetGame() { // reset bird bird.x = 140; bird.y = 200; bird.speedX = 0; bird.speedY = 0; bird.gravitySpeed = 0; bird.physics = true; // clear pipes and reset score clearPipes(); score = 0; updateScore(); lastSpawn = performance.now() + 500; running = true; msgEl.textContent = "Press Space or click/tap to flap β€” avoid pipes"; } function updateScore() { scoreEl.textContent = "Score: " + score; } // Flap action function flap() { bird.speedY = FLAP_STRENGTH; } // Input handlers window.addEventListener("keydown", (e) => { if (e.code === "Space") { if (!running) { resetGame(); return; } flap(); } // optional arrow up too if (e.keyCode === 38) flap(); }); // Click / touch flap or restart display.canvas.addEventListener("mousedown", (e) => { if (!running) { resetGame(); return; } flap(); }); display.canvas.addEventListener("touchstart", (e) => { e.preventDefault(); if (!running) { resetGame(); return; } flap(); }, { passive:false }); // The global update function called by TCJSgame (v3) function update(dt) { // dt may be passed by perf-extension (in seconds). If not provided, ignore dt and use frame-based speeds. const useDt = typeof dt === "number"; // Apply gravity (if component physics uses gravitySpeed when moving) if (useDt) { // If perf extension supplies dt, convert speeds to px/sec style (we interpret our speeds as px per frame in this simple version, // so we keep scale consistent by multiplying by 60 for older values β€” to simplify, we just update position directly with physics.) bird.gravitySpeed += bird.gravity * dt * 60; bird.y += bird.speedY * dt * 60 + bird.gravitySpeed; } else { // original per-frame behavior bird.gravitySpeed += bird.physics ? bird.gravity : 0; bird.y += bird.speedY + bird.gravitySpeed; } // ensure bird doesn't rotate or move horizontally // horizontal movement not used in flappy // spawn pipes periodically const now = performance.now(); if (now - lastSpawn > PIPE_SPACING) { spawnPipes(); lastSpawn = now; } // move pipes & check for offscreen and scoring for (let i = pipes.length - 1; i >= 0; i--) { const pair = pipes[i]; // move each pipe (they have speedX already set and will be moved by their component.move() in the engine loop) // But we need to increase their x manually if using dt mode (if component.move expects no dt) // Instead rely on their speedX and the engine's move() which will increment per frame. // Remove pipes that are fully offscreen if (pair.top.x + pair.top.width < -50) { // remove both removeComponentFromComm(pair.top); removeComponentFromComm(pair.bottom); pipes.splice(i, 1); continue; } // scoring: when pipe's right edge passes the bird and not yet scored if (!pair.scored && pair.top.x + pair.top.width < bird.x) { pair.scored = true; score += 1; updateScore(); } // collision: if bird crashes with either pipe -> game over if (pair.top.crashWith(bird) || pair.bottom.crashWith(bird)) { gameOver(); } } // check top / bottom bounds if (bird.y < -20 || bird.y + bird.height > display.canvas.height + 20) { // bird out of bounds gameOver(); } } function gameOver() { if (!running) return; running = false; msgEl.textContent = "Game Over β€” Click or press Space to restart"; // stop all pipe movement pipes.forEach(p => { p.top.speedX = 0; p.bottom.speedX = 0; }); // optionally, show a small bounce bird.speedY = 0; bird.gravitySpeed = 0; } // initialize game resetGame(); // Note: display.updat() is already scheduled by the engine's start() call. // The global update(dt) function above will be called on each tick. </script> </div> </body> </html> 
Enter fullscreen mode Exit fullscreen mode

4. Explanations & tips

  • Gravity & flap: The bird uses physics = true and gravity/gravitySpeed. When the player flaps we set bird.speedY to a negative number to push the bird upward. hitBottom() and bounce can be used if you want bounce physics; here we treat hitting floor as game over.
  • Spawning pipes: spawnPipes() creates two Component rectangles positioned outside the right edge and sets speedX negative so they move left. We keep our own pipes array so we can easily manage scoring and removal.
  • Collision: crashWith() works for rotated/translated rectangles too β€” we use that to detect bird/pipe collisions.
  • Restart & cleanup: To remove a component from rendering/updating we splice it from the engine's comm array (helper removeComponentFromComm()).
  • Scoring: We mark the pipe pair as scored once its right edge passes the bird to avoid double counting.

5. Optional improvements

  • Use perf extension + dt: include tcjsgame-perf.js and call enableTCJSPerf(display, { useDelta:true }). Then convert movement math to true px/sec by adjusting speeds and multiply by dt in move(dt).
  • Polish visuals: replace rectangle pipes and bird with sprites or images.
  • Add sounds: let s = new Sound("flap.wav"); s.play() on flap, and play a hit sound on game over.
  • Difficulty curve: increase PIPE_SPEED and/or decrease PIPE_GAP as score increases.
  • Mobile friendly: add on-screen touch buttons, tune gravity and flap strength.

6. Troubleshooting

  • If pipes don’t move, ensure you added them with display.add(pipe) so they’re in comm and updated each frame.
  • If collisions feel off, double-check component sizes & positions and consider using slightly smaller collision boxes for fair gameplay.
  • If the engine is running too fast on high-refresh monitors, include the perf extension and enable useDelta.

7. Final notes

This version uses simple per-frame motion so it runs with the stock v3 engine (which uses setInterval by default). For production quality, integrate the tcjsgame-perf.js extension and migrate movement to delta-time so the game speed is consistent on different devices.

Top comments (0)