<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D FPS 射线落地 绝不穿模 (已修复)</title>
<style>
* {margin:0;padding:0;box-sizing:border-box;}
body {overflow:hidden;background:#000;cursor:none;font-family: Arial, sans-serif;}
canvas {display:block;}
.crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 24px;
pointer-events: none;
z-index: 100;
text-shadow: 0 0 2px #000;
}
.aim-scope {
position: fixed;
inset: 0;
background: radial-gradient(transparent 40%, rgba(0,0,0,0.75) 75%);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
z-index: 98;
}
.gun {
position: fixed;
bottom: 0;
right: 50%;
transform: translateX(50%);
width: 300px;
height: 160px;
background: linear-gradient(135deg, #222 30%, #444 100%);
border-radius: 8px 8px 0 0;
pointer-events: none;
z-index: 99;
box-shadow: 0 -5px 15px rgba(0,0,0,0.5);
}
/* UI界面 */
.ui-container {
position: fixed;
top: 20px;
left: 20px;
z-index: 101;
background: rgba(0, 0, 0, 0.6);
padding: 15px;
border-radius: 8px;
color: white;
font-size: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.health-bar {
width: 200px;
height: 20px;
background: #333;
border-radius: 10px;
overflow: hidden;
margin-top: 5px;
border: 1px solid #555;
}
.health-fill {
height: 100%;
background: linear-gradient(90deg, #ff3300, #ff9900);
width: 100%;
transition: width 0.3s ease;
}
.ammo-display {
margin-top: 10px;
font-size: 18px;
}
.kill-counter {
margin-top: 10px;
font-size: 18px;
}
.damage-number {
position: fixed;
color: #ff5555;
font-size: 20px;
font-weight: bold;
pointer-events: none;
z-index: 102;
animation: floatUp 1s forwards;
}
@keyframes floatUp {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(-50px); opacity: 0; }
}
.game-title {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
color: white;
font-size: 24px;
z-index: 101;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="game-title">3D FPS 游戏</div>
<div class="crosshair">+</div>
<div class="aim-scope" id="aimScope"></div>
<div class="gun"></div>
<div class="ui-container">
<div>生命值</div>
<div class="health-bar">
<div class="health-fill" id="healthFill"></div>
</div>
<div class="ammo-display">弹药: ∞</div>
<div class="kill-counter">击杀: <span id="killCount">0</span></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<script>
let scene, camera, renderer;
let player = {
speed: 0.15,
yVel: 0,
gravity: -0.015,
jumpPower: 0.32,
isGrounded: true,
groundY: 0,
health: 100
};
let keys = {};
let enemies = [];
let bullets = [];
let colliders = [];
let pitch = 0, yaw = 0;
let killCount = 0;
let isAiming = false;
const sensNormal = 0.002;
const sensAim = 0.0008;
let mouseSens = sensNormal;
const aimScope = document.getElementById('aimScope');
const healthFill = document.getElementById('healthFill');
const killCountDisplay = document.getElementById('killCount');
// 射线落地检测
let raycaster = new THREE.Raycaster();
const downDir = new THREE.Vector3(0, -1, 0);
// 粒子系统
let particleSystem;
let particles;
// 创建简单的纹理图案
function createTexture(color, patternType = 'none') {
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
// 填充基础颜色
ctx.fillStyle = '#' + color.getHexString();
ctx.fillRect(0, 0, 32, 32);
// 根据类型添加简单图案
switch(patternType) {
case 'grass':
ctx.fillStyle = '#3a5f3a';
for(let i = 0; i < 64; i++) {
const x = Math.random() * 32;
const y = Math.random() * 32;
ctx.fillRect(x, y, 2, 2);
}
break;
case 'stone':
ctx.fillStyle = '#666666';
for(let i = 0; i < 32; i++) {
const x = Math.floor(Math.random() * 32);
const y = Math.floor(Math.random() * 32);
ctx.fillRect(x, y, 3, 3);
}
break;
case 'dirt':
ctx.fillStyle = '#5d4037';
for(let i = 0; i < 40; i++) {
const x = Math.random() * 32;
const y = Math.random() * 32;
ctx.fillRect(x, y, 2, 2);
}
break;
case 'skin':
// 简单的皮肤纹理
ctx.fillStyle = '#ffcc99';
ctx.fillRect(0, 0, 16, 16);
ctx.fillRect(16, 0, 16, 16);
ctx.fillRect(0, 16, 16, 16);
ctx.fillRect(16, 16, 16, 16);
// 添加一些细节
ctx.fillStyle = '#ffaa66';
for(let i = 0; i < 10; i++) {
const x = Math.random() * 32;
const y = Math.random() * 32;
ctx.fillRect(x, y, 1, 1);
}
break;
case 'cloth':
// 简单的布料纹理
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 1;
for(let i = 0; i < 32; i += 4) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(32, i);
ctx.stroke();
}
for(let i = 0; i < 32; i += 4) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, 32);
ctx.stroke();
}
break;
}
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
return texture;
}
// 创建粒子系统
function createParticleSystem() {
const particleCount = 1000;
particles = new Float32Array(particleCount * 3);
for(let i = 0; i < particleCount * 3; i += 3) {
particles[i] = (Math.random() - 0.5) * 2; // x
particles[i+1] = (Math.random() - 0.5) * 2; // y
particles[i+2] = (Math.random() - 0.5) * 2; // z
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(particles, 3));
const material = new THREE.PointsMaterial({
color: 0x87ceeb,
size: 0.1,
transparent: true,
opacity: 0.6
});
particleSystem = new THREE.Points(geometry, material);
particleSystem.position.y = -10;
scene.add(particleSystem);
}
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 30, 120);
camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(0, 2, 0);
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// 更好的光照
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(50, 80, 30);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 200;
directionalLight.shadow.camera.left = -50;
directionalLight.shadow.camera.right = 50;
directionalLight.shadow.camera.top = 50;
directionalLight.shadow.camera.bottom = -50;
scene.add(ambientLight, directionalLight);
createTerrain();
createParticleSystem();
spawnEnemies(6);
bindEvents();
animate();
}
function createTerrain() {
// 地面纹理
const groundTexture = createTexture(new THREE.Color(0x4a7c4a), 'grass');
const groundMat = new THREE.MeshLambertMaterial({map: groundTexture});
const ground = new THREE.Mesh(new THREE.PlaneGeometry(150,150), groundMat);
ground.rotation.x = -Math.PI/2;
ground.receiveShadow = true;
scene.add(ground);
colliders.push(ground);
// 山丘纹理
for(let i=0;i<25;i++){
let w = 6 + Math.random()*12;
let h = 1.5 + Math.random()*3;
let d = 6 + Math.random()*12;
const dirtTexture = createTexture(new THREE.Color(0x5a7c4a), 'dirt');
let hill = new THREE.Mesh(
new THREE.BoxGeometry(w,h,d),
new THREE.MeshLambertMaterial({map: dirtTexture})
);
hill.position.set((Math.random()-0.5)*90, h/2, (Math.random()-0.5)*90);
hill.castShadow = hill.receiveShadow = true;
scene.add(hill);
colliders.push(hill);
}
// 围墙纹理
for(let i=0;i<18;i++){
const stoneTexture = createTexture(new THREE.Color(0x777777), 'stone');
let wall = new THREE.Mesh(
new THREE.BoxGeometry(4,3.5,1.2),
new THREE.MeshLambertMaterial({map: stoneTexture})
);
wall.position.set((Math.random()-0.5)*85, 1.75, (Math.random()-0.5)*85);
wall.castShadow = true;
scene.add(wall);
colliders.push(wall);
}
// 添加一些装饰性树木
for(let i = 0; i < 15; i++) {
// 树干
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(0.2, 0.3, 2, 8),
new THREE.MeshLambertMaterial({color: 0x8B4513})
);
trunk.position.set((Math.random()-0.5)*80, 1, (Math.random()-0.5)*80);
trunk.castShadow = true;
scene.add(trunk);
// 树冠
const leaves = new THREE.Mesh(
new THREE.SphereGeometry(1.5, 8, 8),
new THREE.MeshLambertMaterial({color: 0x2E8B57})
);
leaves.position.set(trunk.position.x, 2.5, trunk.position.z);
leaves.castShadow = true;
scene.add(leaves);
}
}
// 创建我的世界风格的方块人模型(带纹理)
function createMinecraftCharacter(color = 0xff3333) {
const group = new THREE.Group();
// 皮肤纹理
const skinTexture = createTexture(new THREE.Color(color), 'skin');
const detailTexture = createTexture(new THREE.Color(0x222222), 'cloth');
// 身体 (2x3x1)
const body = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 1.5, 0.5),
new THREE.MeshLambertMaterial({map: skinTexture})
);
body.position.y = 1.0; // 身体位于腰部
body.castShadow = true;
group.add(body);
// 头部 (1x1x1)
const head = new THREE.Mesh(
new THREE.BoxGeometry(0.75, 0.75, 0.75),
new THREE.MeshLambertMaterial({map: skinTexture})
);
head.position.y = 2.0; // 头部位于身体上方
head.castShadow = true;
group.add(head);
// 帽子细节 (可选)
const hat = new THREE.Mesh(
new THREE.BoxGeometry(0.78, 0.78, 0.78),
new THREE.MeshLambertMaterial({map: detailTexture})
);
hat.position.y = 2.0;
hat.castShadow = true;
group.add(hat);
// 左臂 (0.5x1.5x0.5)
const leftArm = new THREE.Mesh(
new THREE.BoxGeometry(0.25, 0.75, 0.25),
new THREE.MeshLambertMaterial({map: skinTexture})
);
leftArm.position.set(0.5, 1.0, 0);
leftArm.castShadow = true;
group.add(leftArm);
// 右臂 (0.5x1.5x0.5)
const rightArm = new THREE.Mesh(
new THREE.BoxGeometry(0.25, 0.75, 0.25),
new THREE.MeshLambertMaterial({map: skinTexture})
);
rightArm.position.set(-0.5, 1.0, 0);
rightArm.castShadow = true;
group.add(rightArm);
// 左腿 (0.5x1.5x0.5)
const leftLeg = new THREE.Mesh(
new THREE.BoxGeometry(0.25, 0.75, 0.25),
new THREE.MeshLambertMaterial({map: skinTexture})
);
leftLeg.position.set(0.2, 0.25, 0);
leftLeg.castShadow = true;
group.add(leftLeg);
// 右腿 (0.5x1.5x0.5)
const rightLeg = new THREE.Mesh(
new THREE.BoxGeometry(0.25, 0.75, 0.25),
new THREE.MeshLambertMaterial({map: skinTexture})
);
rightLeg.position.set(-0.2, 0.25, 0);
rightLeg.castShadow = true;
group.add(rightLeg);
return group;
}
function spawnEnemies(count) {
// 随机颜色列表
const colors = [
0xff5555, // 红色
0x5555ff, // 蓝色
0x55ff55, // 绿色
0xffff55, // 黄色
0xff55ff, // 紫色
0x55ffff, // 青色
0xffaa00, // 橙色
0xaa00ff // 紫罗兰
];
for(let i=0;i<count;i++){
// 随机选择一个颜色
const randomColor = colors[Math.floor(Math.random() * colors.length)];
// 使用新的方块人模型
let enemyGroup = createMinecraftCharacter(randomColor);
// 设置位置(避免生成在障碍物上)
let validPosition = false;
let attempts = 0;
while(!validPosition && attempts < 50) {
enemyGroup.position.set((Math.random()-0.5)*70, 0, (Math.random()-0.5)*70);
// 检查是否与其他物体重叠
validPosition = true;
for(const obj of colliders) {
if(obj.userData.isEnemyCollider) continue; // 跳过其他敌人碰撞框
const box = new THREE.Box3().setFromObject(obj);
const sph = new THREE.Sphere(enemyGroup.position, 0.8);
if(box.intersectsSphere(sph)) {
validPosition = false;
break;
}
}
attempts++;
}
// 添加生命值属性
enemyGroup.hp = 100;
// 添加移动相关属性
enemyGroup.moveSpeed = 0.02 + Math.random() * 0.03; // 随机移动速度
enemyGroup.targetPosition = new THREE.Vector3(
(Math.random()-0.5)*70,
0,
(Math.random()-0.5)*70
);
// 添加到场景和敌人数组
enemies.push(enemyGroup);
scene.add(enemyGroup);
// 为碰撞检测添加包围盒(用于性能优化)
const boundingBox = new THREE.Mesh(
new THREE.BoxGeometry(1.0, 2.5, 1.0), // 包围盒略大于实际模型
new THREE.MeshBasicMaterial({visible: false}) // 不可见
);
boundingBox.position.copy(enemyGroup.position);
boundingBox.position.y += 1.25; // 垂直中心
boundingBox.userData = { isEnemyCollider: true, parent: enemyGroup }; // 标记为敌人碰撞框并关联敌人
scene.add(boundingBox);
colliders.push(boundingBox);
}
}
// 检查敌人移动碰撞
function checkEnemyCollision(position, excludeEnemy = null) {
const enemyRadius = 0.8;
const testPos = new THREE.Vector3(position.x, position.y, position.z);
// 检查与其他敌人碰撞
for(const enemy of enemies) {
if(enemy === excludeEnemy) continue;
const distance = testPos.distanceTo(enemy.position);
if(distance < enemyRadius * 2) {
return true;
}
}
// 检查与地形碰撞
for(const obj of colliders) {
// 跳过敌人自己的碰撞框
if(obj.userData.parent === excludeEnemy) continue;
const box = new THREE.Box3().setFromObject(obj);
const sph = new THREE.Sphere(testPos, enemyRadius);
if(box.intersectsSphere(sph)) {
return true;
}
}
return false;
}
// 更新敌人移动
function updateEnemies() {
for(const enemy of enemies) {
// 计算到目标位置的方向
const direction = new THREE.Vector3();
direction.subVectors(enemy.targetPosition, enemy.position).normalize();
// 计算下一个位置
const nextPosition = enemy.position.clone().add(direction.multiplyScalar(enemy.moveSpeed));
// 检查碰撞
if(!checkEnemyCollision(nextPosition, enemy)) {
// 没有碰撞,移动敌人
enemy.position.copy(nextPosition);
// 更新碰撞框位置
const collider = colliders.find(c => c.userData.parent === enemy);
if(collider) {
collider.position.copy(enemy.position);
collider.position.y = enemy.position.y + 1.25;
}
} else {
// 发生碰撞,设置新目标
enemy.targetPosition.set(
(Math.random()-0.5)*70,
0,
(Math.random()-0.5)*70
);
}
// 检查是否到达目标位置,如果是则设置新目标
if(enemy.position.distanceTo(enemy.targetPosition) < 2) {
enemy.targetPosition.set(
(Math.random()-0.5)*70,
0,
(Math.random()-0.5)*70
);
}
}
}
// 向下射线检测地面
function rayGroundCheck() {
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const intersects = raycaster.intersectObjects(colliders, false);
if(intersects.length > 0) {
// 排除敌人碰撞框
const groundIntersects = intersects.filter(intersect =>
!intersect.object.userData.isEnemyCollider
);
if(groundIntersects.length > 0) {
return groundIntersects[0].point.y;
}
}
return -999;
}
// 水平碰撞
function checkMoveCollision(x,z){
const playerRad = 0.8;
const testPos = new THREE.Vector3(x, camera.position.y, z);
for(let obj of colliders){
// 排除敌人碰撞框
if(obj.userData.isEnemyCollider) continue;
const box = new THREE.Box3().setFromObject(obj);
const sph = new THREE.Sphere(testPos, playerRad);
if(box.intersectsSphere(sph)) return true;
}
return false;
}
function bindEvents() {
document.addEventListener('keydown', e=>{
keys[e.code] = true;
if(e.code==='Space' && player.isGrounded){
player.yVel = player.jumpPower;
player.isGrounded = false;
}
});
document.addEventListener('keyup', e=>keys[e.code]=false);
document.addEventListener('mousemove', e=>{
yaw -= e.movementX * mouseSens;
pitch -= e.movementY * mouseSens;
pitch = Math.max(-Math.PI/3.5, Math.min(Math.PI/3.5, pitch));
camera.rotation.order = 'YXZ';
camera.rotation.y = yaw;
camera.rotation.x = pitch;
});
document.addEventListener('mousedown', e=>{
if(e.button === 0) {
shoot();
}
if(e.button === 2){
e.preventDefault();
isAiming = true;
mouseSens = sensAim;
aimScope.style.opacity = 1;
camera.fov = 55;
camera.updateProjectionMatrix();
}
});
document.addEventListener('mouseup', e=>{
if(e.button === 2){
isAiming = false;
mouseSens = sensNormal;
aimScope.style.opacity = 0;
camera.fov = 75;
camera.updateProjectionMatrix();
}
});
document.addEventListener('contextmenu', e=>e.preventDefault());
document.body.addEventListener('click', ()=>document.body.requestPointerLock());
window.onresize = ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
};
}
function shoot() {
let bullet = new THREE.Mesh(
new THREE.SphereGeometry(0.12,6,6),
new THREE.MeshBasicMaterial({color:0xffff00})
);
bullet.position.copy(camera.position);
let dir = new THREE.Vector3();
camera.getWorldDirection(dir);
bullet.userData.dir = dir;
bullet.userData.spd = 1.5;
bullets.push(bullet);
scene.add(bullet);
}
function movePlayer() {
let dir = new THREE.Vector3();
camera.getWorldDirection(dir);
dir.y = 0;
dir.normalize();
let right = new THREE.Vector3().crossVectors(dir, new THREE.Vector3(0,1,0)).normalize();
let moveVec = new THREE.Vector3(0,0,0);
if(keys.KeyW) moveVec.add(dir.clone().multiplyScalar(player.speed));
if(keys.KeyS) moveVec.add(dir.clone().multiplyScalar(-player.speed));
if(keys.KeyA) moveVec.add(right.clone().multiplyScalar(-player.speed));
if(keys.KeyD) moveVec.add(right.clone().multiplyScalar(player.speed));
// 水平移动碰撞阻挡
let nextX = camera.position.x + moveVec.x;
let nextZ = camera.position.z + moveVec.z;
if(!checkMoveCollision(nextX, nextZ)){
camera.position.x = nextX;
camera.position.z = nextZ;
}
// 重力应用
if (!player.isGrounded || player.yVel !== 0) {
player.yVel += player.gravity;
}
// 更新位置
camera.position.y += player.yVel;
// 射线检测脚下地面高度
const groundY = rayGroundCheck();
const standHeight = groundY + 2.05;
// 检测是否接近地面且正在下降
const isFalling = player.yVel < 0;
const isCloseToGround = camera.position.y <= standHeight;
if(isCloseToGround && isFalling) {
// 确保角色恰好站在地面上
camera.position.y = standHeight;
// 设置为接地状态并停止垂直速度
player.yVel = 0;
player.isGrounded = true;
player.groundY = groundY;
} else if (!isCloseToGround) {
// 如果离开地面,标记为非接地状态
player.isGrounded = false;
}
camera.position.x = THREE.MathUtils.clamp(camera.position.x, -70,70);
camera.position.z = THREE.MathUtils.clamp(camera.position.z, -70,70);
}
function updateBullets(){
for(let i=bullets.length-1;i>=0;i--){
let b = bullets[i];
b.position.addScaledVector(b.userData.dir, b.userData.spd);
for(let j=enemies.length-1;j>=0;j--){
let e = enemies[j];
// 检查子弹是否击中敌人(使用碰撞框位置)
const collider = colliders.find(c => c.userData.parent === e);
if(collider && b.position.distanceTo(collider.position) < 1.0){
e.hp -= 50;
console.log(`击中敌人! 剩余血量: ${e.hp}`); // 调试信息
// 创建伤害数字效果
createDamageNumber(b.position, 50);
scene.remove(b);
bullets.splice(i,1);
// 检查敌人是否死亡
if(e.hp <= 0){
console.log("敌人被击杀!"); // 调试信息
// 移除敌人及其碰撞框
scene.remove(e);
enemies.splice(j,1);
// 找到对应的碰撞框并移除
for(let k=colliders.length-1; k>=0; k--) {
if(colliders[k].userData.parent === e) {
scene.remove(colliders[k]);
colliders.splice(k,1);
break;
}
}
// 更新击杀计数
killCount++;
killCountDisplay.textContent = killCount;
// 生成新敌人
spawnEnemies(1);
}
break;
}
}
}
}
// 创建伤害数字效果
function createDamageNumber(position, damage) {
const damageElement = document.createElement('div');
damageElement.className = 'damage-number';
damageElement.textContent = '-' + damage;
damageElement.style.left = (innerWidth/2 + (Math.random() - 0.5) * 100) + 'px';
damageElement.style.top = (innerHeight/2 + (Math.random() - 0.5) * 100) + 'px';
document.body.appendChild(damageElement);
// 移除元素
setTimeout(() => {
document.body.removeChild(damageElement);
}, 1000);
}
function animate(){
requestAnimationFrame(animate);
// 更新粒子系统
if(particleSystem) {
particleSystem.rotation.y += 0.001;
particleSystem.position.y += 0.01;
if(particleSystem.position.y > 10) {
particleSystem.position.y = -10;
}
}
movePlayer();
updateEnemies(); // 更新敌人移动
updateBullets();
// 更新UI
healthFill.style.width = player.health + '%';
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>