<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Flappy Bird Fixed</title>
<style>
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #222;
font-family: 'Arial', sans-serif;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
touch-action: none; /* Prevents zoom on mobile */
}
#game-container {
position: relative;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
}
canvas {
background-color: #87CEEB;
display: block;
border-radius: 4px;
}
/* UI Overlays */
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
#score-display {
position: absolute;
top: 40px;
left: 50%;
transform: translateX(-50%);
font-size: 48px;
font-weight: bold;
color: white;
text-shadow: 3px 3px 0 #000;
z-index: 10;
font-family: 'Courier New', Courier, monospace;
}
#start-screen, #game-over-screen {
background: rgba(0, 0, 0, 0.7);
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
pointer-events: auto; /* Allow clicks */
cursor: pointer;
}
h1 { margin: 0 0 10px 0; font-size: 60px; text-shadow: 4px 4px 0 #000; line-height: 1; }
p { font-size: 22px; margin: 10px 0; color: #ddd; }
.btn {
background: linear-gradient(to bottom, #ffeb3b, #fbc02d);
border: 3px solid #fff;
color: #333;
padding: 15px 40px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
border-radius: 8px;
box-shadow: 0 6px 0 #c49000, 0 10px 10px rgba(0,0,0,0.3);
text-transform: uppercase;
transition: all 0.1s;
margin-top: 10px;
}
.btn:active {
transform: translateY(4px);
box-shadow: 0 2px 0 #c49000, 0 4px 4px rgba(0,0,0,0.3);
}
.score-board {
background: #ded895;
border: 2px solid #543847;
padding: 20px 40px;
border-radius: 10px;
box-shadow: 4px 4px 0 #543847;
margin-bottom: 30px;
display: none;
border: 4px solid #543847;
}
.score-board h2 { margin: 0 0 5px 0; color: #e86101; font-size: 40px; }
.score-board span { font-size: 20px; color: #543847; display: block; text-align: center; }
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas" width="400" height="600"></canvas>
<div id="score-display">0</div>
<div id="ui-layer">
<div id="start-screen">
<h1>FLAPPY<br>BIRD</h1>
<p>Tap or Press Space to Start</p>
<button class="btn" style="display:none;" id="touch-instruction">Click to Start</button>
</div>
<div id="game-over-screen">
<h1>GAME OVER</h1>
<div class="score-board" id="score-board">
<h2>Score: <span id="final-score">0</span></h2>
</div>
<button class="btn" id="restart-btn">Try Again</button>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// --- Constants ---
const GRAVITY = 0.35; // Slightly harder gravity for better feel
const JUMP_STRENGTH = -6.5;
const PIPE_GAP = 180;
const PIPE_WIDTH = 60;
const PIPE_SPEED = 2.5;
const PIPE_SPAWN_RATE = 100;
// --- State ---
let frames = 0;
let score = 0;
let isGameOver = false;
let isPlaying = false;
let gameLoopId;
const ui = {
startScreen: document.getElementById('start-screen'),
gameOverScreen: document.getElementById('game-over-screen'),
scoreDisplay: document.getElementById('score-display'),
scoreBoard: document.getElementById('score-board'),
finalScore: document.getElementById('final-score'),
restartBtn: document.getElementById('restart-btn')
};
const bird = {
x: 80,
y: 150,
radius: 12, // Fixed radius
velocity: 0,
draw: function() {
// Rotate bird based on velocity for visual flair
let angle = Math.min(Math.PI / 4, Math.max(-Math.PI / 4, (this.velocity * 0.1)));
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(angle);
// Draw Body
ctx.fillStyle = '#FFD700';
ctx.beginPath();
ctx.arc(0, 0, this.radius, 0, Math.PI * 2);
ctx.fill();
// Eye
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(4, -4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(6, -4, 2, 0, Math.PI * 2);
ctx.fill();
// Wing
ctx.fillStyle = '#FFA500';
ctx.beginPath();
ctx.arc(-4, 4, 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
},
update: function() {
this.velocity += GRAVITY;
this.y += this.velocity;
},
jump: function() {
this.velocity = JUMP_STRENGTH;
},
reset: function() {
this.y = 150;
this.velocity = 0;
}
};
let pipes = [];
function createPipe() {
const minHeight = 50;
const maxPos = canvas.height - PIPE_GAP - minHeight;
const topHeight = Math.floor(Math.random() * (maxPos - minHeight + 1)) + minHeight;
pipes.push({
x: canvas.width,
topHeight: topHeight,
bottomY: topHeight + PIPE_GAP,
width: PIPE_WIDTH,
passed: false
});
}
function drawPipes() {
ctx.fillStyle = '#2ecc71';
ctx.strokeStyle = '#27ae60';
ctx.lineWidth = 2;
pipes.forEach(pipe => {
// Top Pipe
ctx.fillRect(pipe.x, 0, pipe.width, pipe.topHeight);
ctx.strokeRect(pipe.x, 0, pipe.width, pipe.topHeight);
// Bottom Pipe
ctx.fillRect(pipe.x, pipe.bottomY, pipe.width, canvas.height - pipe.bottomY);
ctx.strokeRect(pipe.x, pipe.bottomY, pipe.width, canvas.height - pipe.bottomY);
// Cap
const capH = 20;
ctx.fillStyle = '#27ae60';
ctx.fillRect(pipe.x - 2, pipe.topHeight - capH, pipe.width + 4, capH);
ctx.fillRect(pipe.x - 2, pipe.bottomY, pipe.width + 4, capH);
});
}
function updatePipes() {
if (frames % PIPE_SPAWN_RATE === 0) createPipe();
for (let i = 0; i < pipes.length; i++) {
let p = pipes[i];
p.x -= PIPE_SPEED;
// --- FIXED COLLISION DETECTION ---
// Treat bird as a circle of radius 'bird.radius'
// Pipe is a rect: x, x+w, y_top, y_bottom
let birdLeft = bird.x - bird.radius;
let birdRight = bird.x + bird.radius;
let birdTop = bird.y - bird.radius;
let birdBottom = bird.y + bird.radius;
// Horizontal Overlap
if (birdRight > p.x && birdLeft < p.x + p.width) {
// Vertical Overlap (Hit Top Pipe OR Hit Bottom Pipe)
if (birdTop < p.topHeight || birdBottom > p.bottomY) {
gameOver();
}
}
// --- FIXED SCORING ---
// If pipe is off screen to the left, and hasn't been scored
if (p.x + p.width < 0 && !p.passed) {
score++;
p.passed = true;
ui.scoreDisplay.innerText = score;
}
if (p.x < -60) {
pipes.shift();
i--;
}
}
}
function checkCollisions() {
// Floor
if (bird.y + bird.radius >= canvas.height) {
gameOver();
}
// Ceiling
if (bird.y - bird.radius <= 0) {
bird.y = bird.radius;
bird.velocity = 0;
}
}
function drawBackground() {
// Clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Floor
ctx.fillStyle = '#654321';
ctx.fillRect(0, canvas.height - 10, canvas.width, 10);
// Clouds
ctx.fillStyle = "rgba(255, 255, 255, 0.6)";
for(let i=0; i<4; i++) {
let x = (frames * 0.5 + i * 150) % (canvas.width + 100) - 50;
let y = (i * 40) + 20;
let scale = 1 + (Math.sin(frames * 0.02 + i) * 0.1);
let size = 30 * scale;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.arc(x + 20*scale, y - 10*scale, size * 1.2, 0, Math.PI * 2);
ctx.arc(x + 40*scale, y, size, 0, Math.PI * 2);
ctx.fill();
}
}
function update() {
if (!isPlaying) return;
bird.update();
updatePipes();
checkCollisions();
frames++;
}
function draw() {
drawBackground();
drawPipes();
bird.draw();
}
function loop() {
update();
draw();
if (isPlaying || !isGameOver) {
gameLoopId = requestAnimationFrame(loop);
}
}
function startGame() {
isPlaying = true;
isGameOver = false;
// UI
ui.startScreen.style.display = 'none';
ui.gameOverScreen.style.display = 'none';
ui.scoreBoard.style.display = 'none';
// Logic
bird.reset();
pipes = [];
score = 0;
frames = 0;
ui.scoreDisplay.innerText = '0';
loop();
}
function gameOver() {
isPlaying = false;
isGameOver = true;
cancelAnimationFrame(gameLoopId);
ui.finalScore.innerText = score;
ui.gameOverScreen.style.display = 'flex';
ui.scoreBoard.style.display = 'block';
}
// --- Input Handling ---
function handleInput(e) {
if (e.type === 'keydown' && e.code === 'Space') e.preventDefault();
if (isPlaying) {
bird.jump();
} else if (!isGameOver) {
// Click on start screen (or any click if game hasn't started)
startGame();
}
}
// Keyboard
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') handleInput(e);
});
// Mouse / Touch
window.addEventListener('mousedown', handleInput);
window.addEventListener('touchstart', (e) => {
// 阻止缩放/滚动
// e.preventDefault();
handleInput(e);
}, {passive: false});
// 用户界面交互
ui.restartBtn.addEventListener('click', (e) => {
e.stopPropagation();
startGame();
});
// 允许点击游戏结束覆盖层以重新开始
ui.gameOverScreen.addEventListener('mousedown', (e) => {
if(e.target === ui.gameOverScreen) startGame();
});
ui.gameOverScreen.addEventListener('touchstart', (e) => {
if(e.target === ui.gameOverScreen) {
e.preventDefault();
startGame();
}
});
// 初始绘制
drawBackground();
bird.reset();
bird.draw();
</script>
</body>
</html>