🎮ArcadeLab

手机 FPS 兼容版

by FlashMaker22
372 lines13.2 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>手机 FPS 兼容版</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#000;font-family:sans-serif;user-select:none;-webkit-user-select:none;}
  canvas{display:block;touch-action:none;}
  #hud{position:fixed;top:10px;left:12px;color:#fff;font-size:17px;z-index:10;text-shadow:0 0 4px #000;}
  #hpBox{position:fixed;top:12px;right:12px;width:130px;height:14px;border:1px solid #fff6;background:#0008;border-radius:7px;overflow:hidden;z-index:10;}
  #hpBar{height:100%;width:100%;background:linear-gradient(90deg,#ff3b3b,#ff8a00);}
  #ammo{position:fixed;right:20px;bottom:175px;color:#fff;font-size:20px;font-weight:bold;z-index:10;text-shadow:0 0 5px #000;}
  #crosshair{position:fixed;left:50%;top:50%;width:22px;height:22px;margin:-11px 0 0 -11px;z-index:10;pointer-events:none;}
  #crosshair:before,#crosshair:after{content:"";position:absolute;background:#0f0;}
  #crosshair:before{left:10px;top:0;width:2px;height:22px;}
  #crosshair:after{top:10px;left:0;height:2px;width:22px;}
  .btn{position:fixed;z-index:20;border:1px solid #fff8;background:#0008;color:#fff;border-radius:12px;font-size:16px;touch-action:none;}
  .move{width:58px;height:58px;}
  #forward{left:70px;bottom:150px;}
  #back{left:70px;bottom:60px;}
  #left{left:10px;bottom:105px;}
  #right{left:130px;bottom:105px;}
  #shootBtn{right:24px;bottom:70px;width:92px;height:92px;border-radius:50%;font-size:20px;background:#c81e1e99;}
  #reloadBtn{right:128px;bottom:70px;width:66px;height:66px;border-radius:50%;font-size:15px;background:#1e50c899;}
  #flash{position:fixed;inset:0;background:#f00;opacity:0;pointer-events:none;z-index:15;transition:opacity .15s;}
  #over{position:fixed;inset:0;background:#000d;color:#fff;display:none;flex-direction:column;align-items:center;justify-content:center;z-index:30;font-size:22px;text-align:center;gap:14px;}
</style>
<script src="https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js"></script>
</head>
<body>
<div id="hud">得分:<span id="score">0</span></div>
<div id="hpBox"><div id="hpBar"></div></div>
<div id="ammo"><span id="ammoNow">30</span>/30</div>
<div id="crosshair"></div>
<div id="flash"></div>

<button class="btn move" id="forward">前</button>
<button class="btn move" id="back">后</button>
<button class="btn move" id="left">左</button>
<button class="btn move" id="right">右</button>
<button class="btn" id="reloadBtn">换弹</button>
<button class="btn" id="shootBtn">射击</button>

<div id="over"><div>你阵亡了</div><div>得分:<span id="finalScore">0</span></div><div style="font-size:15px;color:#bbb;">点屏幕重新开始</div></div>

<script>
window.onload = function () {
  if (typeof THREE === 'undefined') {
    document.body.innerHTML = '<h2 style="color:#fff;padding:20px;">Three.js 没加载出来,请检查网络后刷新</h2>';
    return;
  }

  var scene = new THREE.Scene();
  scene.background = new THREE.Color(0x87a8c8);
  scene.fog = new THREE.Fog(0x87a8c8, 25, 90);

  var camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 300);
  camera.rotation.order = 'YXZ';
  camera.position.set(0, 1.6, 8);
  scene.add(camera);

  var renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(innerWidth, innerHeight);
  renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
  document.body.appendChild(renderer.domElement);

  scene.add(new THREE.HemisphereLight(0xffffff, 0x445544, 1.8));
  var sun = new THREE.DirectionalLight(0xffffff, 1.8);
  sun.position.set(8, 14, 6);
  scene.add(sun);

  var ground = new THREE.Mesh(
    new THREE.PlaneGeometry(200, 200),
    new THREE.MeshStandardMaterial({ color: 0x556655 })
  );
  ground.rotation.x = -Math.PI / 2;
  scene.add(ground);

  // 枪
  var gun = new THREE.Group();
  var gunMat = new THREE.MeshStandardMaterial({ color: 0x2b2b2b });
  var barrel = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.07, 0.5), gunMat);
  barrel.position.set(0, 0, -0.26);
  var body = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.14, 0.28), gunMat);
  body.position.set(0, -0.05, 0);
  var grip = new THREE.Mesh(new THREE.BoxGeometry(0.08, 0.2, 0.1), gunMat);
  grip.position.set(0, -0.16, 0.11);
  grip.rotation.x = 0.22;
  gun.add(barrel, body, grip);
  gun.position.set(0.22, -0.2, -0.45);
  camera.add(gun);

  // 障碍物
  var hittable = [];
  var boxMat = new THREE.MeshStandardMaterial({ color: 0x8a6b4a });
  var boxGeo = new THREE.BoxGeometry(2, 2.4, 2);
  for (var i = 0; i < 12; i++) {
    var b = new THREE.Mesh(boxGeo, boxMat);
    var x, z, ok;
    do {
      x = (Math.random() - 0.5) * 60;
      z = (Math.random() - 0.5) * 70 - 5;
      ok = Math.sqrt(x * x + (z - 8) * (z - 8)) > 8;
    } while (!ok);
    b.position.set(x, 1.2, z);
    scene.add(b);
    hittable.push(b);
  }

  // 敌人
  var enemies = [];
  var ENEMY_MAX_HP = 3;

  function makeEnemy() {
    var g = new THREE.Group();
    var bodyMat = new THREE.MeshStandardMaterial({ color: 0xcc3333 });
    var eb = new THREE.Mesh(new THREE.CapsuleGeometry(0.36, 0.95, 6, 12), bodyMat);
    eb.position.y = 1.05;
    var headMat = new THREE.MeshStandardMaterial({ color: 0xffcc99 });
    var eh = new THREE.Mesh(new THREE.SphereGeometry(0.28, 14, 14), headMat);
    eh.position.y = 1.82;
    g.add(eb, eh);
    scene.add(g);
    hittable.push(g);
    return { group: g, body: eb, hp: ENEMY_MAX_HP, alive: true, respawn: 0, dir: new THREE.Vector3(), dirTimer: 0, fireCd: 1 + Math.random() * 2, speed: 2.2 + Math.random() * 1.2 };
  }

  function resetEnemy(e) {
    e.alive = true;
    e.hp = ENEMY_MAX_HP;
    e.group.visible = true;
    e.body.material.color.setHex(0xcc3333);
    var a = Math.random() * Math.PI * 2;
    var r = 16 + Math.random() * 22;
    var x = camera.position.x + Math.cos(a) * r;
    var z = camera.position.z + Math.sin(a) * r;
    x = Math.max(-48, Math.min(48, x));
    z = Math.max(-58, Math.min(42, z));
    e.group.position.set(x, 0, z);
    e.fireCd = 1 + Math.random() * 2;
    e.dirTimer = 0;
  }

  for (var i = 0; i < 5; i++) {
    var e = makeEnemy();
    enemies.push(e);
    resetEnemy(e);
  }

  var hp = 100, score = 0, ammo = 30, MAG = 30, reloading = false, reloadTimer = 0, recoil = 0, gameOver = false;

  var scoreEl = document.getElementById('score');
  var hpBar = document.getElementById('hpBar');
  var ammoNow = document.getElementById('ammoNow');
  var flashEl = document.getElementById('flash');
  var overEl = document.getElementById('over');
  var finalScoreEl = document.getElementById('finalScore');

  function updateHud() {
    scoreEl.textContent = score;
    hpBar.style.width = Math.max(0, hp) + '%';
    ammoNow.textContent = ammo;
  }

  // 视角
  var yaw = 0, pitch = 0, lookTouch = null;
  var canvas = renderer.domElement;

  canvas.addEventListener('touchstart', function (ev) {
    if (ev.touches.length === 1) {
      lookTouch = { x: ev.touches[0].clientX, y: ev.touches[0].clientY };
    }
  }, { passive: false });

  canvas.addEventListener('touchmove', function (ev) {
    if (!lookTouch || ev.touches.length !== 1) return;
    ev.preventDefault();
    var t = ev.touches[0];
    yaw -= (t.clientX - lookTouch.x) * 0.005;
    pitch -= (t.clientY - lookTouch.y) * 0.005;
    pitch = Math.max(-1.35, Math.min(1.35, pitch));
    lookTouch.x = t.clientX;
    lookTouch.y = t.clientY;
  }, { passive: false });

  canvas.addEventListener('touchend', function () { lookTouch = null; });

  // 移动按钮
  var keys = { forward: false, back: false, left: false, right: false };
  function bindMove(id, key) {
    var el = document.getElementById(id);
    el.addEventListener('pointerdown', function (ev) { ev.preventDefault(); ev.stopPropagation(); keys[key] = true; });
    el.addEventListener('pointerup', function (ev) { ev.preventDefault(); ev.stopPropagation(); keys[key] = false; });
    el.addEventListener('pointercancel', function (ev) { ev.preventDefault(); keys[key] = false; });
    el.addEventListener('pointerleave', function (ev) { keys[key] = false; });
  }
  bindMove('forward', 'forward');
  bindMove('back', 'back');
  bindMove('left', 'left');
  bindMove('right', 'right');

  // 射击
  var raycaster = new THREE.Raycaster();
  var CENTER = new THREE.Vector2(0, 0);

  function doReload() {
    if (reloading || ammo === MAG) return;
    reloading = true;
    reloadTimer = 1.4;
  }

  document.getElementById('reloadBtn').addEventListener('pointerdown', function (ev) {
    ev.preventDefault(); ev.stopPropagation(); doReload();
  });

  function shoot() {
    if (gameOver || reloading) return;
    if (ammo <= 0) { doReload(); return; }
    ammo--;
    updateHud();
    recoil = 1;

    camera.updateMatrixWorld(true);
    raycaster.setFromCamera(CENTER, camera);
    var hits = raycaster.intersectObjects(hittable, true);
    if (hits.length === 0) return;

    var o = hits[0].object;
    while (o && o.userData.enemyIndex === undefined) {
      // 找到敌人组:敌人组没有 enemyIndex,用位置匹配
      if (o.parent && enemies.some(function(en){return en.group === o;})) break;
      o = o.parent;
    }
    // 上面匹配复杂,改为直接判断命中对象是否属于某个敌人
    var hitEnemy = null;
    for (var i = 0; i < enemies.length; i++) {
      var en = enemies[i];
      if (!en.alive) continue;
      var temp = new THREE.Box3().setFromObject(en.group);
      if (temp.containsPoint(hits[0].point)) { hitEnemy = en; break; }
    }
    if (!hitEnemy) return;

    hitEnemy.hp--;
    hitEnemy.body.material.emissive = new THREE.Color(0xffffff);
    setTimeout(function () { hitEnemy.body.material.emissive = new THREE.Color(0x000000); }, 70);

    if (hitEnemy.hp <= 0) {
      hitEnemy.alive = false;
      hitEnemy.group.visible = false;
      hitEnemy.respawn = 2.5 + Math.random() * 2;
      score += 100;
      updateHud();
    } else {
      hitEnemy.dirTimer = 0.6;
    }
  }

  document.getElementById('shootBtn').addEventListener('pointerdown', function (ev) {
    ev.preventDefault(); ev.stopPropagation(); shoot();
  });

  function damagePlayer(dmg) {
    if (gameOver) return;
    hp -= dmg;
    updateHud();
    flashEl.style.opacity = 0.35;
    setTimeout(function () { flashEl.style.opacity = 0; }, 120);
    if (hp <= 0) {
      hp = 0;
      updateHud();
      gameOver = true;
      finalScoreEl.textContent = score;
      overEl.style.display = 'flex';
    }
  }

  function restart() {
    hp = 100; score = 0; ammo = MAG; reloading = false; reloadTimer = 0; gameOver = false;
    overEl.style.display = 'none';
    camera.position.set(0, 1.6, 8);
    yaw = 0; pitch = 0;
    enemies.forEach(resetEnemy);
    updateHud();
  }
  overEl.addEventListener('pointerdown', function (ev) { ev.preventDefault(); restart(); });

  var clock = new THREE.Clock();
  var tmpV = new THREE.Vector3();
  var UP = new THREE.Vector3(0, 1, 0);

  function animate() {
    requestAnimationFrame(animate);
    var dt = Math.min(clock.getDelta(), 0.05);

    recoil = Math.max(0, recoil - dt * 6);
    gun.position.z = -0.45 + recoil * 0.12;
    gun.rotation.x = recoil * 0.35;

    if (!gameOver) {
      camera.rotation.y = yaw;
      camera.rotation.x = pitch;

      var speed = 5 * dt;
      var fwd = tmpV.set(0, 0, -1).applyAxisAngle(UP, yaw);
      if (keys.forward) camera.position.addScaledVector(fwd, speed);
      if (keys.back) camera.position.addScaledVector(fwd, -speed);
      var right = new THREE.Vector3(1, 0, 0).applyAxisAngle(UP, yaw);
      if (keys.left) camera.position.addScaledVector(right, -speed);
      if (keys.right) camera.position.addScaledVector(right, speed);
      camera.position.y = 1.6;
      camera.position.x = Math.max(-48, Math.min(48, camera.position.x));
      camera.position.z = Math.max(-58, Math.min(42, camera.position.z));

      if (reloading) {
        reloadTimer -= dt;
        if (reloadTimer <= 0) { reloading = false; ammo = MAG; updateHud(); }
      }

      for (var i = 0; i < enemies.length; i++) {
        var en = enemies[i];
        if (!en.alive) {
          en.respawn -= dt;
          if (en.respawn <= 0) resetEnemy(en);
          continue;
        }
        var p = en.group.position;
        var dx = camera.position.x - p.x;
        var dz = camera.position.z - p.z;
        var dist = Math.sqrt(dx * dx + dz * dz);

        en.dirTimer -= dt;
        if (en.dirTimer <= 0) {
          en.dirTimer = 1.2 + Math.random() * 2;
          var a = Math.random() * Math.PI * 2;
          en.dir.set(Math.cos(a), 0, Math.sin(a));
        }

        var mvx = en.dir.x, mvz = en.dir.z;
        if (dist > 13) { mvx = dx / dist; mvz = dz / dist; }
        else if (dist < 5) { mvx = -dx / dist; mvz = -dz / dist; }

        p.x += mvx * en.speed * dt;
        p.z += mvz * en.speed * dt;
        p.x = Math.max(-48, Math.min(48, p.x));
        p.z = Math.max(-58, Math.min(42, p.z));
        en.group.rotation.y = Math.atan2(dx, dz);

        en.fireCd -= dt;
        if (en.fireCd <= 0 && dist < 22) {
          en.fireCd = 1.3 + Math.random() * 1.5;
          if (Math.random() < 0.55) damagePlayer(5 + Math.random() * 5);
        }
      }
    }

    renderer.render(scene, camera);
  }

  updateHud();
  animate();

  addEventListener('resize', function () {
    camera.aspect = innerWidth / innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });
};
</script>
</body>
</html>

Game Source: 手机 FPS 兼容版

Creator: FlashMaker22

Libraries: three

Complexity: complex (372 lines, 13.2 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-flashmaker22" to link back to the original. Then publish at arcadelab.ai/publish.