1.介绍
复刻经典小游戏——水果忍者
2.预览
3.代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fruit Ninja Web Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
font-family: Arial, sans-serif;
touch-action: none;
}
#gameCanvas {
background-color: #87CEEB;
display: block;
margin: 0 auto;
max-width: 100%;
max-height: 100vh;
}
#gameContainer {
position: relative;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#startScreen, #gameOverScreen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.7);
color: white;
z-index: 10;
}
#gameOverScreen {
display: none;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 5px;
}
.scoreBoard {
position: absolute;
top: 10px;
left: 10px;
color: white;
font-size: 24px;
z-index: 5;
text-shadow: 2px 2px 4px black;
}
.comboText {
position: absolute;
color: yellow;
font-size: 36px;
font-weight: bold;
text-shadow: 2px 2px 4px black;
opacity: 1;
transition: opacity 1s, transform 1s;
z-index: 5;
}
</style>
</head>
<body>
<div id="gameContainer">
<div id="startScreen">
<h1>Fruit Ninja Web Demo</h1>
<p>Slice fruits with your mouse or finger to score points!</p>
<p>Avoid bombs or you'll lose!</p>
<button id="startButton">Start Game</button>
</div>
<canvas id="gameCanvas"></canvas>
<div id="gameOverScreen">
<h1>Game Over</h1>
<p>Your Score: <span id="finalScore">0</span></p>
<button id="restartButton">Play Again</button>
</div>
<div class="scoreBoard">Score: <span id="scoreDisplay">0</span></div>
</div>
<script>
// Game variables
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const startScreen = document.getElementById('startScreen');
const gameOverScreen = document.getElementById('gameOverScreen');
const startButton = document.getElementById('startButton');
const restartButton = document.getElementById('restartButton');
const scoreDisplay = document.getElementById('scoreDisplay');
const finalScore = document.getElementById('finalScore');
let score = 0;
let gameActive = false;
let gameObjects = [];
let sliceTrail = [];
let sliceActive = false;
let lastTimestamp = 0;
let spawnTimer = 0;
let comboCount = 0;
let comboTimer = 0;
// Resize canvas
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
// Initialize game
function initGame() {
resizeCanvas();
score = 0;
gameObjects = [];
sliceTrail = [];
sliceActive = false;
scoreDisplay.textContent = score;
gameActive = true;
lastTimestamp = 0;
spawnTimer = 0;
// Start animation
requestAnimationFrame(gameLoop);
}
// Game objects classes
class GameObject {
constructor(x, y, type) {
this.x = x;
this.y = y;
this.type = type;
this.sliced = false;
this.velocityX = Math.random() * 8 - 4;
this.velocityY = -15 - Math.random() * 5;
this.gravity = 0.5;
this.rotation = 0;
this.rotationSpeed = Math.random() * 0.1 - 0.05;
this.size = 40 + Math.random() * 20;
if (this.type === 'banana') {
this.color = '#FFD700';
} else if (this.type === 'bomb') {
this.color = '#333';
this.size = 30 + Math.random() * 10;
} else if (this.type === 'pomegranate') {
this.color = '#FF4500';
this.size = 50 + Math.random() * 10;
} else if (this.type === 'watermelon') {
this.color = '#32CD32';
this.size = 60 + Math.random() * 10;
} else {
// Default apple
this.color = '#FF0000';
}
this.slicedColor1 = this.color;
this.slicedColor2 = this.type === 'watermelon' ? '#FF6347' : this.color;
// For slice animation
this.sliceAngle = 0;
this.slicePart1 = { x: 0, y: 0, vx: 0, vy: 0, rotation: 0 };
this.slicePart2 = { x: 0, y: 0, vx: 0, vy: 0, rotation: 0 };
}
update() {
if (this.sliced) {
// Update sliced parts
this.slicePart1.x += this.slicePart1.vx;
this.slicePart1.y += this.slicePart1.vy;
this.slicePart1.vy += this.gravity;
this.slicePart1.rotation += 0.05;
this.slicePart2.x += this.slicePart2.vx;
this.slicePart2.y += this.slicePart2.vy;
this.slicePart2.vy += this.gravity;
this.slicePart2.rotation += 0.05;
return this.slicePart1.y < canvas.height && this.slicePart2.y < canvas.height;
} else {
// Update normal object
this.x += this.velocityX;
this.y += this.velocityY;
this.velocityY += this.gravity;
this.rotation += this.rotationSpeed;
return this.y < canvas.height + 100;
}
}
draw() {
ctx.save();
if (this.sliced) {
// Draw first slice part
ctx.save();
ctx.translate(this.slicePart1.x, this.slicePart1.y);
ctx.rotate(this.slicePart1.rotation);
ctx.beginPath();
ctx.arc(0, 0, this.size / 2, 0, Math.PI, false);
ctx.fillStyle = this.slicedColor1;
ctx.fill();
if (this.type === 'watermelon') {
ctx.beginPath();
ctx.arc(0, 0, this.size / 2 - 5, 0, Math.PI, false);
ctx.fillStyle = this.slicedColor2;
ctx.fill();
}
ctx.restore();
// Draw second slice part
ctx.save();
ctx.translate(this.slicePart2.x, this.slicePart2.y);
ctx.rotate(this.slicePart2.rotation);
ctx.beginPath();
ctx.arc(0, 0, this.size / 2, Math.PI, 2 * Math.PI, false);
ctx.fillStyle = this.slicedColor1;
ctx.fill();
if (this.type === 'watermelon') {
ctx.beginPath();
ctx.arc(0, 0, this.size / 2 - 5, Math.PI, 2 * Math.PI, false);
ctx.fillStyle = this.slicedColor2;
ctx.fill();
}
ctx.restore();
} else {
// Draw normal object
ctx.translate(this.x, this.y);
ctx.rotate(this.rotation);
if (this.type === 'bomb') {
// Draw bomb
ctx.beginPath();
ctx.arc(0, 0, this.size / 2, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
// Draw fuse
ctx.beginPath();
ctx.moveTo(0, -this.size / 2);
ctx.quadraticCurveTo(10, -this.size / 2 - 15, 20, -this.size / 2 - 10);
ctx.lineWidth = 3;
ctx.strokeStyle = '#8B4513';
ctx.stroke();
} else if (this.type === 'banana') {
// Draw banana
ctx.beginPath();
ctx.arc(0, 0, this.size / 2, 0.3 * Math.PI, 1.7 * Math.PI);
ctx.lineWidth = this.size / 2;
ctx.strokeStyle = this.color;
ctx.stroke();
} else if (this.type === 'watermelon') {
// Draw watermelon
ctx.beginPath();
ctx.arc(0, 0, this.size / 2, 0, 2 * Math.PI);
ctx.fillStyle = '#32CD32';
ctx.fill();
// Inner part
ctx.beginPath();
ctx.arc(0, 0, this.size / 2 - 5, 0, 2 * Math.PI);
ctx.fillStyle = '#FF6347';
ctx.fill();
// Seeds
ctx.fillStyle = 'black';
for (let i = 0; i < 8; i++) {
const angle = i * (Math.PI / 4);
const distance = this.size / 4;
ctx.beginPath();
ctx.ellipse(
Math.cos(angle) * distance,
Math.sin(angle) * distance,
3, 5, angle, 0, 2 * Math.PI
);
ctx.fill();
}
} else if (this.type === 'pomegranate') {
// Draw pomegranate
ctx.beginPath();
ctx.arc(0, 0, this.size / 2, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
// Crown
ctx.beginPath();
ctx.moveTo(-10, -this.size / 2);
ctx.lineTo(10, -this.size / 2);
ctx.lineTo(0, -this.size / 2 - 10);
ctx.closePath();
ctx.fillStyle = '#8B4513';
ctx.fill();
} else {
// Draw apple
ctx.beginPath();
ctx.arc(0, 0, this.size / 2, 0, 2 * Math.PI);
ctx.fillStyle = this.color;
ctx.fill();
// Stem
ctx.beginPath();
ctx.moveTo(0, -this.size / 2);
ctx.lineTo(0, -this.size / 2 - 7);
ctx.lineWidth = 3;
ctx.strokeStyle = '#8B4513';
ctx.stroke();
}
}
ctx.restore();
}
checkSlice(slicePath) {
if (this.sliced) return false;
// Check if the slice path intersects with the object
for (let i = 1; i < slicePath.length; i++) {
const x1 = slicePath[i-1].x;
const y1 = slicePath[i-1].y;
const x2 = slicePath[i].x;
const y2 = slicePath[i].y;
// Calculate distance from line segment to center of object
const distance = distToSegment(this.x, this.y, x1, y1, x2, y2);
if (distance < this.size / 2) {
// Calculate slice angle
this.sliceAngle = Math.atan2(y2 - y1, x2 - x1);
// Set the slice parts
this.slicePart1.x = this.x;
this.slicePart1.y = this.y;
this.slicePart1.vx = this.velocityX - 1 + Math.random() * 2;
this.slicePart1.vy = this.velocityY - 2;
this.slicePart2.x = this.x;
this.slicePart2.y = this.y;
this.slicePart2.vx = this.velocityX + 1 + Math.random() * 2;
this.slicePart2.vy = this.velocityY - 2;
this.sliced = true;
// Handle special fruits
if (this.type === 'bomb') {
return 'bomb';
} else if (this.type === 'banana') {
return 'banana';
} else if (this.type === 'pomegranate') {
return 'pomegranate';
} else {
return 'fruit';
}
}
}
return false;
}
}
// Helper function to calculate distance from point to line segment
function sqr(x) { return x * x; }
function dist2(v, w) { return sqr(v.x - w.x) + sqr(v.y - w.y); }
function distToSegmentSquared(p, v, w) {
const l2 = dist2(v, w);
if (l2 === 0) return dist2(p, v);
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
t = Math.max(0, Math.min(1, t));
return dist2(p, {
x: v.x + t * (w.x - v.x),
y: v.y + t * (w.y - v.y)
});
}
function distToSegment(px, py, x1, y1, x2, y2) {
return Math.sqrt(distToSegmentSquared(
{x: px, y: py},
{x: x1, y: y1},
{x: x2, y: y2}
));
}
// Spawn new game objects
function spawnObjects() {
const types = ['apple', 'watermelon', 'banana', 'pomegranate'];
const x = Math.random() * canvas.width;
const y = canvas.height + 20;
// 20% chance to spawn a bomb
if (Math.random() < 0.2) {
gameObjects.push(new GameObject(x, y, 'bomb'));
} else {
const type = types[Math.floor(Math.random() * types.length)];
gameObjects.push(new GameObject(x, y, type));
}
}
// Show combo text animation
function showComboText(count) {
const comboText = document.createElement('div');
comboText.className = 'comboText';
comboText.textContent = `COMBO x${count}!`;
comboText.style.left = `${canvas.width / 2 - 100}px`;
comboText.style.top = `${canvas.height / 2 - 50}px`;
document.getElementById('gameContainer').appendChild(comboText);
setTimeout(() => {
comboText.style.opacity = '0';
comboText.style.transform = 'translateY(-50px)';
setTimeout(() => {
comboText.remove();
}, 1000);
}, 10);
}
// Spawn small fruits (for pomegranate effect)
function spawnSmallFruits(x, y) {
const count = 5 + Math.floor(Math.random() * 5);
for (let i = 0; i < count; i++) {
const fruit = new GameObject(x, y, 'apple');
fruit.size = 15 + Math.random() * 10;
fruit.velocityX = Math.random() * 10 - 5;
fruit.velocityY = -10 - Math.random() * 5;
gameObjects.push(fruit);
}
}
// Game loop
function gameLoop(timestamp) {
if (!gameActive) return;
// Calculate delta time
const deltaTime = timestamp - lastTimestamp;
lastTimestamp = timestamp;
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Spawn objects
spawnTimer += deltaTime;
if (spawnTimer > 1000) {
spawnObjects();
spawnTimer = 0;
}
// Draw slice trail
if (sliceActive && sliceTrail.length > 1) {
ctx.beginPath();
ctx.moveTo(sliceTrail[0].x, sliceTrail[0].y);
for (let i = 1; i < sliceTrail.length; i++) {
ctx.lineTo(sliceTrail[i].x, sliceTrail[i].y);
}
ctx.strokeStyle = 'white';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
// Trail glow effect
ctx.shadowColor = '#FFF';
ctx.shadowBlur = 15;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 15;
ctx.stroke();
ctx.shadowBlur = 0;
}
// Update and draw game objects
let fruitSlicedThisFrame = 0;
for (let i = gameObjects.length - 1; i >= 0; i--) {
const obj = gameObjects[i];
// Check for slicing
if (sliceActive && sliceTrail.length > 3 && !obj.sliced) {
const sliceResult = obj.checkSlice(sliceTrail);
if (sliceResult) {
if (sliceResult === 'bomb') {
// Game over if bomb is sliced
gameActive = false;
finalScore.textContent = score;
gameOverScreen.style.display = 'flex';
return;
} else {
fruitSlicedThisFrame++;
if (sliceResult === 'fruit') {
score += 10;
} else if (sliceResult === 'banana') {
// Banana gives double points for a short time
score += 20;
} else if (sliceResult === 'pomegranate') {
score += 30;
// Spawn small fruits
spawnSmallFruits(obj.x, obj.y);
}
scoreDisplay.textContent = score;
}
}
}
const isVisible = obj.update();
if (isVisible) {
obj.draw();
} else {
gameObjects.splice(i, 1);
}
}
// Handle combo
if (fruitSlicedThisFrame > 1) {
comboCount = fruitSlicedThisFrame;
comboTimer = 0;
// Add combo bonus
const comboBonus = comboCount * 5;
score += comboBonus;
scoreDisplay.textContent = score;
// Show combo text
showComboText(comboCount);
} else if (fruitSlicedThisFrame === 1) {
comboTimer += deltaTime;
if (comboTimer > 500) {
comboCount = 0;
}
}
// Reduce slice trail gradually
if (sliceTrail.length > 0 && !sliceActive) {
sliceTrail.shift();
}
// Continue animation
requestAnimationFrame(gameLoop);
}
// Event listeners for mouse/touch
canvas.addEventListener('mousedown', (e) => {
sliceActive = true;
sliceTrail = [];
sliceTrail.push({
x: e.clientX,
y: e.clientY
});
});
canvas.addEventListener('mousemove', (e) => {
if (sliceActive) {
sliceTrail.push({
x: e.clientX,
y: e.clientY
});
// Limit trail length
if (sliceTrail.length > 20) {
sliceTrail.shift();
}
}
});
canvas.addEventListener('mouseup', () => {
sliceActive = false;
});
canvas.addEventListener('mouseleave', () => {
sliceActive = false;
});
// Touch events
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
sliceActive = true;
sliceTrail = [];
sliceTrail.push({
x: e.touches[0].clientX,
y: e.touches[0].clientY
});
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (sliceActive) {
sliceTrail.push({
x: e.touches[0].clientX,
y: e.touches[0].clientY
});
// Limit trail length
if (sliceTrail.length > 20) {
sliceTrail.shift();
}
}
});
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
sliceActive = false;
});
// Button event listeners
startButton.addEventListener('click', () => {
startScreen.style.display = 'none';
initGame();
});
restartButton.addEventListener('click', () => {
gameOverScreen.style.display = 'none';
initGame();
});
// Handle window resize
window.addEventListener('resize', resizeCanvas);
// Initial setup
resizeCanvas();
</script>
</body>
</html>