🎮ArcadeLab

触屏第一视角房间

by SolarBuilder29
310 lines11.4 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; }
  body { overflow: hidden; background: #222; font-family: system-ui; touch-action:none; }
  canvas { display:block; }
  #crosshair {
    position: fixed; left: 50%; top: 50%; transform: translate(-50%,-50%);
    width:12px;height:12px;border:2px solid #fff;border-radius:50%;
    pointer-events:none;opacity:0.8;
  }
  /* 摇杆 */
  #joystickWrap{
    position:fixed;left:20px;bottom:20px;width:130px;height:130px;
    background:rgba(255,255,255,0.25);border-radius:50%;
  }
  #joystick{
    width:50px;height:50px;background:rgba(255,255,255,0.55);border-radius:50%;
    position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  }
  /* 物品栏 */
  #hotbar{
    position:fixed;bottom:20px;left:50%;transform:translateX(-50%);
    display:flex;gap:8px;background:rgba(0,0,0,0.45);padding:8px;border-radius:12px;
  }
  .slot{
    width:56px;height:56px;background:#444;border:3px solid #666;border-radius:10px;
    color:#fff;font-size:12px;text-align:center;display:flex;align-items:center;justify-content:center;
  }
  .slot.active{border-color:#ffcc00;background:#665500;}
  #tip{
    position:fixed;top:12px;left:50%;transform:translateX(-50%);
    background:rgba(0,0,0,0.45);color:#fff;padding:6px 10px;border-radius:6px;font-size:13px;
  }
</style>
</head>
<body>
<div id="tip">左侧摇杆移动,右侧滑动转头,点物品切换,点击屏幕攻击/放方块</div>
<div id="crosshair"></div>
<div id="joystickWrap">
  <div id="joystick"></div>
</div>
<div id="hotbar">
  <div class="slot active" data-item="hand">手</div>
  <div class="slot" data-item="block">方块</div>
  <div class="slot" data-item="bat">棒球棍</div>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://unpkg.com/three@0.160.0/build/three.module.js"
  }
}
</script>
<script type="module">
import * as THREE from 'three';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 1.6, 0);
const renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(devicePixelRatio);
document.body.appendChild(renderer.domElement);

scene.add(new THREE.AmbientLight(0xffffff,0.6));
const dirLight = new THREE.DirectionalLight(0xffffff,0.8);
dirLight.position.set(5,10,7);
scene.add(dirLight);

// 房间
const room = new THREE.Group();
const floor = new THREE.Mesh(new THREE.PlaneGeometry(20,20),new THREE.MeshStandardMaterial({color:0x8b5a2b}));
floor.rotation.x = -Math.PI/2;
room.add(floor);
const wallMat = new THREE.MeshStandardMaterial({color:0xd9b38c});
const wall1 = new THREE.Mesh(new THREE.BoxGeometry(20,6,0.2),wallMat); wall1.position.set(0,3,-10); room.add(wall1);
const wall2 = wall1.clone(); wall2.position.set(0,3,10); room.add(wall2);
const wall3 = new THREE.Mesh(new THREE.BoxGeometry(0.2,6,20),wallMat); wall3.position.set(-10,3,0); room.add(wall3);
const wall4 = wall3.clone(); wall4.position.set(10,3,0); room.add(wall4);
const ceiling = new THREE.Mesh(new THREE.PlaneGeometry(20,20),new THREE.MeshStandardMaterial({color:0xffffff}));
ceiling.rotation.x = Math.PI/2; ceiling.position.y = 6; room.add(ceiling);
scene.add(room);

// 桌子
const tableTop = new THREE.Mesh(new THREE.BoxGeometry(3,0.2,1.2),new THREE.MeshStandardMaterial({color:0x6b4226}));
tableTop.position.set(0,1,-6); room.add(tableTop);
const legGeo = new THREE.BoxGeometry(0.1,1,0.1);
const legMat = new THREE.MeshStandardMaterial({color:0x4a2c16});
[[ -1.4,0.5,-6.55],[1.4,0.5,-6.55],[-1.4,0.5,-5.45],[1.4,0.5,-5.45]].forEach(p=>{
  const l = new THREE.Mesh(legGeo,legMat); l.position.set(...p); room.add(l);
});

// 椅子
const chair = new THREE.Group();
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.6,0.1,0.6),new THREE.MeshStandardMaterial({color:0x333333}));
seat.position.set(4,0.5,-6); chair.add(seat);
for(let x of [-0.25,0.25])for(let z of [-0.25,0.25]){
  const leg = new THREE.Mesh(new THREE.BoxGeometry(0.08,0.5,0.08),new THREE.MeshStandardMaterial({color:0x222222}));
  leg.position.set(4+x,0.25,-6+z); chair.add(leg);
}
scene.add(chair);

// 人物
const person = new THREE.Group();
const head = new THREE.Mesh(new THREE.SphereGeometry(0.3,16,16),new THREE.MeshStandardMaterial({color:0xffdbac}));
head.position.y = 1.7; person.add(head);
const torso = new THREE.Mesh(new THREE.BoxGeometry(0.6,0.8,0.4),new THREE.MeshStandardMaterial({color:0xcc3333}));
torso.position.y = 1.1; person.add(torso);
const armMat = new THREE.MeshStandardMaterial({color:0xffdbac});
const arm1 = new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.08,0.6),armMat); arm1.position.set(-0.4,1.1,0); person.add(arm1);
const arm2 = arm1.clone(); arm2.position.set(0.4,1.1,0); person.add(arm2);
const legMat = new THREE.MeshStandardMaterial({color:0x333366});
const leg1Mesh = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,0.8),legMat); leg1Mesh.position.set(-0.2,0.3,0); person.add(leg1Mesh);
const leg2Mesh = leg1Mesh.clone(); leg2Mesh.position.set(0.2,0.3,0); person.add(leg2Mesh);
person.position.set(-4,0,-4); scene.add(person);

const blocks = new THREE.Group();
scene.add(blocks);

// 视角
let yaw=0, pitch=0;
const maxPitch = 1.4;
let moveX=0, moveZ=0;
let currentItem = "hand";
let attackCooldown = 0;

// 摇杆
const joyWrap = document.getElementById("joystickWrap");
const joy = document.getElementById("joystick");
let joyActive = false;
const joyRadius = joyWrap.clientWidth / 2;
joyWrap.addEventListener("touchstart",e=>{joyActive=true;updateJoy(e.touches[0])});
document.addEventListener("touchmove",e=>{if(joyActive)updateJoy(e.touches[0])});
document.addEventListener("touchend",()=>{joyActive=false;joy.style.left="50%";joy.style.top="50%";moveX=0;moveZ=0});
function updateJoy(touch){
  const rect = joyWrap.getBoundingClientRect();
  const cx = rect.left + rect.width/2;
  const cy = rect.top + rect.height/2;
  let dx = touch.clientX - cx;
  let dy = touch.clientY - cy;
  const dist = Math.hypot(dx,dy);
  if(dist>joyRadius*0.45){
    dx = dx/dist*joyRadius*0.45; dy = dy/dist*joyRadius*0.45;
  }
  joy.style.left = `calc(50% + ${dx}px)`;
  joy.style.top = `calc(50% + ${dy}px)`;
  moveX = dx/(joyRadius*0.45);
  moveZ = dy/(joyRadius*0.45);
}

// 右侧滑动转头
let lookTouchId = null;
let lastX=0,lastY=0;
renderer.domElement.addEventListener("touchstart",e=>{
  for(let t of e.touches){
    if(t.clientX > window.innerWidth/2){
      lookTouchId = t.identifier; lastX = t.clientX; lastY = t.clientY;
    }
  }
});
renderer.domElement.addEventListener("touchmove",e=>{
  for(let t of e.touches){
    if(t.identifier === lookTouchId){
      const dx = t.clientX - lastX;
      const dy = t.clientY - lastY;
      yaw -= dx * 0.003;
      pitch -= dy * 0.003;
      pitch = Math.max(-maxPitch, Math.min(maxPitch, pitch));
      lastX = t.clientX; lastY = t.clientY;
    }
  }
});
renderer.domElement.addEventListener("touchend",e=>{
  for(let t of e.changedTouches){
    if(t.identifier === lookTouchId) lookTouchId = null;
  }
});

// 物品栏
const slots = document.querySelectorAll(".slot");
slots.forEach(s=>{
  s.onclick = ()=>{
    slots.forEach(x=>x.classList.remove("active"));
    s.classList.add("active");
    currentItem = s.dataset.item;
    updateHand();
  }
})

// 手上模型
const handGroup = new THREE.Group();
handGroup.position.set(0.3,-0.25,-0.5);
camera.add(handGroup);
scene.add(camera);
function buildHand(){handGroup.clear();handGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.12,0.12,0.35),new THREE.MeshStandardMaterial({color:0xffdbac})))}
function buildBlock(){handGroup.clear();handGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.25,0.25,0.25),new THREE.MeshStandardMaterial({color:0xa0785a})))}
function buildBat(){
  handGroup.clear();
  const h = new THREE.Mesh(new THREE.CylinderGeometry(0.04,0.04,0.5),new THREE.MeshStandardMaterial({color:0x6b4226}));
  h.rotation.x = Math.PI/2;
  handGroup.add(h);
  const b = new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.08,0.25),new THREE.MeshStandardMaterial({color:0x8b5a2b}));
  b.rotation.x = Math.PI/2; b.position.z = -0.35; handGroup.add(b);
}
function updateHand(){
  if(currentItem==="hand") buildHand();
  if(currentItem==="block") buildBlock();
  if(currentItem==="bat") buildBat();
}
updateHand();

// 点击屏幕:放方块 / 挥棒
renderer.domElement.addEventListener("touchstart",e=>{
  if(e.touches[0].clientX < window.innerWidth/2) return;
  if(attackCooldown>0) return; attackCooldown=0.35;
  if(currentItem==="block") placeBlock();
  else if(currentItem==="bat") swingBat();
})

function raycast(){
  const dir = new THREE.Vector3();
  camera.getWorldDirection(dir);
  const ray = new THREE.Raycaster(camera.position, dir);
  const arr = [];
  blocks.children.forEach(x=>arr.push(x));
  person.traverse(c=>{if(c.isMesh) arr.push(c)});
  return ray.intersectObjects(arr,false);
}
function placeBlock(){
  const hits = raycast();
  if(hits.length>0){
    const p = hits[0].object.position.clone().add(hits[0].face.normal);
    p.y = 0.5;
    const blk = new THREE.Mesh(new THREE.BoxGeometry(1,1,1),new THREE.MeshStandardMaterial({color:0xa0785a}));
    blk.position.copy(p); blocks.add(blk);
  }
}
function swingBat(){
  const start = handGroup.rotation.x;
  const end = -1.2;
  const dur = 0.25;
  let t=0;
  function anim(){
    t+=0.016;
    const k = Math.min(1, t/dur);
    handGroup.rotation.x = start + (end-start)*k;
    if(k<1){requestAnimationFrame(anim)}else{
      const hits = raycast();
      if(hits.length>0){
        const obj = hits[0].object;
        if(blocks.children.includes(obj)) blocks.remove(obj);
        else if([head,torso,arm1,arm2,leg1Mesh,leg2Mesh].includes(obj)){
          person.traverse(c=>{if(c.isMesh){c.material = c.material.clone();c.material.color.set(0xff0000);setTimeout(()=>c.material.color.set(0xffdbac),200)}})
        }
      }
      setTimeout(()=>handGroup.rotation.x=0,100);
    }
  }
  anim();
}

// 移动碰撞
const playerRadius = 0.4;
function collides(x,z){
  if(x<-9.6+playerRadius||x>9.6-playerRadius) return true;
  if(z<-9.6+playerRadius||z>9.6-playerRadius) return true;
  for(const b of blocks.children){
    const dx = x-b.position.x; const dz = z-b.position.z;
    if(Math.abs(dx)<0.5+playerRadius && Math.abs(dz)<0.5+playerRadius) return true;
  }
  return false;
}
function updatePlayer(dt){
  const forward = new THREE.Vector3(-Math.sin(yaw),0,-Math.cos(yaw));
  const right = new THREE.Vector3(Math.cos(yaw),0,-Math.sin(yaw));
  const move = forward.clone().multiplyScalar(-moveZ).add(right.clone().multiplyScalar(moveX));
  if(move.length()>0) move.normalize().multiplyScalar(4.5*dt);
  const nx = camera.position.x + move.x;
  const nz = camera.position.z + move.z;
  if(!collides(nx,camera.position.z)) camera.position.x = nx;
  if(!collides(camera.position.x,nz)) camera.position.z = nz;
}

window.addEventListener("resize",()=>{
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth,innerHeight);
})
const clock = new THREE.Clock();
function loop(){
  const dt = clock.getDelta();
  if(attackCooldown>0) attackCooldown -= dt;
  camera.rotation.order = "YXZ";
  camera.rotation.y = yaw;
  camera.rotation.x = pitch;
  updatePlayer(dt);
  renderer.render(scene,camera);
  requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

Game Source: 触屏第一视角房间

Creator: SolarBuilder29

Libraries: three

Complexity: complex (310 lines, 11.4 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: game-solarbuilder29" to link back to the original. Then publish at arcadelab.ai/publish.