2025-03-29 10:39:20 +08:00

480 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>3D第一人称射击游戏</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { touch-action: none; }
#crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
background-color: red;
border-radius: 50%;
pointer-events: none;
}
.ui { font-family: Arial, sans-serif; color: white; font-size: 16px; position: absolute; pointer-events: none; }
#joystick {
position: absolute;
bottom: 20px;
left: 20px;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
z-index: 10;
display: none; /* Hidden by default */
}
#joystickKnob {
position: absolute;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
}
#shootButton, #reloadButton {
display: none; /* Hidden by default */
}
#shootButton {
position: absolute;
bottom: 50px;
right: 20px;
width: 80px;
height: 80px;
background: rgba(255, 0, 0, 0.7);
border-radius: 50%;
border: none;
color: white;
font-size: 20px;
z-index: 10;
}
#reloadButton {
position: absolute;
bottom: 150px;
right: 40px;
width: 40px;
height: 40px;
background: rgba(0, 255, 0, 0.7);
border-radius: 50%;
border: none;
color: white;
font-size: 16px;
z-index: 10;
}
#modeSelectModal, #gameOverModal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 20;
}
#modeSelectModal button, #gameOverModal button {
padding: 10px 20px;
font-size: 16px;
margin: 5px;
cursor: pointer;
}
#gameOverModal { display: none; }
</style>
</head>
<body>
<div id="crosshair"></div>
<div id="damageOverlay" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 0, 0, 0); pointer-events: none;"></div>
<div id="modeSelectModal">
<h2>选择游戏模式</h2>
<p>请选择您的设备类型:</p>
<button id="desktopMode">桌面模式</button>
<button id="mobileMode">移动模式</button>
</div>
<div id="gameOverModal">
<h2>游戏结束</h2>
<p id="finalScore"></p>
<button id="restartButton">重新开始</button>
</div>
<div id="joystick"><div id="joystickKnob"></div></div>
<button id="shootButton">射击</button>
<button id="reloadButton">换弹</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
<script>
let isGamePaused = true; // Start paused until mode is selected
let mode = null; // 'desktop' or 'mobile'
// Audio Functions
function playShootSound() { const audio = new Audio('./audio/手枪开枪.mp3'); audio.play(); }
function playReloadSound() { const audio = new Audio('./audio/手枪换弹.mp3'); audio.play(); }
function playDamageSound() { const audio = new Audio('./audio/受击.mp3'); audio.play(); }
// Scene Setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
// Camera Control
let yaw = 0, pitch = 0, sensitivity = 0.005;
const keys = {};
function setupDesktopControls() {
document.addEventListener('click', () => renderer.domElement.requestPointerLock());
document.addEventListener('mousemove', (event) => {
if (document.pointerLockElement === renderer.domElement) {
yaw -= (event.movementX || 0) * sensitivity;
pitch -= (event.movementY || 0) * sensitivity;
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
camera.rotation.order = 'YXZ';
camera.rotation.set(pitch, yaw, 0);
}
});
document.addEventListener('keydown', (event) => keys[event.key.toLowerCase()] = true);
document.addEventListener('keyup', (event) => keys[event.key.toLowerCase()] = false);
}
let touchStartX = 0, touchStartY = 0, isSwiping = false;
function setupMobileControls() {
document.addEventListener('touchstart', (e) => {
if (e.target.tagName !== 'BUTTON' && e.target.id !== 'joystick' && e.target.id !== 'joystickKnob') {
isSwiping = true;
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
});
document.addEventListener('touchmove', (e) => {
if (isSwiping) {
const deltaX = e.touches[0].clientX - touchStartX;
const deltaY = e.touches[0].clientY - touchStartY;
yaw -= deltaX * sensitivity;
pitch -= deltaY * sensitivity;
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
camera.rotation.order = 'YXZ';
camera.rotation.set(pitch, yaw, 0);
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
});
document.addEventListener('touchend', () => { isSwiping = false; });
}
// Joystick Control
const joystick = document.getElementById('joystick');
const knob = document.getElementById('joystickKnob');
let joystickActive = false, joystickX = 0, joystickY = 0, joystickTouchId = null;
function setupJoystick() {
joystick.style.display = 'block';
joystick.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
joystickTouchId = touch.identifier;
joystickActive = true;
updateJoystick(touch);
});
document.addEventListener('touchmove', (e) => {
if (joystickActive) {
for (let touch of e.touches) {
if (touch.identifier === joystickTouchId) {
updateJoystick(touch);
break;
}
}
}
});
document.addEventListener('touchend', (e) => {
for (let touch of e.changedTouches) {
if (touch.identifier === joystickTouchId) {
joystickActive = false;
knob.style.left = '50%';
knob.style.top = '50%';
joystickX = 0;
joystickY = 0;
joystickTouchId = null;
break;
}
}
});
}
function updateJoystick(touch) {
const rect = joystick.getBoundingClientRect();
let dx = touch.clientX - (rect.left + rect.width / 2);
let dy = touch.clientY - (rect.top + rect.height / 2);
const distance = Math.sqrt(dx * dx + dy * dy);
const maxDistance = rect.width / 2 - 20;
if (distance > maxDistance) {
dx = dx * maxDistance / distance;
dy = dy * maxDistance / distance;
}
knob.style.left = `${50 + (dx / maxDistance) * 50}%`;
knob.style.top = `${50 + (dy / maxDistance) * 50}%`;
joystickX = dx / maxDistance;
joystickY = dy / maxDistance;
}
// UI Elements
let bullets = 10, maxBullets = 10, health = 100;
const bulletDisplay = document.createElement('div');
bulletDisplay.className = 'ui';
bulletDisplay.style.bottom = '10px';
bulletDisplay.style.right = '10px';
bulletDisplay.innerText = `子弹: ${bullets}`;
document.body.appendChild(bulletDisplay);
const healthDisplay = document.createElement('div');
healthDisplay.className = 'ui';
healthDisplay.style.bottom = '10px';
healthDisplay.style.left = '10px';
healthDisplay.innerText = `生命值: ${health}`;
document.body.appendChild(healthDisplay);
function deductHealth(amount) {
if (isGamePaused) return;
health -= amount;
healthDisplay.innerText = `生命值: ${health}`;
playDamageSound();
const damageOverlay = document.getElementById('damageOverlay');
gsap.fromTo(damageOverlay, { backgroundColor: 'rgba(255, 0, 0, 0.5)' }, { backgroundColor: 'rgba(255, 0, 0, 0)', duration: 0.5, ease: 'power1.out' });
if (health <= 0) endGame();
}
// Gun
const gunTexture = new THREE.TextureLoader().load('./image/gun.png');
const gunMaterial = new THREE.MeshBasicMaterial({ map: gunTexture, transparent: true });
const gunGeometry = new THREE.PlaneGeometry(1, 1);
const gun = new THREE.Mesh(gunGeometry, gunMaterial);
gun.position.set(0.5, -0.5, -1);
camera.add(gun);
scene.add(camera);
// Shoot Function
function shoot() {
if (bullets > 0 && !isGamePaused) {
bullets--;
bulletDisplay.innerText = `子弹: ${bullets}`;
playShootSound();
const tl = gsap.timeline();
tl.to(gun.position, { z: -0.8, duration: 0.02, ease: "power1.in" })
.to(gun.position, { z: -1, duration: 0.3, ease: "power2.out" });
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const intersects = raycaster.intersectObjects(enemies.map(e => e.sprite));
if (intersects.length > 0) {
const enemy = intersects[0].object;
const hitDirection = new THREE.Vector3().subVectors(enemy.position, camera.position).normalize();
gsap.to(enemy.position, {
x: enemy.position.x + hitDirection.x * 5,
y: enemy.position.y + 2,
z: enemy.position.z + hitDirection.z * 5,
duration: 0.3,
ease: "power2.out",
onComplete: () => { scene.remove(enemy); enemies = enemies.filter(e => e.sprite !== enemy); }
});
gsap.to(enemy.material, { opacity: 0, duration: 0.3, onComplete: () => { enemy.material.opacity = 1; } });
score += 20;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
}
}
}
// Reload Function
function reload() {
if (bullets < maxBullets && !isGamePaused) {
playReloadSound();
setTimeout(() => {
bullets = maxBullets;
bulletDisplay.innerText = `子弹: ${bullets}`;
}, 1500);
}
}
// Desktop/Mobile Shoot/Reload Setup
function setupControls() {
if (mode === 'desktop') {
document.addEventListener('click', shoot);
document.addEventListener('keydown', (event) => { if (event.key === 'r') reload(); });
setupDesktopControls();
} else if (mode === 'mobile') {
const shootButton = document.getElementById('shootButton');
const reloadButton = document.getElementById('reloadButton');
shootButton.style.display = 'block';
reloadButton.style.display = 'block';
shootButton.addEventListener('touchstart', (e) => { e.preventDefault(); shoot(); });
reloadButton.addEventListener('touchstart', (e) => { e.preventDefault(); reload(); });
setupMobileControls();
setupJoystick();
}
}
// Terrain
const noise = new SimplexNoise();
const terrainSize = 100, terrainDetail = 0.1, terrainHeight = 1;
const geometry = new THREE.PlaneGeometry(terrainSize, terrainSize, 100, 100);
geometry.rotateX(-Math.PI / 2);
const vertices = geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i], z = vertices[i + 2];
vertices[i + 1] = noise.noise2D(x * terrainDetail, z * terrainDetail) * terrainHeight;
}
geometry.computeVertexNormals();
const terrainMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
const terrain = new THREE.Mesh(geometry, terrainMaterial);
scene.add(terrain);
const raycaster = new THREE.Raycaster();
raycaster.ray.direction.set(0, -1, 0);
// Enemies
let enemies = [];
class Enemy {
constructor() {
const enemyTextures = ['./image/1.png', './image/2.png'];
const texture = new THREE.TextureLoader().load(enemyTextures[Math.floor(Math.random() * 2)]);
const material = new THREE.SpriteMaterial({ map: texture });
this.sprite = new THREE.Sprite(material);
this.sprite.scale.set(2, 2, 1);
const edge = Math.floor(Math.random() * 4), halfSize = terrainSize / 2;
switch (edge) {
case 0: this.sprite.position.set(Math.random() * terrainSize - halfSize, 10, halfSize); break;
case 1: this.sprite.position.set(Math.random() * terrainSize - halfSize, 10, -halfSize); break;
case 2: this.sprite.position.set(-halfSize, 10, Math.random() * terrainSize - halfSize); break;
case 3: this.sprite.position.set(halfSize, 10, Math.random() * terrainSize - halfSize); break;
}
scene.add(this.sprite);
gsap.to(this.sprite.material, { opacity: 1, duration: 1 });
}
update() {
this.sprite.lookAt(camera.position);
const direction = new THREE.Vector3().subVectors(camera.position, this.sprite.position).normalize();
this.sprite.position.add(direction.multiplyScalar(0.05));
raycaster.ray.origin.copy(this.sprite.position);
raycaster.ray.origin.y = 10;
const intersects = raycaster.intersectObject(terrain);
if (intersects.length > 0) this.sprite.position.y = intersects[0].point.y + 1;
if (this.sprite.position.distanceTo(camera.position) < 1) {
deductHealth(15);
scene.remove(this.sprite);
enemies = enemies.filter(e => e !== this);
}
}
}
setInterval(() => { if (enemies.length < 10 && !isGamePaused) enemies.push(new Enemy()); }, 2000);
// Timer and Score
let timeLeft = 150, score = 0;
const timerDisplay = document.createElement('div');
timerDisplay.className = 'ui';
timerDisplay.style.top = '10px';
timerDisplay.style.left = '10px';
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
document.body.appendChild(timerDisplay);
setInterval(() => {
if (!isGamePaused) {
timeLeft--;
score++;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
if (timeLeft <= 0) endGame();
}
}, 1000);
function endGame() {
isGamePaused = true;
if (mode === 'desktop') document.exitPointerLock();
const modal = document.getElementById('gameOverModal');
document.getElementById('finalScore').innerText = `最终得分: ${score}`;
modal.style.display = 'block';
}
function restartGame() {
isGamePaused = false;
health = 100; bullets = maxBullets; timeLeft = 150; score = 0;
enemies.forEach(enemy => scene.remove(enemy.sprite));
enemies = [];
healthDisplay.innerText = `生命值: ${health}`;
bulletDisplay.innerText = `子弹: ${bullets}`;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
document.getElementById('gameOverModal').style.display = 'none';
animate();
}
document.getElementById('restartButton').addEventListener('click', restartGame);
// Skybox
const skyboxLoader = new THREE.CubeTextureLoader();
scene.background = skyboxLoader.load([
'https://i.imgur.com/px.jpg', 'https://i.imgur.com/nx.jpg',
'https://i.imgur.com/py.jpg', 'https://i.imgur.com/ny.jpg',
'https://i.imgur.com/pz.jpg', 'https://i.imgur.com/nz.jpg'
]);
// Animation Loop
function animate() {
if (isGamePaused) return;
requestAnimationFrame(animate);
const speed = 0.1;
const frontVector = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
const rightVector = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
if (mode === 'desktop') {
if (keys['w']) camera.position.add(frontVector.multiplyScalar(speed));
if (keys['s']) camera.position.add(frontVector.multiplyScalar(-speed));
if (keys['a']) camera.position.add(rightVector.multiplyScalar(-speed));
if (keys['d']) camera.position.add(rightVector.multiplyScalar(speed));
} else if (mode === 'mobile') {
camera.position.add(frontVector.multiplyScalar(-joystickY * speed));
camera.position.add(rightVector.multiplyScalar(joystickX * speed));
}
raycaster.ray.origin.copy(camera.position);
raycaster.ray.origin.y += 1.6;
const intersects = raycaster.intersectObject(terrain);
if (intersects.length > 0) camera.position.y = intersects[0].point.y + 1.6;
enemies.forEach(enemy => enemy.update());
renderer.render(scene, camera);
}
// Mode Selection
const modeSelectModal = document.getElementById('modeSelectModal');
document.getElementById('desktopMode').addEventListener('click', () => {
mode = 'desktop';
modeSelectModal.style.display = 'none';
isGamePaused = false;
setupControls();
animate();
});
document.getElementById('mobileMode').addEventListener('click', () => {
mode = 'mobile';
modeSelectModal.style.display = 'none';
isGamePaused = false;
setupControls();
animate();
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>