|
<!DOCTYPE html> |
|
<html lang="zh"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>3D太空射击游戏</title> |
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/PointerLockControls.js"></script> |
|
<style> |
|
body { |
|
margin: 0; |
|
overflow: hidden; |
|
background-color: #000; |
|
font-family: Arial, sans-serif; |
|
} |
|
canvas { |
|
display: block; |
|
} |
|
#menu { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
color: white; |
|
text-align: center; |
|
z-index: 100; |
|
} |
|
#startButton { |
|
background: linear-gradient(to bottom, #4a6bff, #0026ff); |
|
border: none; |
|
color: white; |
|
padding: 15px 32px; |
|
text-align: center; |
|
text-decoration: none; |
|
display: inline-block; |
|
font-size: 16px; |
|
margin: 10px 2px; |
|
cursor: pointer; |
|
border-radius: 5px; |
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); |
|
transition: all 0.3s; |
|
} |
|
#startButton:hover { |
|
background: linear-gradient(to bottom, #3a5bef, #0015ee); |
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); |
|
} |
|
#hud { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
padding: 20px; |
|
color: white; |
|
font-size: 18px; |
|
z-index: 10; |
|
pointer-events: none; |
|
} |
|
#crosshair { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
width: 20px; |
|
height: 20px; |
|
transform: translate(-50%, -50%); |
|
pointer-events: none; |
|
opacity: 0.8; |
|
} |
|
#gameOver { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: rgba(0, 0, 0, 0.7); |
|
display: none; |
|
flex-direction: column; |
|
justify-content: center; |
|
align-items: center; |
|
color: white; |
|
font-size: 36px; |
|
z-index: 100; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="menu"> |
|
<h1>太空射击战争</h1> |
|
<p>准备好驾驶你的战机与外星人战斗了吗?</p> |
|
<button id="startButton">开始战斗</button> |
|
</div> |
|
|
|
<div id="hud"> |
|
分数: <span id="score">0</span> | |
|
生命: <span id="health">100</span>% | |
|
敌人剩余: <span id="enemies">0</span> |
|
</div> |
|
|
|
<div id="crosshair"> |
|
<svg width="20" height="20" viewBox="0 0 20 20"> |
|
<circle cx="10" cy="10" r="5" stroke="white" stroke-width="2" fill="none"/> |
|
<line x1="10" y1="0" x2="10" y2="6" stroke="white" stroke-width="2"/> |
|
<line x1="10" y1="14" x2="10" y2="20" stroke="white" stroke-width="2"/> |
|
<line x1="0" y1="10" x2="6" y2="10" stroke="white" stroke-width="2"/> |
|
<line x1="14" y1="10" x2="20" y2="10" stroke="white" stroke-width="2"/> |
|
</svg> |
|
</div> |
|
|
|
<div id="gameOver"> |
|
<h1>游戏结束</h1> |
|
<p>你的最终分数: <span id="finalScore">0</span></p> |
|
<button id="restartButton">再来一次</button> |
|
</div> |
|
|
|
<script> |
|
|
|
const gameState = { |
|
score: 0, |
|
health: 100, |
|
enemies: 0, |
|
isGameOver: false, |
|
isGameStarted: false, |
|
enemySpawnRate: 1000, |
|
lastSpawnTime: 0, |
|
lastBulletTime: 0, |
|
bulletCooldown: 200, |
|
bullets: [], |
|
enemies: [], |
|
explosions: [] |
|
}; |
|
|
|
|
|
let scene, camera, renderer, controls; |
|
let spaceship, stars = [], enemySpawnInterval; |
|
const clock = new THREE.Clock(); |
|
const movementSpeed = 20; |
|
const rotationSpeed = 0.002; |
|
|
|
|
|
function init() { |
|
|
|
scene = new THREE.Scene(); |
|
scene.fog = new THREE.FogExp2(0x000033, 0.001); |
|
|
|
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); |
|
camera.position.z = 5; |
|
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
renderer.shadowMap.enabled = true; |
|
document.body.appendChild(renderer.domElement); |
|
|
|
|
|
addLights(); |
|
|
|
|
|
createStarfield(); |
|
|
|
|
|
createSpaceship(); |
|
|
|
|
|
window.addEventListener('resize', onWindowResize); |
|
document.getElementById('startButton').addEventListener('click', startGame); |
|
document.getElementById('restartButton').addEventListener('click', restartGame); |
|
document.addEventListener('keydown', onKeyDown); |
|
document.addEventListener('keyup', onKeyUp); |
|
document.addEventListener('click', fireBullet); |
|
} |
|
|
|
|
|
function addLights() { |
|
const ambientLight = new THREE.AmbientLight(0x404040); |
|
scene.add(ambientLight); |
|
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); |
|
directionalLight.position.set(1, 1, 1); |
|
directionalLight.castShadow = true; |
|
directionalLight.shadow.mapSize.width = 2048; |
|
directionalLight.shadow.mapSize.height = 2048; |
|
scene.add(directionalLight); |
|
|
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 1); |
|
hemiLight.position.set(0, 20, 0); |
|
scene.add(hemiLight); |
|
} |
|
|
|
|
|
function createStarfield() { |
|
for (let i = 0; i < 1000; i++) { |
|
const geometry = new THREE.SphereGeometry(0.1, 8, 8); |
|
const material = new THREE.MeshBasicMaterial({ |
|
color: Math.random() > 0.5 ? 0xffffff : 0xaaaaaa, |
|
transparent: true, |
|
opacity: Math.random() |
|
}); |
|
const star = new THREE.Mesh(geometry, material); |
|
|
|
star.position.x = Math.random() * 2000 - 1000; |
|
star.position.y = Math.random() * 2000 - 1000; |
|
star.position.z = Math.random() * 2000 - 1000; |
|
|
|
stars.push(star); |
|
scene.add(star); |
|
} |
|
} |
|
|
|
|
|
function createSpaceship() { |
|
const group = new THREE.Group(); |
|
|
|
|
|
const bodyGeometry = new THREE.ConeGeometry(1, 3, 8); |
|
const bodyMaterial = new THREE.MeshPhongMaterial({ |
|
color: 0x3366ff, |
|
flatShading: true, |
|
emissive: 0x0033cc, |
|
emissiveIntensity: 0.5 |
|
}); |
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); |
|
body.rotation.x = Math.PI / 2; |
|
group.add(body); |
|
|
|
|
|
const wingGeometry = new THREE.BoxGeometry(2, 0.2, 1); |
|
const wingMaterial = new THREE.MeshPhongMaterial({ |
|
color: 0x0044ff, |
|
flatShading: true |
|
}); |
|
|
|
const leftWing = new THREE.Mesh(wingGeometry, wingMaterial); |
|
leftWing.position.set(-1, 0, 0); |
|
group.add(leftWing); |
|
|
|
const rightWing = new THREE.Mesh(wingGeometry, wingMaterial); |
|
rightWing.position.set(1, 0, 0); |
|
group.add(rightWing); |
|
|
|
|
|
const tailWingGeometry = new THREE.BoxGeometry(0.2, 1, 0.8); |
|
const tailWing = new THREE.Mesh(tailWingGeometry, wingMaterial); |
|
tailWing.position.set(0, 0, -1.5); |
|
group.add(tailWing); |
|
|
|
|
|
const engineLightGeometry = new THREE.SphereGeometry(0.5, 8, 8); |
|
const engineLightMaterial = new THREE.MeshBasicMaterial({ |
|
color: 0xffff00, |
|
transparent: true, |
|
opacity: 0.8 |
|
}); |
|
const engineLight = new THREE.Mesh(engineLightGeometry, engineLightMaterial); |
|
engineLight.position.set(0, 0, 1.5); |
|
group.add(engineLight); |
|
|
|
group.position.z = -10; |
|
scene.add(group); |
|
spaceship = group; |
|
} |
|
|
|
|
|
function createEnemy() { |
|
const group = new THREE.Group(); |
|
|
|
|
|
const bodyGeometry = new THREE.OctahedronGeometry(1); |
|
const bodyMaterial = new THREE.MeshPhongMaterial({ |
|
color: 0xff0000, |
|
flatShading: true, |
|
emissive: 0xcc0000, |
|
emissiveIntensity: 0.2 |
|
}); |
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); |
|
group.add(body); |
|
|
|
|
|
group.userData = { |
|
speed: Math.random() * 2 + 1, |
|
health: 30, |
|
lastShot: 0, |
|
shotCooldown: 2000 + Math.random() * 2000 |
|
}; |
|
|
|
|
|
const distance = 50 + Math.random() * 50; |
|
const angle = Math.random() * Math.PI * 2; |
|
|
|
group.position.x = Math.cos(angle) * distance; |
|
group.position.y = Math.sin(angle) * distance; |
|
group.position.z = -100 - Math.random() * 50; |
|
|
|
|
|
group.lookAt(spaceship.position); |
|
|
|
scene.add(group); |
|
gameState.enemies.push(group); |
|
gameState.enemies++; |
|
updateHUD(); |
|
|
|
return group; |
|
} |
|
|
|
|
|
function createBullet(position, direction, isPlayerBullet = true) { |
|
const geometry = new THREE.SphereGeometry(0.1, 16, 16); |
|
const material = new THREE.MeshBasicMaterial({ |
|
color: isPlayerBullet ? 0x00ffff : 0xff6600, |
|
emissive: isPlayerBullet ? 0x00aaff : 0xff3300, |
|
emissiveIntensity: 0.8 |
|
}); |
|
const bullet = new THREE.Mesh(geometry, material); |
|
|
|
bullet.position.copy(position); |
|
bullet.userData = { |
|
direction: direction.clone().normalize(), |
|
speed: isPlayerBullet ? 20 : 10, |
|
damage: isPlayerBullet ? 10 : 5, |
|
isPlayerBullet |
|
}; |
|
|
|
scene.add(bullet); |
|
gameState.bullets.push(bullet); |
|
|
|
return bullet; |
|
} |
|
|
|
|
|
function createExplosion(position) { |
|
const particles = 50; |
|
const geometry = new THREE.BufferGeometry(); |
|
const positions = new Float32Array(particles * 3); |
|
const sizes = new Float32Array(particles); |
|
|
|
for (let i = 0; i < particles; i++) { |
|
positions[i * 3] = (Math.random() - 0.5) * 4; |
|
positions[i * 3 + 1] = (Math.random() - 0.5) * 4; |
|
positions[i * 3 + 2] = (Math.random() - 0.5) * 4; |
|
sizes[i] = Math.random() * 0.5 + 0.1; |
|
} |
|
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); |
|
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); |
|
|
|
const material = new THREE.PointsMaterial({ |
|
color: new THREE.Color(Math.random() > 0.5 ? 0xff6600 : 0xff0000), |
|
size: 0.1, |
|
transparent: true, |
|
opacity: 1, |
|
sizeAttenuation: true |
|
}); |
|
|
|
const particleSystem = new THREE.Points(geometry, material); |
|
particleSystem.position.copy(position); |
|
|
|
particleSystem.userData = { |
|
life: 1.0, |
|
decay: 0.05 |
|
}; |
|
|
|
scene.add(particleSystem); |
|
gameState.explosions.push(particleSystem); |
|
} |
|
|
|
|
|
function fireBullet() { |
|
if (!gameState.isGameStarted || gameState.isGameOver) return; |
|
|
|
const now = Date.now(); |
|
if (now - gameState.lastBulletTime < gameState.bulletCooldown) return; |
|
|
|
gameState.lastBulletTime = now; |
|
|
|
|
|
const bulletPosition = new THREE.Vector3(); |
|
spaceship.getWorldPosition(bulletPosition); |
|
|
|
|
|
bulletPosition.z -= 1; |
|
|
|
|
|
const direction = new THREE.Vector3(0, 0, -1); |
|
direction.applyQuaternion(camera.quaternion); |
|
|
|
createBullet(bulletPosition, direction); |
|
|
|
|
|
playSound('shoot', 0.1); |
|
} |
|
|
|
|
|
function enemyFire(enemy) { |
|
const now = Date.now(); |
|
if (now - enemy.userData.lastShot < enemy.userData.shotCooldown) return; |
|
|
|
enemy.userData.lastShot = now; |
|
|
|
const bulletPosition = new THREE.Vector3(); |
|
enemy.getWorldPosition(bulletPosition); |
|
|
|
|
|
const direction = new THREE.Vector3(); |
|
spaceship.getWorldPosition(direction); |
|
direction.sub(bulletPosition).normalize(); |
|
|
|
createBullet(bulletPosition, direction, false); |
|
} |
|
|
|
|
|
function startGame() { |
|
document.getElementById('menu').style.display = 'none'; |
|
gameState.isGameStarted = true; |
|
|
|
|
|
controls = new THREE.PointerLockControls(camera, document.body); |
|
scene.add(controls.getObject()); |
|
|
|
|
|
document.body.requestPointerLock = document.body.requestPointerLock || |
|
document.body.mozRequestPointerLock || |
|
document.body.webkitRequestPointerLock; |
|
document.body.requestPointerLock(); |
|
|
|
|
|
enemySpawnInterval = setInterval(() => { |
|
if (!gameState.isGameOver) { |
|
createEnemy(); |
|
} |
|
}, gameState.enemySpawnRate); |
|
|
|
|
|
animate(); |
|
} |
|
|
|
|
|
function gameOver() { |
|
gameState.isGameOver = true; |
|
clearInterval(enemySpawnInterval); |
|
document.exitPointerLock(); |
|
|
|
document.getElementById('finalScore').textContent = gameState.score; |
|
document.getElementById('gameOver').style.display = 'flex'; |
|
} |
|
|
|
|
|
function restartGame() { |
|
|
|
gameState.score = 0; |
|
gameState.health = 100; |
|
gameState.enemies = 0; |
|
gameState.isGameOver = false; |
|
|
|
|
|
gameState.enemies.forEach(enemy => scene.remove(enemy)); |
|
gameState.bullets.forEach(bullet => scene.remove(bullet)); |
|
gameState.explosions.forEach(explosion => scene.remove(explosion)); |
|
|
|
gameState.enemies = []; |
|
gameState.bullets = []; |
|
gameState.explosions = []; |
|
|
|
|
|
spaceship.position.set(0, 0, -10); |
|
spaceship.rotation.set(0, 0, 0); |
|
|
|
|
|
document.getElementById('gameOver').style.display = 'none'; |
|
|
|
|
|
updateHUD(); |
|
|
|
|
|
startGame(); |
|
} |
|
|
|
|
|
function updateHUD() { |
|
document.getElementById('score').textContent = gameState.score; |
|
document.getElementById('health').textContent = gameState.health; |
|
document.getElementById('enemies').textContent = gameState.enemies; |
|
} |
|
|
|
|
|
function checkCollisions() { |
|
|
|
for (let i = gameState.bullets.length - 1; i >= 0; i--) { |
|
const bullet = gameState.bullets[i]; |
|
|
|
if (bullet.userData.isPlayerBullet) { |
|
for (let j = gameState.enemies.length - 1; j >= 0; j--) { |
|
const enemy = gameState.enemies[j]; |
|
|
|
if (bullet.position.distanceTo(enemy.position) < 1.5) { |
|
|
|
enemy.userData.health -= bullet.userData.damage; |
|
|
|
|
|
createExplosion(bullet.position); |
|
|
|
|
|
scene.remove(bullet); |
|
gameState.bullets.splice(i, 1); |
|
|
|
|
|
if (enemy.userData.health <= 0) { |
|
gameState.score += 10; |
|
gameState.enemies--; |
|
|
|
createExplosion(enemy.position); |
|
scene.remove(enemy); |
|
gameState.enemies.splice(j, 1); |
|
|
|
|
|
playSound('explosion', 0.3); |
|
} else { |
|
|
|
playSound('hit', 0.2); |
|
} |
|
|
|
updateHUD(); |
|
break; |
|
} |
|
} |
|
} else { |
|
|
|
if (bullet.position.distanceTo(spaceship.position) < 2) { |
|
|
|
gameState.health -= bullet.userData.damage; |
|
|
|
createExplosion(bullet.position); |
|
scene.remove(bullet); |
|
gameState.bullets.splice(i, 1); |
|
|
|
updateHUD(); |
|
|
|
|
|
playSound('hit', 0.3); |
|
|
|
|
|
if (gameState.health <= 0) { |
|
gameState.health = 0; |
|
gameOver(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
gameState.enemies.forEach(enemy => { |
|
if (enemy.position.distanceTo(spaceship.position) < 3) { |
|
gameState.health -= 2; |
|
updateHUD(); |
|
|
|
|
|
playSound('collision', 0.5); |
|
|
|
if (gameState.health <= 0) { |
|
gameState.health = 0; |
|
gameOver(); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
|
|
function playSound(type, volume) { |
|
if (!window.AudioContext) return; |
|
|
|
const ctx = new (window.AudioContext || window.webkitAudioContext)(); |
|
const oscillator = ctx.createOscillator(); |
|
const gainNode = ctx.createGain(); |
|
|
|
oscillator.connect(gainNode); |
|
gainNode.connect(ctx.destination); |
|
|
|
gainNode.gain.value = volume; |
|
|
|
|
|
switch (type) { |
|
case 'shoot': |
|
oscillator.type = 'square'; |
|
oscillator.frequency.value = 220; |
|
oscillator.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.1); |
|
break; |
|
case 'hit': |
|
oscillator.type = 'sine'; |
|
oscillator.frequency.value = 440; |
|
oscillator.frequency.exponentialRampToValueAtTime(200, ctx.currentTime + 0.1); |
|
break; |
|
case 'explosion': |
|
oscillator.type = 'sawtooth'; |
|
oscillator.frequency.value = 60; |
|
oscillator.frequency.exponentialRampToValueAtTime(20, ctx.currentTime + 0.5); |
|
break; |
|
case 'collision': |
|
oscillator.type = 'sine'; |
|
oscillator.frequency.value = 110; |
|
oscillator.frequency.exponentialRampToValueAtTime(55, ctx.currentTime + 0.5); |
|
break; |
|
} |
|
|
|
oscillator.start(); |
|
oscillator.stop(ctx.currentTime + 0.5); |
|
} |
|
|
|
|
|
const keys = {}; |
|
function onKeyDown(event) { |
|
keys[event.code] = true; |
|
} |
|
function onKeyUp(event) { |
|
keys[event.code] = false; |
|
} |
|
|
|
|
|
function processInput() { |
|
const delta = clock.getDelta(); |
|
|
|
|
|
if (keys['KeyW']) { |
|
camera.position.z -= movementSpeed * delta; |
|
spaceship.position.z -= movementSpeed * delta; |
|
} |
|
if (keys['KeyS']) { |
|
camera.position.z += movementSpeed * delta; |
|
spaceship.position.z += movementSpeed * delta; |
|
} |
|
if (keys['KeyA']) { |
|
camera.position.x -= movementSpeed * delta; |
|
spaceship.position.x -= movementSpeed * delta; |
|
} |
|
if (keys['KeyD']) { |
|
camera.position.x += movementSpeed * delta; |
|
spaceship.position.x += movementSpeed * delta; |
|
} |
|
if (keys['Space']) { |
|
camera.position.y += movementSpeed * delta; |
|
spaceship.position.y += movementSpeed * delta; |
|
} |
|
if (keys['ShiftLeft']) { |
|
camera.position.y -= movementSpeed * delta; |
|
spaceship.position.y -= movementSpeed * delta; |
|
} |
|
|
|
|
|
const maxDistance = 100; |
|
spaceship.position.x = Math.max(-maxDistance, Math.min(maxDistance, spaceship.position.x)); |
|
spaceship.position.y = Math.max(5, Math.min(maxDistance, spaceship.position.y)); |
|
spaceship.position.z = Math.max(-maxDistance-10, Math.min(20, spaceship.position.z)); |
|
camera.position.copy(spaceship.position); |
|
camera.position.z += 5; |
|
} |
|
|
|
|
|
function update(delta) { |
|
|
|
for (let i = gameState.bullets.length - 1; i >= 0; i--) { |
|
const bullet = gameState.bullets[i]; |
|
bullet.position.add(bullet.userData.direction.multiplyScalar(bullet.userData.speed * delta)); |
|
|
|
|
|
if (bullet.position.length() > 500) { |
|
scene.remove(bullet); |
|
gameState.bullets.splice(i, 1); |
|
} |
|
} |
|
|
|
|
|
gameState.enemies.forEach(enemy => { |
|
|
|
enemy.lookAt(spaceship.position); |
|
const direction = new THREE.Vector3(); |
|
spaceship.getWorldPosition(direction); |
|
direction.sub(enemy.position).normalize(); |
|
enemy.position.add(direction.multiplyScalar(enemy.userData.speed * delta)); |
|
|
|
|
|
if (Math.random() < 0.01) { |
|
enemyFire(enemy); |
|
} |
|
}); |
|
|
|
|
|
for (let i = gameState.explosions.length - 1; i >= 0; i--) { |
|
const explosion = gameState.explosions[i]; |
|
explosion.userData.life -= explosion.userData.decay; |
|
|
|
if (explosion.userData.life <= 0) { |
|
scene.remove(explosion); |
|
gameState.explosions.splice(i, 1); |
|
} else { |
|
explosion.material.opacity = explosion.userData.life; |
|
} |
|
} |
|
|
|
|
|
checkCollisions(); |
|
} |
|
|
|
|
|
function onWindowResize() { |
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
} |
|
|
|
|
|
function animate() { |
|
if (gameState.isGameOver) return; |
|
|
|
requestAnimationFrame(animate); |
|
const delta = clock.getDelta(); |
|
|
|
if (gameState.isGameStarted && !gameState.isGameOver) { |
|
processInput(); |
|
update(delta); |
|
controls.update(delta); |
|
} |
|
|
|
renderer.render(scene, camera); |
|
} |
|
|
|
|
|
init(); |
|
</script> |
|
</body> |
|
</html> |