Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>3D Snake Game</title> | |
<style> | |
body { | |
margin: 0; | |
overflow: hidden; | |
} | |
#score { | |
position: absolute; | |
top: 10px; | |
left: 10px; | |
color: white; | |
font-family: Arial, sans-serif; | |
font-size: 20px; | |
} | |
#gameOver { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
color: white; | |
font-family: Arial, sans-serif; | |
font-size: 36px; | |
display: none; | |
} | |
#bonusActive { | |
position: absolute; | |
top: 40px; | |
left: 10px; | |
color: gold; | |
font-family: Arial, sans-serif; | |
font-size: 20px; | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="score">Score: 0</div> | |
<div id="bonusActive">SPEED BOOST ACTIVE!</div> | |
<div id="gameOver">Game Over! Press Space to restart</div> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script> | |
<script> | |
// Game variables | |
let scene, camera, renderer; | |
let snake = [], | |
food, | |
goldenFood; | |
let direction = { x: 1, y: 0, z: 0 }; | |
let gridSize = 20; | |
let speed = 5; // Moves per second | |
let baseSpeed = 5; // Default speed | |
let lastTime = 0; | |
let score = 0; | |
let isGameOver = false; | |
let bonusActive = false; | |
let bonusEndTime = 0; | |
let goldenFoodChance = 0.2; // 20% chance | |
let bonusSpeedMultiplier = 1.5; // 50% faster | |
let bonusDuration = 5000; // 5 seconds | |
// Camera rotation variables | |
let cameraRotationSpeed = 0.0005; // Speed of rotation | |
let cameraRotationRadius = 40; // Distance from center | |
let cameraRotationAngle = 0; // Current angle | |
let cameraRotationActive = true; // Toggle rotation | |
let cameraHeight = 30; // Height of camera | |
// Initialize the game | |
function init() { | |
// Create scene | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x333333); | |
// Create camera | |
camera = new THREE.PerspectiveCamera( | |
75, | |
window.innerWidth / window.innerHeight, | |
0.1, | |
1000 | |
); | |
// Set initial camera position | |
camera.position.set(0, cameraHeight, cameraRotationRadius); | |
camera.lookAt(0, 0, 0); | |
// Create renderer | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
document.body.appendChild(renderer.domElement); | |
// Add lights | |
const ambientLight = new THREE.AmbientLight(0x404040); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); | |
directionalLight.position.set(10, 20, 15); | |
scene.add(directionalLight); | |
// Add grid for reference | |
const gridHelper = new THREE.GridHelper(gridSize, gridSize); | |
scene.add(gridHelper); | |
// Create snake | |
createSnake(); | |
// Create food | |
createFood(); | |
// Add event listeners | |
document.addEventListener("keydown", handleKeyDown); | |
window.addEventListener("resize", onWindowResize); | |
// Start animation loop | |
animate(); | |
} | |
// Create initial snake | |
function createSnake() { | |
// Start with a single segment | |
const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9); | |
const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); | |
const segment = new THREE.Mesh(geometry, material); | |
segment.position.set(0, 0.5, 0); | |
scene.add(segment); | |
snake.push({ mesh: segment, position: { x: 0, y: 0, z: 0 } }); | |
} | |
// Create food | |
function createFood() { | |
// Remove old food if exists | |
if (food) scene.remove(food.mesh); | |
const geometry = new THREE.SphereGeometry(0.5); | |
const material = new THREE.MeshPhongMaterial({ color: 0xff0000 }); | |
const mesh = new THREE.Mesh(geometry, material); | |
// Random position | |
let position = getRandomPosition(); | |
// Make sure food doesn't spawn on snake | |
while (isPositionOccupied(position)) { | |
position = getRandomPosition(); | |
} | |
mesh.position.set(position.x, 0.5, position.z); | |
scene.add(mesh); | |
food = { mesh: mesh, position: position }; | |
// Random chance to create golden food | |
if (Math.random() < goldenFoodChance) { | |
setTimeout(createGoldenFood, Math.random() * 5000 + 2000); | |
} | |
} | |
// Create golden bonus food | |
function createGoldenFood() { | |
if (isGameOver || goldenFood) return; | |
const geometry = new THREE.SphereGeometry(0.6); | |
const material = new THREE.MeshPhongMaterial({ | |
color: 0xffd700, | |
emissive: 0xffd700, | |
emissiveIntensity: 0.5, | |
}); | |
const mesh = new THREE.Mesh(geometry, material); | |
// Random position | |
let position = getRandomPosition(); | |
// Make sure golden food doesn't spawn on snake or regular food | |
while ( | |
isPositionOccupied(position) || | |
(food && | |
position.x === food.position.x && | |
position.z === food.position.z) | |
) { | |
position = getRandomPosition(); | |
} | |
mesh.position.set(position.x, 0.5, position.z); | |
scene.add(mesh); | |
goldenFood = { mesh: mesh, position: position }; | |
// Make golden food disappear after some time | |
setTimeout(() => { | |
if (goldenFood) { | |
scene.remove(goldenFood.mesh); | |
goldenFood = null; | |
} | |
}, 5000); | |
} | |
// Get random grid position | |
function getRandomPosition() { | |
const halfGrid = Math.floor(gridSize / 2); | |
return { | |
x: Math.floor(Math.random() * gridSize) - halfGrid, | |
y: 0, | |
z: Math.floor(Math.random() * gridSize) - halfGrid, | |
}; | |
} | |
// Check if position is occupied by snake | |
function isPositionOccupied(position) { | |
return snake.some( | |
(segment) => | |
segment.position.x === position.x && | |
segment.position.z === position.z | |
); | |
} | |
// Move the snake | |
function moveSnake() { | |
if (isGameOver) return; | |
// Calculate new head position | |
const head = snake[0]; | |
const newPosition = { | |
x: head.position.x + direction.x, | |
y: 0, | |
z: head.position.z + direction.z, | |
}; | |
// Handle wrap-around at boundaries | |
const halfGrid = Math.floor(gridSize / 2); | |
if (newPosition.x < -halfGrid) { | |
newPosition.x = halfGrid - 1; | |
} else if (newPosition.x >= halfGrid) { | |
newPosition.x = -halfGrid; | |
} | |
if (newPosition.z < -halfGrid) { | |
newPosition.z = halfGrid - 1; | |
} else if (newPosition.z >= halfGrid) { | |
newPosition.z = -halfGrid; | |
} | |
// Check self-collision | |
if (isPositionOccupied(newPosition)) { | |
gameOver(); | |
return; | |
} | |
// Check if food is eaten | |
if ( | |
newPosition.x === food.position.x && | |
newPosition.z === food.position.z | |
) { | |
// Add new head | |
const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9); | |
const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); | |
const segment = new THREE.Mesh(geometry, material); | |
segment.position.set(newPosition.x, 0.5, newPosition.z); | |
scene.add(segment); | |
// Add to beginning of snake array | |
snake.unshift({ mesh: segment, position: { ...newPosition } }); | |
// Create new food | |
createFood(); | |
// Update score | |
score += 10; | |
document.getElementById("score").textContent = "Score: " + score; | |
} | |
// Check if golden food is eaten | |
else if ( | |
goldenFood && | |
newPosition.x === goldenFood.position.x && | |
newPosition.z === goldenFood.position.z | |
) { | |
// Add new head | |
const geometry = new THREE.BoxGeometry(0.9, 0.9, 0.9); | |
const material = new THREE.MeshPhongMaterial({ color: 0x00ff00 }); | |
const segment = new THREE.Mesh(geometry, material); | |
segment.position.set(newPosition.x, 0.5, newPosition.z); | |
scene.add(segment); | |
// Add to beginning of snake array | |
snake.unshift({ mesh: segment, position: { ...newPosition } }); | |
// Remove golden food | |
scene.remove(goldenFood.mesh); | |
goldenFood = null; | |
// Update score (higher bonus for golden food) | |
score += 25; | |
document.getElementById("score").textContent = "Score: " + score; | |
// Activate speed boost | |
bonusActive = true; | |
speed = baseSpeed * bonusSpeedMultiplier; | |
bonusEndTime = Date.now() + bonusDuration; | |
// Show bonus message | |
document.getElementById("bonusActive").style.display = "block"; | |
// Set timer to end bonus | |
setTimeout(() => { | |
bonusActive = false; | |
speed = baseSpeed; | |
document.getElementById("bonusActive").style.display = "none"; | |
}, bonusDuration); | |
} else { | |
// Move body segments | |
for (let i = snake.length - 1; i > 0; i--) { | |
snake[i].position = { ...snake[i - 1].position }; | |
snake[i].mesh.position.set( | |
snake[i].position.x, | |
0.5, | |
snake[i].position.z | |
); | |
} | |
// Move head | |
head.position = { ...newPosition }; | |
head.mesh.position.set(newPosition.x, 0.5, newPosition.z); | |
} | |
} | |
// Handle keyboard controls | |
function handleKeyDown(event) { | |
if (isGameOver) { | |
// Restart game with space | |
if (event.code === "Space") { | |
restartGame(); | |
} | |
return; | |
} | |
switch (event.code) { | |
case "ArrowUp": | |
case "KeyW": | |
if (direction.z !== 1) direction = { x: 0, y: 0, z: -1 }; | |
break; | |
case "ArrowDown": | |
case "KeyS": | |
if (direction.z !== -1) direction = { x: 0, y: 0, z: 1 }; | |
break; | |
case "ArrowLeft": | |
case "KeyA": | |
if (direction.x !== 1) direction = { x: -1, y: 0, z: 0 }; | |
break; | |
case "ArrowRight": | |
case "KeyD": | |
if (direction.x !== -1) direction = { x: 1, y: 0, z: 0 }; | |
break; | |
case "KeyR": // Toggle camera rotation with 'R' key | |
cameraRotationActive = !cameraRotationActive; | |
if (!cameraRotationActive) { | |
// Reset camera to fixed position when disabled | |
camera.position.set(0, cameraHeight, cameraRotationRadius); | |
camera.lookAt(0, 0, 0); | |
} | |
break; | |
} | |
} | |
// Handle game over | |
function gameOver() { | |
isGameOver = true; | |
document.getElementById("gameOver").style.display = "block"; | |
} | |
// Restart the game | |
function restartGame() { | |
// Remove all snake segments | |
for (let segment of snake) { | |
scene.remove(segment.mesh); | |
} | |
// Remove golden food if it exists | |
if (goldenFood) { | |
scene.remove(goldenFood.mesh); | |
goldenFood = null; | |
} | |
// Reset game state | |
snake = []; | |
direction = { x: 1, y: 0, z: 0 }; | |
score = 0; | |
isGameOver = false; | |
bonusActive = false; | |
speed = baseSpeed; | |
// Hide game over and bonus messages | |
document.getElementById("gameOver").style.display = "none"; | |
document.getElementById("bonusActive").style.display = "none"; | |
document.getElementById("score").textContent = "Score: 0"; | |
// Create new snake and food | |
createSnake(); | |
createFood(); | |
} | |
// Handle window resize | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
// Animation loop | |
function animate(time) { | |
requestAnimationFrame(animate); | |
// Move snake at consistent speed | |
if (time - lastTime > 1000 / speed) { | |
moveSnake(); | |
lastTime = time; | |
} | |
// Rotate camera around the scene | |
if (cameraRotationActive) { | |
cameraRotationAngle += cameraRotationSpeed * (bonusActive ? 2 : 1); // Rotate faster during bonus | |
// Update camera position in a circular path | |
camera.position.x = | |
Math.sin(cameraRotationAngle) * cameraRotationRadius; | |
camera.position.z = | |
Math.cos(cameraRotationAngle) * cameraRotationRadius; | |
camera.position.y = cameraHeight; | |
// Always look at the center | |
camera.lookAt(0, 0, 0); | |
} | |
renderer.render(scene, camera); | |
} | |
// Start the game | |
init(); | |
</script> | |
</body> | |
</html> | |