手机 FPS 兼容版
by FlashMaker22372 lines13.2 KB🛠️ Three.js (3D graphics)
<!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.