DEV Community

Cover image for Build Flappy Bird with TCJSGame โ€“ Complete Tutorial with Pause Button & Sound
Kehinde Owolabi
Kehinde Owolabi

Posted on

Build Flappy Bird with TCJSGame โ€“ Complete Tutorial with Pause Button & Sound

Build Flappy Bird with TCJSGame โ€“ Complete Tutorial with Pause Button & Sound

๐ŸŽฎ Live Demo: https://tcjsgame.vercel.app/sample/inx.html

The Complete Game Code

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, inital-scale=1.0" />
<title>Flappy bird</title>
<style type="text/css">
canvas{
  background-color: grey;
  width: 100%;
  height:100vh;
}
body{
  margin:0;
}
button{
    position: fixed;
    top:0;
    right:0;
    text-align: center;
    width: 1cm;
    height: 2cm;
    font-size: 30px;
}   
</style>
<script src="https://tcjsgame.vercel.app/mat/tcjsgame-v3.js"></script>
</head>
<body>  
<script>
if(!localStorage.hs){
    localStorage.hs = 0
}
console.log(localStorage.hs)
let display = new Display()
display.start(700, 300)
let hh = new Component(700,300,"hh.png",0,0,"image")
display.add(hh)
display.add(hh, 1)
display.add(hh, 2)
let bird = new Component(30,30,"player.png",10,10,"image")
bird.physics = true
bird.gravity = 0.03
display.add(bird)
bird.bounce = 0
let rotate = 0
let bgMusic = new Sound("/Vicke Blanka - Black Rover (Audio Video (TVใ‚ขใƒ‹ใƒกใ€Œใƒ–ใƒฉใƒƒใ‚ฏใ‚ฏใƒญใƒผใƒใƒผใ€็ฌฌ3ใ‚ฏใƒผใƒซ.mp3")
addEventListener("keypress", ()=>{
    // bgMusic.play()
})
addEventListener("mousedown",(e)=>{
    if (display.scene == 0) {
        bird.gravity = -0.1
        rotate = -3.14 * 5 / 1800
        setTimeout(() => {
            bird.gravity = 0.03
            rotate = 3.14 * 3 / 360
        }, 250)
    } else if (display.scene == 1) {
        display.scene = 0
        bird.y = 0
        tt.x = 700
        tt2.x = 700 + 233.333333333
        tt3.x = 700 + 466.666666667
        bb.x = 700
        bb2.x = 700 + 233.333333333
        bb3.x = 700 + 466.666666667
        tt.speedX = -1
        tt2.speedX = -1
        tt3.speedX = -1
        bb.speedX = -1
        bb2.speedX = -1
        bb3.speedX = -1
        num = 0
        gj = setInterval(() => {
            num++
            nhs = Number(localStorage.hs)
            if (nhs <= num) {
                localStorage.hs = num
            }
        }, 1000)
    }
})
addEventListener("keypress", (e)=>{
  if(e.key == " "){
      if(display.scene == 0){
    bird.gravity = -0.1
    rotate = -3.14*5/1800
    setTimeout(()=>{
      bird.gravity = 0.03
      rotate = 3.14*3/360
        },250)
    }else if(display.scene == 1){
        display.scene = 0
        bird.y = 0
        tt.x = 700
        tt2.x = 700+233.333333333
        tt3.x = 700+466.666666667
        bb.x = 700
        bb2.x = 700+233.333333333
        bb3.x = 700+466.666666667
        tt.speedX = -1
        tt2.speedX = -1
        tt3.speedX = -1
        bb.speedX = -1
        bb2.speedX = -1
        bb3.speedX = -1
        num = 0
        gj = setInterval(() => {
    num++
    nhs = Number(localStorage.hs)
    if (nhs <= num) {
        localStorage.hs = num
    }
}, 1000)
    }
  }
})
    let height = Math.random()*200
    let otherHeight = display.canvas.height - height - 75
    let tt = new Component(30, height, "pole2.png", 700, 0,"image")
    let tt2 = new Component(30, height, "pole2.png", 700+233.333333333, 0,"image")
    let tt3 = new Component(30, height, "pole2.png", 700+466.666666667, 0,"image")
    let bb = new Component(30, otherHeight, "pole2.png", 700, 300-otherHeight,"image")
    let bb2 = new Component(30, otherHeight, "pole2.png", 700+233.333333333, 300-otherHeight,"image")
    let bb3 = new Component(30, otherHeight, "pole2.png", 700+466.666666667, 300-otherHeight,"image")
    console.log(bb)
    tt.speedX = -1
    tt2.speedX = -1
    tt3.speedX = -1
    bb.speedX = -1
    bb2.speedX = -1
    bb3.speedX = -1
    display.add(bb)
    display.add(bb2)
    display.add(bb3)
    display.add(tt)
    display.add(tt2)
    display.add(tt3)
let gameOver = new Component("25px","impact","red",90,90,"text")
let score = new Component("25px","impact","black",10,30,"text")
let iii;
let pause = new Component("24px","impact","red", 675, 25,"text")
pause.text = "||"
display.add(gameOver, 1)
let cc = [tt,tt2,tt3,bb,bb2,bb3]
setInterval(()=>{
    tt.speedX-=0.1
    tt2.speedX-=0.1
    tt3.speedX-=0.1
    bb.speedX-=0.1
    bb2.speedX-=0.1
    bb3.speedX-=0.1
}, 3000)
let nhs;
let gj;
gj = setInterval(()=>{num++
                      nhs = Number(localStorage.hs)
                     if(nhs<=num){
        localStorage.hs = num
    }
                     }, 1000)
display.add(score)
display.add(score, 1)
let num = 0
let rr
display.add(pause)
function update(){
    score.text = num
    gameOver.text=("Game Over! Press space to try again. HighScore :"+localStorage.hs)
    bird.angle += rotate
    if(bird.angle >= 0){
        bird.angle = 0
        rotate = 0
    } else{
    }
    if(bird.y <= 0){
        bird.y = 0
    }
    iii=0
    cc.forEach((e)=>{
        iii+=1
        if(bird.crashWith(e)){
            display.scene = 1
            clearInterval(gj)
        }
        if(e.x < -30){
            e.x = 700- num
            if(e.y == 0){
                e.height = Math.random()*200
            }
            if(iii == 4){
                e.height = 300-tt.height-75
                e.y = 300 - e.height
            }
            if(iii == 5){
                e.height = 300-tt2.height-75
                e.y = 300 - e.height
            }
            if(iii == 6){
                e.height = 300-tt3.height-75
                e.y = 300 - e.height
            }
        }    
    })
    bird.hitBottom()
}
let pp = false
function paus(){
    console.log("Entered")
    if(pp == false){
        console.log("Working")
        document.getElementById("ppp").innerHTML = "<|"
        display.scene = 2
        pp=true
        clearInterval(gj)
    }else{
        pp=false
        console.log("Perfect")
        document.getElementById("ppp").innerHTML = "||"
        display.scene = 0
        gj = setInterval(() => {
    num++
    nhs = Number(localStorage.hs)
    if (nhs <= num) {
        localStorage.hs = num
    }
}, 1000)        
    }
}
</script>
<button id="ppp" onclick="paus()">||</button>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Game Assets You Need

Image File Description Preview
hh.png Sky/Background (700x300px) ๐ŸŒค๏ธ
player.png Bird character (30x30px) ๐Ÿฆ
pole2.png Pipe obstacle (30x200+px) ๐ŸŸข

Step 1: HTML Structure with Pause Button

The HTML now includes a pause button in the top-right corner. The button has position: fixed so it stays visible even when scrolling. The CSS defines width: 1cm, height: 2cm, and font-size: 30px for easy tapping on mobile devices. The button has id="ppp" and calls paus() when clicked. The canvas fills the entire screen with width: 100% and height: 100vh, while the button floats above the game.

button{
    position: fixed;  /* Stays in place when scrolling */
    top: 0;           /* Align to top edge */
    right: 0;         /* Align to right edge */
    width: 1cm;       /* Square button width */
    height: 2cm;      /* Tall button height */
    font-size: 30px;  /* Large pause symbol */
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Three-Scene System with Pause Mode

The game now uses three scenes instead of two. Scene 0 is active gameplay, scene 1 is game over, and scene 2 is pause mode. The background is added to all three scenes with display.add(hh, 1) and display.add(hh, 2). When paused, the game stops updating and scoring pauses. The pause button text changes from "||" (play symbol) to "<|" (pause symbol) when clicked. This creates a professional game feel.

// Background appears in all scenes
display.add(hh)        // Scene 0 (gameplay)
display.add(hh, 1)     // Scene 1 (game over)
display.add(hh, 2)     // Scene 2 (pause menu)

// Pause text component
let pause = new Component("24px","impact","red", 675, 25,"text")
pause.text = "||"      // Pause symbol
display.add(pause)      // Always visible
Enter fullscreen mode Exit fullscreen mode

Step 3: Sound System Integration

The game now includes audio with let bgMusic = new Sound("/path/to/music.mp3"). The Sound class is built into TCJSGame and creates an HTML5 audio element. The music file path points to "Vicke Blanka - Black Rover" anime opening theme. A keypress event listener is set up but commented out with // bgMusic.play() so you can enable it when ready. Sound files must be in the correct format (MP3 works across all modern browsers). You can also add jump sound effects by uncommenting and modifying the code.

// Create sound object
let bgMusic = new Sound("music.mp3")

// Play when game starts (uncomment to enable)
addEventListener("keypress", ()=>{
    bgMusic.play()  // Plays background music
})

// Add jump sound effect
let jumpSound = new Sound("jump.wav")
// Call jumpSound.play() in jump code
Enter fullscreen mode Exit fullscreen mode

Step 4: Dual Input Methods (Mouse + Keyboard)

Your game now supports both mouse clicks and keyboard spacebar. The mousedown event triggers jumps and restarts, making the game playable on mobile devices where keyboards aren't available. Two separate event listeners handle input: one for mouse/touch and one for keyboard. Both check display.scene to determine if the game is playing (scene 0) or game over (scene 1). The jump physics apply negative gravity -0.1 and rotation calculations using 3.14 * 5 / 1800 (approximately 0.0087 radians).

// Mouse/touch input (mobile friendly)
addEventListener("mousedown", (e)=>{
    if (display.scene == 0) {
        bird.gravity = -0.1    // Jump up
        rotate = -3.14 * 5 / 1800  // Rotate upward
    }
})

// Keyboard input (desktop)
addEventListener("keypress", (e)=>{
    if(e.key == " " && display.scene == 0){
        bird.gravity = -0.1    // Same jump mechanics
    }
})
Enter fullscreen mode Exit fullscreen mode

Step 5: Pause Function Logic

The paus() function toggles between paused and playing states. A boolean variable pp tracks the pause state (false = playing, true = paused). When paused (pp == false), the button text changes to "<|", display.scene = 2 switches to pause scene, and clearInterval(gj) stops the scoring timer. When unpaused, the button changes back to "||", display.scene = 0 resumes gameplay, and a new interval restarts scoring. This creates a complete pause system.

let pp = false  // Global pause state

function paus(){
    if(pp == false){
        // Pause the game
        document.getElementById("ppp").innerHTML = "<|"
        display.scene = 2      // Enter pause scene
        pp = true
        clearInterval(gj)      // Stop scoring
    } else {
        // Resume the game
        document.getElementById("ppp").innerHTML = "||"
        display.scene = 0      // Return to gameplay
        pp = false
        // Restart scoring interval
        gj = setInterval(() => {
            num++
            if(num > localStorage.hs){
                localStorage.hs = num
            }
        }, 1000)
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Restart Logic in Both Input Methods

Both mouse and keyboard listeners include restart logic when display.scene == 1 (game over). The restart sequence: sets display.scene = 0, resets bird.y = 0, and repositions all six pipes to their starting X positions (700, 933, 1166 pixels). Pipe speeds reset to -1, score num = 0, and a new scoring interval starts. The clearInterval(gj) in the collision detection stops the old timer before restart creates a new one, preventing multiple intervals running simultaneously.

// Restart code (same in both input handlers)
else if(display.scene == 1){
    display.scene = 0
    bird.y = 0
    tt.x = 700           // Reset first pipe
    tt2.x = 700 + 233    // Reset second pipe
    tt3.x = 700 + 466    // Reset third pipe
    bb.x = 700           // Reset bottom pipes
    bb2.x = 700 + 233
    bb3.x = 700 + 466
    tt.speedX = -1       // Reset speed
    num = 0              // Reset score
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Pause Text Component

A text component named pause displays the pause symbol "||" at position (675, 25) - top right corner near the button. It uses 24px impact font in red color. Unlike game objects, this component is always visible because it's never removed from the display. The actual button is an HTML <button> element that overlays the canvas, while the text component is drawn by the TCJSGame engine. This dual approach ensures the pause indicator is visible even if the button has styling issues.

let pause = new Component("24px","impact","red", 675, 25,"text")
pause.text = "||"           // Pause symbol
display.add(pause)          // Add to default scene

// In paus() function, button text changes
document.getElementById("ppp").innerHTML = "<|"  // Play symbol when paused
Enter fullscreen mode Exit fullscreen mode

Step 8: Background in All Three Scenes

The background component hh is added to scenes 0, 1, and 2 using display.add(hh), display.add(hh, 1), and display.add(hh, 2). This ensures that regardless of the game state (playing, game over, or paused), players always see the same background image. Without this, switching to pause scene (2) would show a blank canvas. This creates visual consistency and professional polish. The background is 700x300 pixels matching the canvas size.

// Background appears in ALL scenes
display.add(hh)       // Scene 0 - Playing
display.add(hh, 1)    // Scene 1 - Game Over  
display.add(hh, 2)    // Scene 2 - Paused

// Without scene 2 background, pause screen would be empty!
Enter fullscreen mode Exit fullscreen mode

Step 9: Collision Detection During Gameplay

The update() function runs 50 times per second. It checks collisions between the bird and all six pipes using bird.crashWith(e). When a collision occurs, display.scene = 1 switches to game over, and clearInterval(gj) stops the scoring timer to prevent score increases after death. The ground collision uses bird.hitBottom() which stops the bird at the canvas bottom. The ceiling boundary if(bird.y <= 0){ bird.y = 0 } prevents flying above the top edge. This comprehensive collision system makes the game fair and responsive.

function update(){
    iii=0
    cc.forEach((e)=>{
        iii+=1
        // Collision detection
        if(bird.crashWith(e)){
            display.scene = 1    // Game over
            clearInterval(gj)    // Stop scoring
        }
        // Pipe repositioning (off-screen)
        if(e.x < -30){
            e.x = 700 - num
            // Random height for top pipes
            if(e.y == 0){
                e.height = Math.random() * 200
            }
        }
    })
    bird.hitBottom()  // Check ground collision
}
Enter fullscreen mode Exit fullscreen mode

Step 10: Score Interval Management

The scoring system uses setInterval() that increments num every 1000 milliseconds (1 second). When the game pauses, clearInterval(gj) stops this timer. When unpausing, a new interval is created. This prevents score accumulation while paused. The high score check if(nhs <= num) updates localStorage only when the current score exceeds the saved high score. The game over text dynamically shows the high score using localStorage.hs. Two interval variables exist: gj for scoring and another for pipe speed increases every 3 seconds.

// Scoring interval
gj = setInterval(() => {
    num++                              // Increase score
    nhs = Number(localStorage.hs)     // Get high score
    if (nhs <= num) {                  // Check if beat record
        localStorage.hs = num          // Save new record
    }
}, 1000)

// Difficulty interval (speeds up pipes)
setInterval(()=>{
    tt.speedX -= 0.1    // Pipes scroll faster
    tt2.speedX -= 0.1
    tt3.speedX -= 0.1
    bb.speedX -= 0.1
    bb2.speedX -= 0.1
    bb3.speedX -= 0.1
}, 3000)
Enter fullscreen mode Exit fullscreen mode

New Features Summary

Feature How It Works
Pause Button Fixed position button toggles scene 2
Three Scenes 0=Game, 1=Game Over, 2=Paused
Sound Support Built-in Sound class for MP3 files
Mouse Input Click/tap works for mobile devices
Dual Controls Both mouse and keyboard supported

Customization Examples

// Change button position
button{
    position: fixed;
    bottom: 20px;    /* Move to bottom */
    left: 20px;      /* Move to left */
}

// Add sound effects to jump
if(display.scene == 0){
    let jumpSound = new Sound("jump.wav")
    jumpSound.play()
    bird.gravity = -0.1
}

// Add resume text in pause scene
let resumeText = new Component("20px","Arial","white",300,150,"text")
resumeText.text = "PAUSED - Click || to resume"
display.add(resumeText, 2)  // Only in pause scene
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

Issue Solution
Sound won't play Browser may block autoplay - add user interaction first
Pause button not working Check button ID matches getElementById("ppp")
Scene 2 shows nothing Add background components to scene 2
Score increments while paused Ensure clearInterval(gj) is called in pause function

Conclusion

You've built an enhanced Flappy Bird game with pause functionality, mouse/touch support, and sound integration! The three-scene system (0=gameplay, 1=game over, 2=paused) creates a professional game feel. The pause button toggles between states and pauses scoring. Dual input methods (mouse + keyboard) make the game playable on both desktop and mobile devices. Use the sound system to add background music and sound effects. Experiment with different button positions, add more scenes for menus, or create custom pause graphics. The complete code runs in any modern browser with full mobile support!


Built with TCJSGame โ€“ Simple JavaScript Game Development

Top comments (0)