π¦ 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)