395 lines
15 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D第一人称射击游戏</title>
<style>
body { margin: 0; overflow: hidden; }
#crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
background-color: red;
border-radius: 50%;
}
.ui { font-family: Arial, sans-serif; color: white; font-size: 20px; position: absolute; }
</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="gameOverModal" style="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; display: none;">
<h2>游戏结束</h2>
<p id="finalScore"></p>
<button id="restartButton" style="padding: 10px 20px; font-size: 16px; cursor: pointer;">重新开始</button>
</div>
<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 = false;
// 初始化场景
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); // 玩家高度1.6米
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);
// 视角控制(重新设计)
let yaw = 0; // 水平旋转角度(左右)
let pitch = 0; // 垂直旋转角度(上下)
const sensitivity = 0.002; // 鼠标灵敏度
document.addEventListener('click', () => {
renderer.domElement.requestPointerLock();
});
document.addEventListener('mousemove', (event) => {
if (document.pointerLockElement === renderer.domElement) {
const movementX = event.movementX || 0;
const movementY = event.movementY || 0;
// 水平旋转(向左移动鼠标镜头向左)
yaw -= movementX * sensitivity;
// 垂直旋转(向上移动鼠标镜头向上)
pitch -= movementY * sensitivity;
pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch)); // 限制垂直角度
// 更新相机旋转
camera.rotation.order = 'YXZ'; // 先Y水平后X垂直
camera.rotation.set(pitch, yaw, 0);
}
});
// WASD移动控制
const keys = {};
document.addEventListener('keydown', (event) => keys[event.key.toLowerCase()] = true);
document.addEventListener('keyup', (event) => keys[event.key.toLowerCase()] = false);
// 子弹管理
let bullets = 10;
const maxBullets = 10;
const bulletDisplay = document.createElement('div');
bulletDisplay.className = 'ui';
bulletDisplay.style.bottom = '10px';
bulletDisplay.style.right = '10px';
bulletDisplay.innerText = `子弹: ${bullets}`;
document.body.appendChild(bulletDisplay);
// 生命值
let health = 100;
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}`;
// 触发红色闪烁效果
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();
}
}
// 枪械展示
const gunTexture = new THREE.TextureLoader().load('./image/gun.png'); // 替换为实际枪械图片URL
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);
// 开枪函数
function shoot() {
if (bullets > 0) {
bullets--;
bulletDisplay.innerText = `子弹: ${bullets}`;
gsap.to(gun.position, { z: -1.2, duration: 0.05, yoyo: true, repeat: 1 });
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, // 沿射击方向飞5个单位
y: enemy.position.y + 2, // 向上飞2个单位
z: enemy.position.z + hitDirection.z * 5,
duration: 0.3, // 飞行时间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}`;
}
}
}
document.addEventListener('click', shoot);
document.addEventListener('keydown', (event) => {
if (event.key === 'r' && bullets < maxBullets) {
bullets = maxBullets;
bulletDisplay.innerText = `子弹: ${bullets}`;
}
});
// 地形生成
const noise = new SimplexNoise();
const terrainSize = 100;
const terrainDetail = 0.1;
const 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];
const z = vertices[i + 2];
const y = noise.noise2D(x * terrainDetail, z * terrainDetail) * terrainHeight;
vertices[i + 1] = y;
}
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);
// 敌人生成(高度贴合地表)
let enemies = [];
class Enemy {
constructor() {
const enemyTextures = [
'./image/1.png',
'./image/2.png',
];
const texture = new THREE.TextureLoader().load(enemyTextures[Math.floor(Math.random() * enemyTextures.length)]);
const material = new THREE.SpriteMaterial({ map: texture });
this.sprite = new THREE.Sprite(material);
this.sprite.scale.set(2, 2, 1);
// 随机选择地图边缘0: 上, 1: 下, 2: 左, 3: 右)
const edge = Math.floor(Math.random() * 4);
const halfSize = terrainSize / 2; // terrainSize = 100, halfSize = 50
switch (edge) {
case 0: // 上边缘 (z = 50)
this.sprite.position.set(
Math.random() * terrainSize - halfSize, // x: [-50, 50]
10, // y: 10
halfSize // z: 50
);
break;
case 1: // 下边缘 (z = -50)
this.sprite.position.set(
Math.random() * terrainSize - halfSize, // x: [-50, 50]
10, // y: 10
-halfSize // z: -50
);
break;
case 2: // 左边缘 (x = -50)
this.sprite.position.set(
-halfSize, // x: -50
10, // y: 10
Math.random() * terrainSize - halfSize // z: [-50, 50]
);
break;
case 3: // 右边缘 (x = 50)
this.sprite.position.set(
halfSize, // x: 50
10, // y: 10
Math.random() * terrainSize - halfSize // z: [-50, 50]
);
break;
}
scene.add(this.sprite);
this.sprite.material.opacity = 0;
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; // 高度为地表+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) {
const newEnemy = new Enemy();
enemies.push(newEnemy);
}
}, 2000); // 每2秒生成一个敌人
// 计时与得分
let timeLeft = 150;
let 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) return; // 如果暂停,不更新计时器
timeLeft--;
score++;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
if (timeLeft <= 0) {
endGame();
}
}, 1000);
// 新增游戏结束函数
function endGame() {
isGamePaused = true;
document.exitPointerLock();
// 显示弹窗
const modal = document.getElementById('gameOverModal');
const finalScore = document.getElementById('finalScore');
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 = [];
// 更新UI
healthDisplay.innerText = `生命值: ${health}`;
bulletDisplay.innerText = `子弹: ${bullets}`;
timerDisplay.innerText = `时间: ${timeLeft} | 分数: ${score}`;
// 隐藏弹窗
document.getElementById('gameOverModal').style.display = 'none';
// 重新启动动画循环
animate();
}
// 为重新开始按钮添加事件监听
document.getElementById('restartButton').addEventListener('click', restartGame);
// 天空贴图
const skyboxLoader = new THREE.CubeTextureLoader();
const skyboxTexture = 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'
]); // 替换为实际天空贴图URL
scene.background = skyboxTexture;
// 动画循环
function animate() {
if (isGamePaused) return; // 如果暂停则跳出循环
requestAnimationFrame(animate);
// WASD移动
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 (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));
// 贴合地形高度
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;
}
// 更新敌人(仅在游戏未暂停时)
if (!isGamePaused) {
enemies.forEach(enemy => enemy.update());
}
renderer.render(scene, camera);
}
animate();
// 自适应窗口大小
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>