🎮ArcadeLab

移动端 FPS

by PrismBunny81
226 lines6.1 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<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>
*{box-sizing:border-box;margin:0;padding:0}
body{overflow:hidden;background:#111;touch-action:none}
canvas{display:block;background:#222}
#ui{position:absolute;top:0;left:0;width:100%;height:100%}
#joystick{position:absolute;bottom:40px;left:30px;width:140px;height:140px;border-radius:50%;background:rgba(255,255,255,0.15);border:2px solid rgba(255,255,255,0.3)}
#stick{position:absolute;width:60px;height:60px;border-radius:50%;background:rgba(255,255,255,0.4);left:40px;top:40px}
#fireBtn{position:absolute;bottom:50px;right:40px;width:90px;height:90px;border-radius:50%;background:rgba(220,40,40,0.6);color:white;font-weight:bold;border:none;font-size:18px}
#cross{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;font-size:32px;pointer-events:none}
#info{position:absolute;top:12px;left:12px;color:#fff;font-size:18px;font-family:system-ui}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui">
  <div id="info">击杀:0</div>
  <div id="cross">+</div>
  <div id="joystick"><div id="stick"></div></div>
  <button id="fireBtn">射击</button>
</div>

<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const info = document.getElementById("info");

function resize(){
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
resize();
window.addEventListener("resize", resize);

// 地图,1=墙
const map = [
  [1,1,1,1,1,1,1,1,1,1,1,1],
  [1,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,1,1,0,0,0,0,1,1,0,1],
  [1,0,1,0,0,0,0,0,0,1,0,1],
  [1,0,0,0,0,1,1,0,0,0,0,1],
  [1,0,0,0,0,1,1,0,0,0,0,1],
  [1,0,1,0,0,0,0,0,0,1,0,1],
  [1,0,1,1,0,0,0,0,1,1,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,1],
  [1,1,1,1,1,1,1,1,1,1,1,1],
];
const mapW = map[0].length;
const mapH = map.length;

// 玩家
let px = 4.2, py = 4.2, pa = 0;
const fov = Math.PI / 3;

// 敌人
let enemies = [];
let kills = 0;
for(let i = 0; i < 5; i++){
  enemies.push({
    x: 1.2 + Math.random() * 9,
    y: 1.2 + Math.random() * 7,
    alive:true
  })
}

// ====== 摇杆 ======
const joystick = document.getElementById("joystick");
const stick = document.getElementById("stick");
let joy = {dx:0, dy:0, active:false};
let joyCenter = {x:70, y:70};

joystick.addEventListener("touchstart", (e)=>{
  e.preventDefault();
  joy.active = true;
})
joystick.addEventListener("touchmove", (e)=>{
  e.preventDefault();
  const t = e.touches[0];
  const rect = joystick.getBoundingClientRect();
  let x = t.clientX - rect.left;
  let y = t.clientY - rect.top;
  let dx = x - joyCenter.x;
  let dy = y - joyCenter.y;
  const r = 50;
  const dist = Math.hypot(dx, dy);
  if(dist > r){
    dx = dx / dist * r;
    dy = dy / dist * r;
  }
  stick.style.left = (joyCenter.x + dx - 30)+"px";
  stick.style.top = (joyCenter.y + dy - 30)+"px";
  joy.dx = dx;
  joy.dy = dy;
})
joystick.addEventListener("touchend", ()=>{
  joy.active = false;
  stick.style.left = "40px";
  stick.style.top = "40px";
  joy.dx = 0;
  joy.dy = 0;
})

// ====== 视角滑动 ======
let lookTouchId = null;
let lastTouch = {x:0, y:0};
canvas.addEventListener("touchstart", (e)=>{
  for(const t of e.touches){
    if(t.target === canvas){
      lookTouchId = t.identifier;
      lastTouch.x = t.clientX;
      lastTouch.y = t.clientY;
    }
  }
})
canvas.addEventListener("touchmove", (e)=>{
  for(const t of e.touches){
    if(t.identifier === lookTouchId){
      const dx = t.clientX - lastTouch.x;
      pa += dx * 0.004;
      lastTouch.x = t.clientX;
      lastTouch.y = t.clientY;
    }
  }
})
canvas.addEventListener("touchend", (e)=>{
  for(const t of e.changedTouches){
    if(t.identifier === lookTouchId) lookTouchId = null;
  }
})

// ====== 射击 ======
document.getElementById("fireBtn").addEventListener("touchstart", ()=>{
  shoot();
})

function shoot(){
  for(const en of enemies){
    if(!en.alive) continue;
    const dx = en.x - px;
    const dy = en.y - py;
    const dist = Math.hypot(dx, dy);
    const ang = Math.atan2(dy, dx);
    let diff = ang - pa;
    while(diff > Math.PI) diff -= Math.PI*2;
    while(diff < -Math.PI) diff += Math.PI*2;
    if(Math.abs(diff) < 0.18 && dist < 7){
      en.alive = false;
      kills++;
      info.innerText = "击杀:"+kills;
    }
  }
}

// ====== 碰撞检测 ======
function canGo(x,y){
  if(x < 0.3 || y < 0.3 || x > mapW-0.3 || y>mapH-0.3) return false;
  return map[Math.floor(y)][Math.floor(x)] === 0;
}

// ====== 游戏循环 ======
function loop(){
  // 移动
  if(joy.active){
    const speed = 0.045;
    const mag = Math.hypot(joy.dx, joy.dy);
    if(mag>0){
      const nx = px + Math.cos(pa) * (-joy.dy / mag) * speed;
      const ny = py + Math.sin(pa) * (-joy.dy / mag) * speed;
      if(canGo(nx, py)) px = nx;
      if(canGo(px, ny)) py = ny;
    }
  }

  // ====== 光线投射 3D ======
  const w = canvas.width;
  const h = canvas.height;
  ctx.fillStyle="#222";
  ctx.fillRect(0,0,w,h);

  for(let col = 0; col < w; col++){
    const rayAng = pa - fov/2 + fov * col / w;
    let rx = px, ry = py;
    const step = 0.02;
    let dist = 0;
    while(true){
      rx += Math.cos(rayAng)*step;
      ry += Math.sin(rayAng)*step;
      dist += step;
      if(map[Math.floor(ry)][Math.floor(rx)] === 1) break;
      if(dist>15) break;
    }
    const wallH = Math.min( h * 1.8 / dist, h );
    const shade = Math.min(255, dist*30);
    ctx.fillStyle = `rgb(${255-shade},${220-shade},${200-shade})`;
    ctx.fillRect(col, (h-wallH)/2, 1, wallH);
  }

  // 绘制敌人
  for(const en of enemies){
    if(!en.alive) continue;
    const dx = en.x - px;
    const dy = en.y - py;
    let ang = Math.atan2(dy, dx) - pa;
    while(ang > Math.PI) ang -= Math.PI*2;
    while(ang < -Math.PI) ang += Math.PI*2;
    const d = Math.hypot(dx, dy);
    if(d < 12 && Math.abs(ang) < fov/2){
      const screenX = w/2 + ang * w / fov;
      const size = Math.min( h * 1.4 / d, h );
      ctx.fillStyle="#881111";
      ctx.fillRect(screenX - size/4, (h-size)/2, size/2, size);
    }
  }

  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

Game Source: 移动端 FPS

Creator: PrismBunny81

Libraries: none

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