横屏手机FPS射击【修复方向】
by BlazeLegend68618 lines15.9 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>横屏FPS(修复方向)</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #111;
font-family: system-ui, sans-serif;
color: #fff;
touch-action: none;
}
#rotateTip{
position:fixed;inset:0;background:#000;z-index:999;
display:flex;align-items:center;justify-content:center;font-size:22px;text-align:center;padding:20px;
}
#game {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(#87ceeb 0%, #b9e5ff 60%, #ffd89b 100%);
}
canvas { display: block; width: 100%; height: 100%; }
/* 准星 */
#crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 14px;
height: 14px;
pointer-events: none;
z-index: 30;
}
#crosshair::before,
#crosshair::after {
content: "";
position: absolute;
background: #0f0;
border-radius: 2px;
}
#crosshair::before {
width: 2px;
height: 14px;
left: 6px;
top: 0;
}
#crosshair::after {
width: 14px;
height: 2px;
left: 0;
top: 6px;
}
/* 开镜遮罩 */
#scope {
position: fixed;
inset: 0;
pointer-events: none;
background: radial-gradient(
circle at center,
transparent 20%,
rgba(0, 0, 0, 0.85) 21%,
rgba(0, 0, 0, 0.85) 100%
);
opacity: 0;
transition: opacity 0.12s;
z-index: 20;
}
#scope.on { opacity: 1; }
/* 分数与提示 */
#hud {
position: fixed;
top: 12px;
left: 12px;
font-size: 16px;
background: rgba(0,0,0,0.35);
padding: 6px 10px;
border-radius: 8px;
z-index: 40;
pointer-events: none;
}
#tip {
position: fixed;
top: 12px;
right: 12px;
font-size: 13px;
background: rgba(0,0,0,0.35);
padding: 6px 10px;
border-radius: 8px;
z-index: 40;
pointer-events: none;
}
/* 虚拟摇杆 */
#joystick {
position: fixed;
left: 18px;
bottom: 18px;
width: 130px;
height: 130px;
border-radius: 50%;
background: rgba(255,255,255,0.25);
border: 2px solid rgba(255,255,255,0.45);
z-index: 50;
}
#stick {
position: absolute;
left: 50%;
top: 50%;
width: 55px;
height: 55px;
border-radius: 50%;
background: rgba(255,255,255,0.75);
transform: translate(-50%, -50%);
}
/* 射击按钮 */
#fire {
position: fixed;
right: 22px;
bottom: 22px;
width: 90px;
height: 90px;
border-radius: 50%;
background: radial-gradient(circle, #ff4f4f, #c41e1e);
border: 3px solid #fff;
color: #fff;
font-size: 18px;
font-weight: bold;
z-index: 50;
box-shadow: 0 6px 18px rgba(0,0,0,0.35);
}
#fire.pressed {
transform: scale(0.92);
background: radial-gradient(circle, #ff7777, #e53935);
}
/* 开镜按钮 */
#aim {
position: fixed;
right: 130px;
bottom: 30px;
width: 70px;
height: 70px;
border-radius: 50%;
background: rgba(0,0,0,0.4);
border: 2px solid #fff;
color: #fff;
font-size: 14px;
z-index: 50;
}
#aim.on {
background: #2f80ed;
}
/* 伤害反馈 */
#hit {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 30px;
font-weight: bold;
color: #ff2f2f;
opacity: 0;
z-index: 60;
pointer-events: none;
text-shadow: 0 0 10px #000;
}
#hit.on {
animation: hit 0.25s ease-out;
}
@keyframes hit {
0% { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
40% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1); }
}
/* 命中闪光 */
#flash {
position: fixed;
inset: 0;
background: radial-gradient(circle at center, rgba(255,80,80,0.35), transparent 40%);
opacity: 0;
pointer-events: none;
z-index: 35;
}
#flash.on {
animation: flash 0.15s ease-out;
}
@keyframes flash {
0% { opacity: 0; }
30% { opacity: 1; }
100% { opacity: 0; }
}
</style>
</head>
<body>
<div id="rotateTip">🔄 请把手机横过来!</div>
<div id="game"></div>
<div id="scope"></div>
<div id="crosshair"></div>
<div id="hud">得分:<span id="score">0</span></div>
<div id="tip">左摇杆移动|右滑屏幕瞄准|开镜+射击</div>
<div id="joystick"><div id="stick"></div></div>
<button id="aim">开镜</button>
<button id="fire">射击</button>
<div id="hit">命中!</div>
<div id="flash"></div>
<script type="module">
import * as THREE from 'three';
// ========== 基础场景 ==========
const rotateTip = document.getElementById('rotateTip');
const container = document.getElementById('game');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0xffd89b, 40, 120);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 200);
camera.position.set(0, 1.6, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
container.appendChild(renderer.domElement);
//横竖屏检测
function checkOrientation(){
if(window.innerWidth > window.innerHeight){
rotateTip.style.display = 'none';
}else{
rotateTip.style.display = 'flex';
}
}
checkOrientation();
window.addEventListener('resize',checkOrientation);
// ========== 光照 ==========
const hemi = new THREE.HemisphereLight(0xb9e5ff, 0xffd89b, 0.6);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffffff, 1.1);
sun.position.set(20, 40, 15);
sun.castShadow = true;
sun.shadow.mapSize.set(1024, 1024);
sun.shadow.camera.left = -40;
sun.shadow.camera.right = 40;
sun.shadow.camera.top = 40;
sun.shadow.camera.bottom = -40;
scene.add(sun);
// ========== 地面与彩色环境 ==========
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(200, 200),
new THREE.MeshStandardMaterial({ color: 0x4caf50 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// 彩色地板格
const grid = new THREE.GridHelper(200, 50, 0x2e7d32, 0x81c784);
grid.position.y = 0.01;
scene.add(grid);
// 彩色障碍物
function addColorBox(x, z, color) {
const box = new THREE.Mesh(
new THREE.BoxGeometry(3, 3, 3),
new THREE.MeshStandardMaterial({ color })
);
box.position.set(x, 1.5, z);
box.castShadow = true;
box.receiveShadow = true;
scene.add(box);
}
addColorBox(-10, -8, 0xf44336);
addColorBox(12, -6, 0xffc107);
addColorBox(-6, 10, 0x2196f3);
addColorBox(14, 12, 0x9c27b0);
addColorBox(0, 18, 0x00bcd4);
// 远处彩色圆柱装饰
for (let i = 0; i < 12; i++) {
const r = 25 + Math.random() * 30;
const a = (i / 12) * Math.PI * 2;
const c = new THREE.Mesh(
new THREE.CylinderGeometry(1.2, 1.2, 4, 16),
new THREE.MeshStandardMaterial({ color: new THREE.Color().setHSL(Math.random(), 0.7, 0.6) })
);
c.position.set(Math.cos(a) * r, 2, Math.sin(a) * r);
c.castShadow = true;
scene.add(c);
}
// ========== 枪械模型 ==========
const gunGroup = new THREE.Group();
gunGroup.position.set(0.35, -0.25, -0.55);
camera.add(gunGroup);
scene.add(camera);
function addGunPart(w, h, d, color, x, y, z) {
const m = new THREE.Mesh(
new THREE.BoxGeometry(w, h, d),
new THREE.MeshStandardMaterial({ color, metalness: 0.6, roughness: 0.3 })
);
m.position.set(x, y, z);
m.castShadow = false;
gunGroup.add(m);
return m;
}
// 枪身
const body = addGunPart(0.12, 0.18, 0.45, 0x222222, 0, 0, 0);
// 枪管
const barrel = addGunPart(0.06, 0.06, 0.35, 0x555555, 0, 0.04, -0.3);
// 弹匣
const mag = addGunPart(0.08, 0.22, 0.08, 0x333333, 0, -0.14, 0.05);
// 瞄准镜
const scopeModel = addGunPart(0.05, 0.08, 0.12, 0x111111, 0, 0.12, -0.05);
// 握把
const grip = addGunPart(0.1, 0.2, 0.12, 0x4a2c0a, 0, -0.08, 0.18);
// 枪口火光
const muzzle = new THREE.Mesh(
new THREE.SphereGeometry(0.08, 10, 10),
new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0 })
);
muzzle.position.set(0, 0.04, -0.5);
gunGroup.add(muzzle);
// ========== 人形靶子 ==========
const targets = [];
function createHumanoid(color) {
const group = new THREE.Group();
const head = new THREE.Mesh(
new THREE.SphereGeometry(0.22, 16, 16),
new THREE.MeshStandardMaterial({ color })
);
head.position.y = 1.7;
group.add(head);
const torso = new THREE.Mesh(
new THREE.CapsuleGeometry(0.25, 0.55, 6, 12),
new THREE.MeshStandardMaterial({ color: 0xffffff })
);
torso.position.y = 1.15;
group.add(torso);
const armMat = new THREE.MeshStandardMaterial({ color });
const leftArm = new THREE.Mesh(new THREE.CapsuleGeometry(0.08, 0.45, 6, 10), armMat);
leftArm.position.set(-0.42, 1.15, 0);
group.add(leftArm);
const rightArm = new THREE.Mesh(new THREE.CapsuleGeometry(0.08, 0.45, 6, 10), armMat);
rightArm.position.set(0.42, 1.15, 0);
group.add(rightArm);
const legMat = new THREE.MeshStandardMaterial({ color: 0x1565c0 });
const leftLeg = new THREE.Mesh(new THREE.CapsuleGeometry(0.1, 0.5, 6, 10), legMat);
leftLeg.position.set(-0.14, 0.45, 0);
group.add(leftLeg);
const rightLeg = new THREE.Mesh(new THREE.CapsuleGeometry(0.1, 0.5, 6, 10), legMat);
rightLeg.position.set(0.14, 0.45, 0);
group.add(rightLeg);
group.traverse(m => {
if (m.isMesh) {
m.castShadow = true;
m.receiveShadow = true;
}
});
return group;
}
function spawnTarget() {
const colors = [0xff4f4f, 0x4f8fff, 0xffd24f, 0x4fff8f, 0xff8fd1];
const human = createHumanoid(colors[Math.floor(Math.random() * colors.length)]);
const angle = Math.random() * Math.PI * 2;
const distance = 18 + Math.random() * 22;
human.position.set(Math.cos(angle) * distance, 0, Math.sin(angle) * distance);
human.lookAt(camera.position.x, human.position.y, camera.position.z);
human.userData = {
hp: 1,
speed: 0.01 + Math.random() * 0.015,
dir: Math.random() * Math.PI * 2,
changeTimer: Math.random() * 120
};
scene.add(human);
targets.push(human);
}
for (let i = 0; i < 6; i++) spawnTarget();
// ========== 移动与视角【修复方向!】 ==========
const moveDir = { x: 0, z: 0 };
let yaw = 0, pitch = 0;
const lookSpeed = 0.0035;
const moveSpeed = 0.08;
// 虚拟摇杆
const joystick = document.getElementById('joystick');
const stick = document.getElementById('stick');
let joyTouchId = null;
joystick.addEventListener('touchstart', e => {
e.preventDefault();
joyTouchId = e.touches[0].identifier;
}, { passive: false });
joystick.addEventListener('touchmove', e => {
e.preventDefault();
for (const t of e.touches) {
if (t.identifier !== joyTouchId) continue;
const rect = joystick.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
let dx = t.clientX - cx;
let dy = t.clientY - cy;
const r = rect.width / 2;
const len = Math.hypot(dx, dy);
if (len > r) { dx = dx / len * r; dy = dy / len * r; }
stick.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
moveDir.x = dx / r;
moveDir.z = dy / r;
}
}, { passive: false });
joystick.addEventListener('touchend', () => {
joyTouchId = null;
stick.style.transform = 'translate(-50%, -50%)';
moveDir.x = 0;
moveDir.z = 0;
});
// 右半屏滑动视角【✅修复方向,正常FPS手感】
let lastTouch = null;
const lookArea = document.createElement('div');
lookArea.style.cssText = 'position:fixed;right:0;top:0;width:60%;height:100%;z-index:40;';
document.body.appendChild(lookArea);
lookArea.addEventListener('touchmove', e => {
e.preventDefault();
const t = e.touches[0];
if (lastTouch) {
// ↓ 这里改了正负号!滑动方向正常了
yaw += (t.clientX - lastTouch.clientX) * lookSpeed;
pitch += (t.clientY - lastTouch.clientY) * lookSpeed;
pitch = Math.max(-1.3, Math.min(1.3, pitch));
}
lastTouch = { clientX: t.clientX, clientY: t.clientY };
}, { passive: false });
lookArea.addEventListener('touchend', () => lastTouch = null);
// ========== 开镜 ==========
const aimBtn = document.getElementById('aim');
const scope = document.getElementById('scope');
let aiming = false;
function setAim(on) {
aiming = on;
if (on) {
aimBtn.classList.add('on');
scope.classList.add('on');
camera.fov = 40;
gunGroup.position.set(0.25, -0.2, -0.45);
} else {
aimBtn.classList.remove('on');
scope.classList.remove('on');
camera.fov = 75;
gunGroup.position.set(0.35, -0.25, -0.55);
}
camera.updateProjectionMatrix();
}
aimBtn.addEventListener('click', e => {
e.stopPropagation();
setAim(!aiming);
});
// ========== 射击 ==========
const fireBtn = document.getElementById('fire');
const scoreEl = document.getElementById('score');
const hitText = document.getElementById('hit');
const flash = document.getElementById('flash');
let score = 0;
let lastShot = 0;
function shoot() {
const now = performance.now();
if (now - lastShot < 220) return;
lastShot = now;
// 后坐力
gunGroup.position.z += 0.08;
gunGroup.rotation.x -= 0.08;
setTimeout(() => {
gunGroup.position.z = aiming ? -0.45 : -0.55;
gunGroup.rotation.x = 0;
}, 90);
// 枪口火光
muzzle.material.opacity = 1;
setTimeout(() => muzzle.material.opacity = 0, 100);
// 射线检测
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
const allMeshes = [];
targets.forEach(t => t.traverse(c => { if (c.isMesh) allMeshes.push(c); }));
const hits = raycaster.intersectObjects(allMeshes, false);
if (hits.length) {
let obj = hits[0].object;
while (obj.parent && !targets.includes(obj)) obj = obj.parent;
if (targets.includes(obj)) {
obj.userData.hp -= 1;
if (obj.userData.hp <= 0) {
scene.remove(obj);
const idx = targets.indexOf(obj);
targets.splice(idx, 1);
score++;
scoreEl.textContent = score;
spawnTarget();
}
hitText.classList.remove('on');
void hitText.offsetWidth;
hitText.classList.add('on');
flash.classList.remove('on');
void flash.offsetWidth;
flash.classList.add('on');
}
}
}
fireBtn.addEventListener('touchstart', e => {
e.preventDefault();
fireBtn.classList.add('pressed');
shoot();
}, { passive: false });
fireBtn.addEventListener('touchend', e => {
e.preventDefault();
fireBtn.classList.remove('pressed');
});
// ========== 靶子AI ==========
function updateTargets() {
for (const t of targets) {
t.userData.changeTimer--;
if (t.userData.changeTimer <= 0) {
t.userData.dir = Math.random() * Math.PI * 2;
t.userData.changeTimer = 80 + Math.random() * 120;
}
t.position.x += Math.sin(t.userData.dir) * t.userData.speed;
t.position.z += Math.cos(t.userData.dir) * t.userData.speed;
// 保持在场地内
if (Math.hypot(t.position.x, t.position.z) > 50) {
t.position.x *= 0.9;
t.position.z *= 0.9;
}
t.lookAt(camera.position.x, t.position.y, camera.position.z);
}
}
// ========== 主循环 ==========
function animate() {
requestAnimationFrame(animate);
camera.rotation.order = 'YXZ';
camera.rotation.y = yaw;
camera.rotation.x = pitch;
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
forward.y = 0;
forward.normalize();
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
camera.position.addScaledVector(forward, moveDir.z * moveSpeed);
camera.position.addScaledVector(right, moveDir.x * moveSpeed);
updateTargets();
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>
Game Source: 横屏手机FPS射击【修复方向】
Creator: BlazeLegend68
Libraries: three
Complexity: complex (618 lines, 15.9 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: fps-blazelegend68" to link back to the original. Then publish at arcadelab.ai/publish.