触屏第一视角房间
by SolarBuilder29310 lines11.4 KB🛠️ Three.js (3D graphics)
<!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.