🎮ArcadeLab

横屏手机FPS射击游戏

by BlazeLegend68
601 lines15.3 KB🛠️ Three.js (3D graphics)
▶ Play
<!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>横屏彩色射击</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;
  }
  #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="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 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);

// ========== 光照 ==========
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;
});

// 右半屏滑动视角
let lastTouch = null;
const lookArea = document.createElement('div');
lookArea.style.cssText = 'position:fixed;right:0;top:0;width:60%;height:75%;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 (601 lines, 15.3 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-muqlpgi2" to link back to the original. Then publish at arcadelab.ai/publish.