console
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const CONFIG = {
BALL_COUNT: 10,
RADIUS: 20,
PLAYER_COLOR: '#3498db',
OTHER_COLOR: '#e74c3c',
BASE_SPEED: 6,
MAX_HITS: 5
};
class GameState {
constructor() {
this.reset();
}
reset() {
this.isPlaying = false;
this.mouseControl = false;
this.countdown = 0;
this.balls = [];
this.collisions = 0;
}
startGame() {
this.isPlaying = false;
this.mouseControl = true;
this.countdown = 3;
this.collisions = 0;
this.balls = [];
}
gameOver() {
this.isPlaying = false;
this.mouseControl = false;
}
}
const gameState = new GameState();
class Ball {
constructor(x, y, isPlayer = false) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.radius = CONFIG.RADIUS;
this.color = isPlayer ? CONFIG.PLAYER_COLOR : CONFIG.OTHER_COLOR;
this.isPlayer = isPlayer;
this.lastCollision = 0;
if (!isPlayer) {
const angle = Math.random() * Math.PI * 2;
this.vx = Math.cos(angle) * CONFIG.BASE_SPEED;
this.vy = Math.sin(angle) * CONFIG.BASE_SPEED;
}
}
update() {
if (this.isPlayer) return;
const boundaryX = canvas.width - this.radius;
const boundaryY = canvas.height - this.radius;
if (this.x < this.radius || this.x > boundaryX) {
this.vx *= -1;
this.x = Math.max(this.radius, Math.min(this.x, boundaryX));
}
if (this.y < this.radius || this.y > boundaryY) {
this.vy *= -1;
this.y = Math.max(this.radius, Math.min(this.y, boundaryY));
}
this.x += this.vx;
this.y += this.vy;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
}
function initGame() {
gameState.balls = [];
const player = new Ball(canvas.width/2, canvas.height/2, true);
gameState.balls.push(player);
for (let i = 1; i < CONFIG.BALL_COUNT; i++) {
let isValid, newBall;
let attempts = 0;
do {
newBall = new Ball(
CONFIG.RADIUS + Math.random()*(canvas.width - CONFIG.RADIUS*2),
CONFIG.RADIUS + Math.random()*(canvas.height - CONFIG.RADIUS*2)
);
isValid = gameState.balls.every(b => {
const dx = b.x - newBall.x;
const dy = b.y - newBall.y;
return Math.sqrt(dx**2 + dy**2) >= CONFIG.RADIUS*2;
});
} while (!isValid && ++attempts < 100);
if (isValid) gameState.balls.push(newBall);
}
}
function processCollisions() {
const now = Date.now();
for (let i = 0; i < gameState.balls.length; i++) {
for (let j = i+1; j < gameState.balls.length; j++) {
const a = gameState.balls[i];
const b = gameState.balls[j];
const dx = b.x - a.x;
const dy = b.y - a.y;
const distance = Math.sqrt(dx**2 + dy**2);
const minDist = a.radius + b.radius;
if (distance < minDist) {
if (a.isPlayer !== b.isPlayer && now - a.lastCollision > 100) {
gameState.collisions++;
a.lastCollision = now;
b.lastCollision = now;
}
const nx = dx / distance;
const ny = dy / distance;
const dvx = b.vx - a.vx;
const dvy = b.vy - a.vy;
const impulse = (2 * (dvx*nx + dvy*ny)) / 2;
a.vx += impulse * nx;
a.vy += impulse * ny;
b.vx -= impulse * nx;
b.vy -= impulse * ny;
const overlap = (minDist - distance)/2;
a.x -= overlap * nx;
a.y -= overlap * ny;
b.x += overlap * nx;
b.y += overlap * ny;
}
}
}
if (gameState.collisions >= CONFIG.MAX_HITS) {
resetGame();
}
}
function resetGame() {
gameState.isPlaying = false;
gameState.mouseControl = false;
gameState.balls = [];
ui.elements.countdown.textContent = `超过${CONFIG.MAX_HITS}次碰撞!`;
ui.elements.countdown.style.opacity = '1';
ui.elements.startBtn.style.opacity = '0';
ui.elements.startBtn.style.pointerEvents = 'none';
setTimeout(() => {
ui.elements.countdown.style.opacity = '0';
ui.elements.startBtn.style.opacity = '1';
ui.elements.startBtn.style.pointerEvents = 'auto';
canvas.style.cursor = 'default';
}, 2000);
}
class UIManager {
constructor() {
this.elements = {
startBtn: this._createStartButton(),
countdown: this._createCountdownElement(),
scoreBoard: this._createScoreBoard()
};
}
_createStartButton() {
const btn = document.createElement('div');
btn.textContent = '开始游戏';
btn.style.cssText = `
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font: bold 48px Arial;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
cursor: pointer;
transition: opacity 0.5s;
user-select: none;
`;
document.body.appendChild(btn);
return btn;
}
_createCountdownElement() {
const el = document.createElement('div');
el.style.cssText = `
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font: bold 100px Arial;
color: white;
text-shadow: 4px 4px 8px rgba(0,0,0,0.7);
opacity: 0;
pointer-events: none;
user-select: none;
`;
document.body.appendChild(el);
return el;
}
_createScoreBoard() {
const board = document.createElement('div');
board.style.cssText = `
position: fixed;
top: 20px;
left: 20px;
font: bold 24px Arial;
color: ${CONFIG.PLAYER_COLOR};
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
user-select: none;
`;
document.body.appendChild(board);
return board;
}
updateScore(current, max) {
this.elements.scoreBoard.textContent = `碰撞次数: ${current}/${max}`;
}
}
const ui = new UIManager();
canvas.addEventListener('mousemove', e => {
if (!gameState.mouseControl) return;
const rect = canvas.getBoundingClientRect();
const player = gameState.balls.find(b => b.isPlayer);
player.x = Math.max(CONFIG.RADIUS,
Math.min(e.clientX - rect.left, canvas.width - CONFIG.RADIUS));
player.y = Math.max(CONFIG.RADIUS,
Math.min(e.clientY - rect.top, canvas.height - CONFIG.RADIUS));
});
function startGame() {
gameState.collisions = 0;
ui.updateScore(0, CONFIG.MAX_HITS);
ui.elements.startBtn.style.opacity = '0';
ui.elements.startBtn.style.pointerEvents = 'none';
canvas.style.cursor = 'none';
gameState.isPlaying = false;
gameState.mouseControl = true;
gameState.countdown = 3;
initGame();
ui.elements.countdown.style.opacity = '1';
const countdownStep = () => {
ui.elements.countdown.textContent = gameState.countdown;
if (gameState.countdown > 0) {
gameState.countdown--;
setTimeout(countdownStep, 1000);
} else {
ui.elements.countdown.textContent = 'GO!';
gameState.isPlaying = true;
setTimeout(() => {
ui.elements.countdown.style.opacity = '0';
}, 100);
}
}
countdownStep();
animate();
}
function animate() {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ui.updateScore(gameState.collisions, CONFIG.MAX_HITS);
gameState.balls.forEach(ball => {
if (gameState.isPlaying) ball.update();
ball.draw();
});
if (gameState.isPlaying) {
processCollisions();
}
requestAnimationFrame(animate);
}
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initGame();
});
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ui.elements.startBtn.addEventListener('click', startGame);
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<meta name="robots" content="noindex, nofollow"/>
<meta name="googlebot" content="noindex, nofollow"/>
<title>Avoid The Red Ball</title>
<link rel="stylesheet" type="text/css" href="main.css"/>
</head>
<body>
<canvas>
<h1>提示:你的浏览器不支持canvas标签!</h1>
</canvas>
<script type="application/javascript" src="main.js"></script>
</body>
</html>
html, body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
}