π¦ 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 thetcjsgame-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
withphysics = true
andgravity
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>
4. Explanations & tips
- Gravity & flap: The bird uses
physics = true
andgravity
/gravitySpeed
. When the player flaps we setbird.speedY
to a negative number to push the bird upward.hitBottom()
andbounce
can be used if you want bounce physics; here we treat hitting floor as game over. - Spawning pipes:
spawnPipes()
creates twoComponent
rectangles positioned outside the right edge and setsspeedX
negative so they move left. We keep our ownpipes
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 (helperremoveComponentFromComm()
). - 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 callenableTCJSPerf(display, { useDelta:true })
. Then convert movement math to true px/sec by adjusting speeds and multiply bydt
inmove(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 decreasePIPE_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 incomm
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)