ð 3D倪空å°å» - æåŠç
by CyberComet321094 lines45.6 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">
<title>ð 3D倪空å°å» - æåŠç</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Microsoft YaHei', sans-serif;
background: #8b0000;
overflow: hidden;
color: #e0e0ff;
}
#gameCanvas {
width: 100vw;
height: 100vh;
display: block;
}
#ui-layer {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 10;
}
/* åªææé®å匹çªå¯ä»¥æ¥æ¶éŒ æ äºä»¶ */
#ui-layer button, .screen { pointer-events: auto; }
/* åæ°ãçåœãåæãæç€ºæåäžæŠæªéŒ æ */
.score, .lives, .crosshair, .controls-hint, #fpsDisplay {
pointer-events: none;
}
.score {
position: absolute;
top: 15px; left: 20px;
font-size: 22px;
color: #00d4ff;
text-shadow: 0 0 10px rgba(0,212,255,0.5);
font-weight: bold;
}
.lives {
position: absolute;
top: 15px; right: 20px;
font-size: 22px;
}
.crosshair {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 36px;
color: rgba(0,212,255,0.7);
}
.screen {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0,0,0,0.85);
}
.screen h1 {
font-size: 3em;
color: #00d4ff;
text-shadow: 0 0 20px rgba(0,212,255,0.5);
margin-bottom: 20px;
}
.screen p {
font-size: 1.2em;
color: #aaa;
margin: 8px 0;
}
.screen button {
margin-top: 30px;
padding: 15px 40px;
font-size: 1.3em;
border: none;
border-radius: 12px;
background: linear-gradient(135deg, #00d4ff, #0066ff);
color: white;
cursor: pointer;
font-family: inherit;
font-weight: bold;
box-shadow: 0 4px 20px rgba(0,212,255,0.3);
transition: all 0.3s;
}
.screen button:hover {
transform: translateY(-3px);
box-shadow: 0 6px 30px rgba(0,212,255,0.5);
}
.hidden { display: none !important; }
.controls-hint {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
font-size: 0.85em;
color: #666;
text-align: center;
}
.key {
display: inline-block;
background: rgba(255,255,255,0.1);
padding: 2px 8px;
border-radius: 4px;
color: #00d4ff;
font-family: monospace;
margin: 0 2px;
}
#fpsDisplay {
position: absolute;
bottom: 15px;
right: 20px;
font-size: 0.8em;
color: #444;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js"
}
}
</script>
</head>
<body>
<div id="gameCanvas"></div>
<div id="ui-layer">
<div class="score">åŸå: <span id="scoreValue">0</span></div>
<div class="lives">â€ïž <span id="livesValue">3</span></div>
<div class="crosshair">+</div>
<div id="startScreen" class="screen">
<h1>ð 倪空å°å»</h1>
<p>WASD ç§»åšé£è¹ | éŒ æ çå | å·Šé®å°å»</p>
<p>廿¯çº¢è²æäººïŒäžèŠè¢«æå°ïŒ</p>
<button id="startBtn">åŒå§æžžæ</button>
</div>
<div id="gameOverScreen" class="screen hidden">
<h1>ð¥ æžžæç»æ</h1>
<p>æç»åŸå: <span id="finalScore">0</span></p>
<button id="restartBtn">åç©äžæ¬¡</button>
</div>
<div class="controls-hint">
<span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> ç§»åš
| <span class="key">éŒ æ </span> çå
| <span class="key">å·Šé®</span> å°å»
</div>
<div id="fpsDisplay">FPS: --</div>
</div>
<script type="module">
import * as THREE from 'three';
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第äžè¯Ÿã3D èå°æå»º
// æ¯äžª 3D æžžæéœéèŠäžäžªåºæ¬ç»ä»¶ïŒ
// 1. SceneïŒåºæ¯ïŒââ è£
ææäžè¥¿ç倧çå
// 2. CameraïŒçžæºïŒââ ç©å®¶ççŒçïŒå³å®çä»ä¹
// 3. RendererïŒæž²æåšïŒââ ç»å®¶ïŒæ3Dç»æ2DæŸç€ºåšå±å¹äž
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ãå®éª 1-1ãä¿®æ¹èæ¯é¢è²
// æ 0x050510 æ¹æå
¶ä»é¢è²ïŒççå€ªç©ºèæ¯åæä»ä¹æ ·
// è¯è¯ïŒ0x000000ïŒçº¯é»ïŒ 0x001100ïŒæ·±ç»¿ïŒ 0x110000ïŒæ·±çº¢ïŒ
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x8b0000);
// ãå®éª 1-2ãä¿®æ¹éŸçæµåºŠ
// 第äºäžªåæ°æ¯éŸçæµåºŠïŒè¶å€§è¶æµ
// è¯è¯ïŒ0.001ïŒåŸæ·¡ïŒ 0.005ïŒåŸæµïŒ 0ïŒæ²¡æéŸïŒ
scene.fog = new THREE.FogExp2(0x8b0000, 0.002);
// åå»ºçžæºïŒPerspectiveCamera æš¡æäººçŒïŒè¿å€§è¿å°
// åæ°ïŒè§éè§åºŠ(床), å®œé«æ¯, æè¿è·çŠ», æè¿è·çŠ»
// ãå®éª 1-3ãä¿®æ¹è§éè§åºŠ
// è¯è¯ïŒ30ïŒæè¿éïŒ 90ïŒå¹¿è§ïŒ 120ïŒé±ŒçŒïŒ
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// â
â
â
ä¿®å€ïŒè®Ÿçœ®çžæºåå§äœçœ®åšé£è¹åæ¹ â
â
â
camera.position.set(0, 5, -15);
// å建枲æåš
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.getElementById('gameCanvas').appendChild(renderer.domElement);
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第äºè¯Ÿãæç©ºèæ¯ - ç²åç³»ç»
// å€ªç©ºäžæåŸå€ææïŒæä»¬çšç²åç³»ç»äžæ¬¡æ§ç»å åé¢
// ç²åç³»ç» = åŸå€å°ç¹ïŒæ¯äžªç¹æäœçœ®(x,y,z)åé¢è²
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
function createStarfield() {
// ãå®éª 2-1ãä¿®æ¹æææ°é
// ææè¶å€è¶å£®è§ïŒäœçµèè¶å¡
// è¯è¯ïŒ500ïŒåŸå°ïŒ 5000ïŒåŸå€ïŒ 10000ïŒè¶
å€ïŒå¯èœå¡ïŒ
const starCount = 3000;
// åå€ååšææäœçœ®çæ°ç»
// æ¯é¢ææéèŠ3䞪æ°åïŒx, y, z
// æä»¥æ°ç»å€§å° = æææ°é à 3
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
// ãå®éª 2-2ãä¿®æ¹ææååžèåŽ
// 2000 衚瀺ææååžåš ±1000 çèåŽå
// è¯è¯ïŒ500ïŒææåŸè¿ïŒ 5000ïŒææåŸè¿ïŒ
const range = 2000;
positions[i * 3] = (Math.random() - 0.5) * range; // x
positions[i * 3 + 1] = (Math.random() - 0.5) * range; // y
positions[i * 3 + 2] = (Math.random() - 0.5) * range; // z
// ãå®éª 2-3ãä¿®æ¹ææé¢è²
// ç°åšæææäžç§é¢è²ïŒçœè²ãæ·¡èãæ·¡é»
// è¯è¯è®©ææææåæåäžç§é¢è²
const colorType = Math.random();
if (colorType < 0.7) {
// çœè²ææ
colors[i * 3] = 1; // R
colors[i * 3 + 1] = 1; // G
colors[i * 3 + 2] = 1; // B
} else if (colorType < 0.85) {
// æ·¡èè²ææ
colors[i * 3] = 0.7;
colors[i * 3 + 1] = 0.8;
colors[i * 3 + 2] = 1;
} else {
// æ·¡é»è²ææ
colors[i * 3] = 1;
colors[i * 3 + 1] = 0.9;
colors[i * 3 + 2] = 0.7;
}
}
const starGeometry = new THREE.BufferGeometry();
starGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
starGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// ãå®éª 2-4ãä¿®æ¹ææå€§å°
// è¯è¯ïŒ1ïŒåŸå°ïŒ 5ïŒåŸå€§ïŒ 10ïŒå·šå€§ïŒ
const starMaterial = new THREE.PointsMaterial({
size: 2,
transparent: true,
opacity: 0.8,
vertexColors: true, // äœ¿çšæä»¬è®Ÿçœ®çé¢è²
blending: THREE.AdditiveBlending // å å åå
ææ
});
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
return stars;
}
const starfield = createStarfield();
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第äžè¯Ÿãç©å®¶é£è¹ - çšå äœäœæç§¯æš
// é£è¹ç±å€äžªåºæ¬å äœäœç»åèæïŒ
// - ConeGeometryïŒåé¥ïŒâ é£è¹äž»äœ
// - BoxGeometryïŒé¿æ¹äœïŒâ ç¿
è
// - SphereGeometryïŒçäœïŒâ 驟驶è±
// - PointLightïŒç¹å
æºïŒâ åŒæåå
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class Player {
constructor(scene) {
this.scene = scene;
this.mesh = new THREE.Group(); // Group å¯ä»¥è£
å€äžªé¶ä»¶
// ========== é£è¹äž»äœ ==========
// ãå®éª 3-1ãä¿®æ¹é£è¹äž»äœåœ¢ç¶
// ConeGeometry(ååŸ, é«åºŠ, åæ®µæ°)
// è¯è¯ïŒConeGeometry(0.5, 5, 4) â æŽå°æŽé¿ïŒ4蟹圢
// è¯è¯ïŒConeGeometry(1.5, 2, 16) â æŽèæŽçïŒ16蟹圢
const bodyGeo = new THREE.ConeGeometry(0.8, 3, 8);
// ãå®éª 3-2ãä¿®æ¹é£è¹é¢è²
// é¢è²æ ŒåŒïŒ0xRRGGBBïŒçº¢ç»¿èïŒ
// è¯è¯ïŒ0xff0000ïŒçº¢ïŒ 0x00ff00ïŒç»¿ïŒ 0xff00ffïŒçŽ«ïŒ
// emissive æ¯èªåå
é¢è²ïŒè®©é£è¹åšæå€ä¹èœçè§
const bodyMat = new THREE.MeshStandardMaterial({
color: 0x0088ff, // åºç¡é¢è²ïŒèè²
emissive: 0x0044aa, // èªåå
é¢è²
emissiveIntensity: 0.5, // åå
区床
metalness: 0.8, // é屿ïŒ0=塿ïŒ1=éåïŒ
roughness: 0.2 // ç²ç³åºŠïŒ0=å
æ»ïŒ1=ç²ç³ïŒ
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.rotation.x = Math.PI / 2; // æèœ¬90床ïŒè®©å饿ååæ¹
body.castShadow = true;
this.mesh.add(body);
// ========== 巊翌 ==========
// ãå®éª 3-3ãä¿®æ¹ç¿
è倧å°
// BoxGeometry(宜, é«, é¿)
// è¯è¯ïŒBoxGeometry(5, 0.3, 2) â æŽå€§æŽå
const leftWingGeo = new THREE.BoxGeometry(2.5, 0.15, 1.2);
const wingMat = new THREE.MeshStandardMaterial({
color: 0x0066cc,
metalness: 0.9,
roughness: 0.3
});
const leftWing = new THREE.Mesh(leftWingGeo, wingMat);
leftWing.position.set(-1.2, -0.3, 0.5); // äœçœ®ïŒå·Šäžæ¹
leftWing.castShadow = true;
this.mesh.add(leftWing);
// ========== å³ç¿Œ ==========
const rightWing = new THREE.Mesh(leftWingGeo, wingMat);
rightWing.position.set(1.2, -0.3, 0.5); // äœçœ®ïŒå³äžæ¹
rightWing.castShadow = true;
this.mesh.add(rightWing);
// ========== é©Ÿé©¶è± ==========
// ãå®éª 3-4ãä¿®æ¹é©Ÿé©¶è±é¢è²
// è¯è¯æ 0x00ffffïŒéè²ïŒæ¹æ 0xffff00ïŒé»è²ïŒ
const cockpitGeo = new THREE.SphereGeometry(0.4, 8, 8);
const cockpitMat = new THREE.MeshStandardMaterial({
color: 0x00ffff,
emissive: 0x00ffff,
emissiveIntensity: 0.3,
transparent: true,
opacity: 0.8
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.3, -0.5);
this.mesh.add(cockpit);
// ========== åŒæåå
==========
// ãå®éª 3-5ãä¿®æ¹åŒæå
é¢è²
// è¯è¯ïŒ0xff0000ïŒçº¢è²ç«ç°ïŒ 0x00ff00ïŒç»¿è²ç«ç°ïŒ
const engineLight = new THREE.PointLight(0x00aaff, 3, 15);
engineLight.position.set(0, -1.5, 0);
this.mesh.add(engineLight);
// ========== åŒæç²åæå°Ÿ ==========
this.engineParticles = this.createEngineTrail();
// â
â
â
å
³é®ä¿®å€ïŒè®©é£è¹å€Žéšæå +z æ¹åïŒæåçžæº/ç©å®¶ïŒ
// é»è®€åé¥å°ç«¯æå -zïŒæèœ¬ 180 床åæå +z
this.mesh.rotation.y = Math.PI;
scene.add(this.mesh);
this.velocity = new THREE.Vector3(0, 0, 0);
// ãå®éª 3-6ãä¿®æ¹é£è¹ç§»åšé床
// è¶å€§é£åŸè¶å¿«
this.speed = 0.3;
}
createEngineTrail() {
// ãå®éª 3-7ãä¿®æ¹åŒææå°Ÿç²åæ°é
// è¯è¯ïŒ20ïŒçæå°ŸïŒ 100ïŒé¿æå°ŸïŒ
const particleCount = 50;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = 0;
positions[i * 3 + 1] = -2;
positions[i * 3 + 2] = 0;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
// ãå®éª 3-8ãä¿®æ¹åŒææå°Ÿé¢è²
color: 0x00aaff,
size: 0.5,
transparent: true,
opacity: 0.6,
blending: THREE.AdditiveBlending
});
const particles = new THREE.Points(geometry, material);
this.mesh.add(particles);
return particles;
}
update(delta, keys, mousePos) {
// å€çé®çèŸå
¥
// ãå®éª 3-9ãä¿®æ¹æ©æŠå
// 0.92 衚瀺æ¯åž§ä¿ç 92% çé床
// è¯è¯ïŒ0.99ïŒåŸæ»ïŒåå°é¢ïŒ 0.8ïŒåŸå¿«åäžïŒ
const friction = 0.92;
if (keys['KeyW'] || keys['ArrowUp']) this.velocity.y += this.speed * delta * 60;
if (keys['KeyS'] || keys['ArrowDown']) this.velocity.y -= this.speed * delta * 60;
if (keys['KeyA'] || keys['ArrowLeft']) this.velocity.x -= this.speed * delta * 60;
if (keys['KeyD'] || keys['ArrowRight']) this.velocity.x += this.speed * delta * 60;
this.velocity.multiplyScalar(friction);
// æŽæ°äœçœ®
this.mesh.position.add(this.velocity.clone().multiplyScalar(delta));
// éå¶é£è¹äžèŠé£åºå±å¹
this.mesh.position.x = Math.max(-20, Math.min(20, this.mesh.position.x));
this.mesh.position.y = Math.max(-15, Math.min(15, this.mesh.position.y));
this.mesh.position.z = 0;
// éŒ æ çåïŒé£è¹åŸ®åŸ®èœ¬åéŒ æ æ¹å
const targetRotationZ = -mousePos.x * 0.3;
const targetRotationX = Math.PI / 2 + mousePos.y * 0.2;
this.mesh.rotation.z += (targetRotationZ - this.mesh.rotation.z) * 0.1;
this.mesh.rotation.x = targetRotationX;
// æŽæ°åŒææå°Ÿ
this.updateEngineTrail();
}
updateEngineTrail() {
const positions = this.engineParticles.geometry.attributes.position.array;
const count = positions.length / 3;
for (let i = count - 1; i > 0; i--) {
positions[i * 3] = positions[(i - 1) * 3] + (Math.random() - 0.5) * 0.1;
positions[i * 3 + 1] = positions[(i - 1) * 3 + 1] - 0.3;
positions[i * 3 + 2] = positions[(i - 1) * 3 + 2] + (Math.random() - 0.5) * 0.1;
}
positions[0] = (Math.random() - 0.5) * 0.3;
positions[1] = -1.8;
positions[2] = (Math.random() - 0.5) * 0.3;
this.engineParticles.geometry.attributes.position.needsUpdate = true;
}
getPosition() {
return this.mesh.position.clone();
}
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第å诟ãå
ç
§ç³»ç»
// 没æå
ïŒ3D äžçå°±æ¯äžçé»ïŒ
// äžç§ç¯å
ïŒ
// - AmbientLightïŒç¯å¢å
ïŒïŒååç
§äº®ææäžè¥¿
// - DirectionalLightïŒæ¹åå
ïŒïŒå倪é³ïŒææ¹åïŒäº§çéŽåœ±
// - PointLightïŒç¹å
æºïŒïŒåç¯æ³¡ïŒä»äžç¹åååšåå
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ãå®éª 4-1ãä¿®æ¹ç¯å¢å
é¢è²
// ç¯å¢å
å³å®æŽäžªåºæ¯çåºæ¬äº®åºŠ
// è¯è¯ïŒ0xffffffïŒçº¯çœïŒ 0x000000ïŒå
šé»ïŒåªé å
¶ä»å
æºïŒ
const ambientLight = new THREE.AmbientLight(0x112244, 0.5);
scene.add(ambientLight);
// ãå®éª 4-2ãä¿®æ¹æ¹åå
ïŒæš¡æææïŒ
// 第äžäžªåæ°æ¯é¢è²ïŒç¬¬äºäžªæ¯åŒºåºŠ
// è¯è¯ïŒ0xffffffïŒçœè²å€ªé³ïŒ 0xffaa00ïŒæ©è²å€ªé³ïŒ
const sunLight = new THREE.DirectionalLight(0xffeedd, 1.5);
sunLight.position.set(50, 50, 50);
sunLight.castShadow = true;
scene.add(sunLight);
// ãå®éª 4-3ãæ·»å æŽå€è£
饰å
æº
// è¿æ¯çŽ«è²æäºå
const nebulaLight = new THREE.PointLight(0xff00ff, 0.5, 100);
nebulaLight.position.set(0, 0, -50);
scene.add(nebulaLight);
// ãå®éª 4-4ãæ·»å äžäžªç»¿è²å
æº
// åæ¶äžé¢äžè¡ç泚éïŒççæä»ä¹ææ
// const greenLight = new THREE.PointLight(0x00ff00, 1, 50);
// greenLight.position.set(20, 10, -30);
// scene.add(greenLight);
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第äºè¯ŸãååŒ¹ç³»ç» + 对象æ±
// å匹éèŠïŒå建 â é£è¡ â 鿝
// åŠææ¯åå匹éœå建æ°å¯¹è±¡ïŒæžžæäŒåå¡ïŒ
// å¯¹è±¡æ± ïŒé¢å
å建äžå å匹ïŒéå€å©çš
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class Bullet {
constructor() {
this.active = false;
// ãå®éª 5-1ãä¿®æ¹å匹倧å°
// SphereGeometry(ååŸ, æ°Žå¹³åæ®µ, åçŽå段)
// è¯è¯ïŒSphereGeometry(0.5, 16, 16) â 倧å匹
const geometry = new THREE.SphereGeometry(0.2, 6, 6);
// ãå®éª 5-2ãä¿®æ¹å匹é¢è²
// è¯è¯ïŒ0xff0000ïŒçº¢è²ååŒ¹ïŒ 0x00ff00ïŒç»¿è²å匹ïŒ
const material = new THREE.MeshBasicMaterial({
color: 0xffee00,
emissive: 0xffaa00,
emissiveIntensity: 1
});
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.visible = false;
scene.add(this.mesh);
// å匹æå°Ÿ
const trailGeo = new THREE.BufferGeometry();
const trailPos = new Float32Array(10 * 3);
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3));
const trailMat = new THREE.PointsMaterial({
// ãå®éª 5-3ãä¿®æ¹æå°Ÿé¢è²
color: 0xffaa00,
size: 0.3,
transparent: true,
opacity: 0.5,
blending: THREE.AdditiveBlending
});
this.trail = new THREE.Points(trailGeo, trailMat);
this.trail.visible = false;
scene.add(this.trail);
}
spawn(position) {
this.active = true;
this.mesh.visible = true;
this.trail.visible = true;
this.mesh.position.copy(position);
// â
â
â
ä¿®å€ïŒå匹å +z æ¹ååå°ïŒé£è¹å€Žéšæ¹åïŒ
this.mesh.position.z += 1;
// é眮æå°Ÿ
const trailPos = this.trail.geometry.attributes.position.array;
for (let i = 0; i < 10; i++) {
trailPos[i * 3] = position.x;
trailPos[i * 3 + 1] = position.y;
trailPos[i * 3 + 2] = position.z - 1;
}
this.trail.geometry.attributes.position.needsUpdate = true;
}
update(delta) {
if (!this.active) return;
// ãå®éª 5-4ãä¿®æ¹å匹é床
// è¶å€§é£åŸè¶å¿«
const bulletSpeed = 3;
const oldPos = this.mesh.position.clone();
// â
â
â
ä¿®å€ïŒå匹å +z æ¹åé£è¡
this.mesh.position.z += bulletSpeed * delta * 60;
// æŽæ°æå°Ÿ
const trailPos = this.trail.geometry.attributes.position.array;
for (let i = 9; i > 0; i--) {
trailPos[i * 3] = trailPos[(i - 1) * 3];
trailPos[i * 3 + 1] = trailPos[(i - 1) * 3 + 1];
trailPos[i * 3 + 2] = trailPos[(i - 1) * 3 + 2];
}
trailPos[0] = oldPos.x;
trailPos[1] = oldPos.y;
trailPos[2] = oldPos.z;
this.trail.geometry.attributes.position.needsUpdate = true;
this.trail.position.copy(this.mesh.position);
// é£åºèŸ¹ç鿝
// â
â
â
ä¿®å€ïŒå匹é£åº +z 蟹ç鿝
if (this.mesh.position.z > 100) {
this.destroy();
}
}
destroy() {
this.active = false;
this.mesh.visible = false;
this.trail.visible = false;
}
}
class BulletPool {
constructor(size) {
// ãå®éª 5-5ãä¿®æ¹å¯¹è±¡æ± 倧å°
// è¶å€§èœåæ¶ååšçå匹è¶å€
// è¯è¯ïŒ10ïŒåŸå°ïŒ 100ïŒåŸå€ïŒ
this.pool = [];
for (let i = 0; i < size; i++) {
this.pool.push(new Bullet());
}
}
get(position) {
const bullet = this.pool.find(b => !b.active);
if (bullet) {
bullet.spawn(position);
return bullet;
}
return null;
}
update(delta) {
this.pool.forEach(b => b.update(delta));
}
getActive() {
return this.pool.filter(b => b.active);
}
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第å
诟ãæäººç³»ç»
// æäºº AI åŸç®åïŒ
// 1. åšè¿å€éæºäœçœ®çæ
// 2. æçç©å®¶é£è¿æ¥
// 3. 被å匹æäžå°±ççž
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class Enemy {
constructor(playerPosition) {
this.active = true;
this.mesh = new THREE.Group();
// ãå®éª 6-1ãä¿®æ¹æäººäž»äœåœ¢ç¶
// è¯è¯ïŒSphereGeometry(2, 8, 8) â 倧ç
// è¯è¯ïŒBoxGeometry(2, 2, 2) â æ¹åæäºº
const bodyGeo = new THREE.SphereGeometry(1, 8, 8);
// ãå®éª 6-2ãä¿®æ¹æäººé¢è²
// è¯è¯ïŒ0x00ff00ïŒç»¿è²æäººïŒ 0x8800ffïŒçŽ«è²æäººïŒ
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xff2244,
emissive: 0xaa0000,
emissiveIntensity: 0.5,
metalness: 0.6,
roughness: 0.4
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.castShadow = true;
this.mesh.add(body);
// 䟧翌
const wingGeo = new THREE.BoxGeometry(2.5, 0.3, 1);
const wingMat = new THREE.MeshStandardMaterial({ color: 0xcc0000 });
const wing = new THREE.Mesh(wingGeo, wingMat);
this.mesh.add(wing);
// çŒç
const eyeGeo = new THREE.SphereGeometry(0.3, 6, 6);
const eyeMat = new THREE.MeshBasicMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 1
});
const leftEye = new THREE.Mesh(eyeGeo, eyeMat);
leftEye.position.set(-0.4, 0.3, -0.7);
this.mesh.add(leftEye);
const rightEye = new THREE.Mesh(eyeGeo, eyeMat);
rightEye.position.set(0.4, 0.3, -0.7);
this.mesh.add(rightEye);
// ãå®éª 6-3ãä¿®æ¹æäººçæäœçœ®
// radius è¶å€§ïŒæäººååžè¶å¹¿
const angle = Math.random() * Math.PI * 2;
const radius = 8 + Math.random() * 15;
// â
â
â
ä¿®å€ïŒæäººåšé£è¹ +z æ¹åïŒåæ¹ïŒçæ
this.mesh.position.set(
Math.cos(angle) * radius,
(Math.random() - 0.5) * 12,
playerPosition.z + 40 + Math.random() * 60
);
// ãå®éª 6-4ãä¿®æ¹æäººé床
// è¶å€§é£åŸè¶å¿«ïŒæžžæè¶éŸ
// è¯è¯ïŒ0.1ïŒåŸæ
¢ïŒ 0.5ïŒåŸå¿«ïŒ 1.0ïŒè¶
å¿«ïŒ
this.speed = 0.2 + Math.random() * 0.3;
scene.add(this.mesh);
}
update(delta, playerPosition) {
if (!this.active) return;
// æçç©å®¶ç§»åš
const direction = new THREE.Vector3().subVectors(
playerPosition, this.mesh.position
).normalize();
this.mesh.position.add(direction.multiplyScalar(this.speed * delta * 60));
// 让æäººé¢åç©å®¶
this.mesh.lookAt(playerPosition);
// ãå®éª 6-5ãä¿®æ¹æäººæèœ¬åšç»
// è¯è¯ïŒdelta * 5ïŒèœ¬åŸå¿«ïŒ delta * 0.5ïŒèœ¬åŸæ
¢ïŒ
this.mesh.rotation.z += delta * 2;
// é£è¿ç©å®¶éæ¯
// â
â
â
ä¿®å€ïŒæäººé£è¿é£è¹ -z æ¹åïŒåæ¹ïŒéæ¯
if (this.mesh.position.z < playerPosition.z - 10) {
this.destroy();
}
}
destroy() {
this.active = false;
scene.remove(this.mesh);
}
}
class EnemyManager {
constructor() {
this.enemies = [];
this.spawnTimer = 0;
// ãå®éª 6-6ãä¿®æ¹æäººçæéŽéïŒç§ïŒ
// è¶å°çæè¶å¿«ïŒæžžæè¶éŸ
// è¯è¯ïŒ0.5ïŒåŸå¿«ïŒ 5ïŒåŸæ
¢ïŒ
this.spawnInterval = 2;
}
update(delta, playerPosition) {
this.spawnTimer += delta;
if (this.spawnTimer >= this.spawnInterval && gameState === 'playing') {
this.spawnTimer = 0;
this.enemies.push(new Enemy(playerPosition));
// éåæ°å¢å ïŒçæé床å å¿«
this.spawnInterval = Math.max(0.5, 2 - score / 500);
}
this.enemies = this.enemies.filter(e => {
if (e.active) {
e.update(delta, playerPosition);
return true;
}
return false;
});
}
getActive() {
return this.enemies.filter(e => e.active);
}
clear() {
this.enemies.forEach(e => e.destroy());
this.enemies = [];
}
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第äžè¯Ÿãççžææ - ç²åç³»ç»
// ççž = åŸå€å°ç²åä»äžå¿ååé¢å
«æ¹é£åºå»
// æ¯äžªç²åæïŒäœçœ®ãé床ãé¢è²ãçåœåŒ
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class Explosion {
constructor(position, color) {
this.active = true;
this.life = 1.0; // çåœåŒïŒä»1æ
¢æ
¢åå°0
// ãå®éª 7-1ãä¿®æ¹ççžç²åæ°é
// è¯è¯ïŒ10ïŒåŸå°ïŒåå°ç«è±ïŒ 100ïŒåŸå€ïŒå倧çè±ïŒ
this.particleCount = 40;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(this.particleCount * 3);
this.velocities = [];
this.colors = new Float32Array(this.particleCount * 3);
const baseColor = new THREE.Color(color);
for (let i = 0; i < this.particleCount; i++) {
// ææç²åä»ççžäžå¿åŒå§
positions[i * 3] = position.x;
positions[i * 3 + 1] = position.y;
positions[i * 3 + 2] = position.z;
// ãå®éª 7-2ãä¿®æ¹ç²å飿£é床
// è¶å€§ççžè¶çç
const speed = 0.3 + Math.random() * 0.5;
// éæºæ¹åïŒçé¢ååååžïŒ
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
this.velocities.push({
x: speed * Math.sin(phi) * Math.cos(theta),
y: speed * Math.sin(phi) * Math.sin(theta),
z: speed * Math.cos(phi)
});
// é¢è²åå
const variation = (Math.random() - 0.5) * 0.3;
this.colors[i * 3] = Math.min(1, baseColor.r + variation);
this.colors[i * 3 + 1] = Math.min(1, baseColor.g + variation);
this.colors[i * 3 + 2] = Math.min(1, baseColor.b + variation);
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(this.colors, 3));
// ãå®éª 7-3ãä¿®æ¹ç²å倧å°
// è¯è¯ïŒ0.5ïŒå°ç²åïŒ 3ïŒå€§ç²åïŒ
const material = new THREE.PointsMaterial({
size: 1.2,
transparent: true,
opacity: 1,
vertexColors: true,
blending: THREE.AdditiveBlending // å å åå
});
this.mesh = new THREE.Points(geometry, material);
scene.add(this.mesh);
// éªå
ææ
this.flash = new THREE.PointLight(color, 5, 20);
this.flash.position.copy(position);
scene.add(this.flash);
this.flashLife = 0.3;
}
update(delta) {
if (!this.active) return;
// ãå®éª 7-4ãä¿®æ¹æ¶å€±é床
// è¶å€§æ¶å€±è¶å¿«
// è¯è¯ïŒdelta * 5ïŒåŸå¿«æ¶å€±ïŒ delta * 0.5ïŒæ
¢æ
¢æ¶å€±ïŒ
this.life -= delta * 2;
this.flashLife -= delta;
this.mesh.material.opacity = Math.max(0, this.life);
this.flash.intensity = this.flashLife * 15;
// æŽæ°ç²åäœçœ®
const positions = this.mesh.geometry.attributes.position.array;
for (let i = 0; i < this.particleCount; i++) {
positions[i * 3] += this.velocities[i].x * delta * 60;
positions[i * 3 + 1] += this.velocities[i].y * delta * 60;
positions[i * 3 + 2] += this.velocities[i].z * delta * 60;
// ãå®éª 7-5ãæ·»å éåææ
// åæ¶äžé¢æ³šéïŒç²åäŒåŸäžæ
// this.velocities[i].y -= delta * 0.5;
}
this.mesh.geometry.attributes.position.needsUpdate = true;
if (this.life <= 0) {
this.destroy();
}
}
destroy() {
this.active = false;
scene.remove(this.mesh);
scene.remove(this.flash);
}
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第å
«è¯Ÿãç¢°ææ£æµ
// ç»æ¯äžªç©äœå¥äžäžªçäžè§ç"å
åŽç"
// çå碰çå = æå°äºïŒ
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
function checkCollision(obj1, obj2) {
// ãå®éª 8-1ãäºè§£å
åŽç
// Box3 æ¯äžäžªé¿æ¹äœïŒå奜å
äœæŽäžªç©äœ
// èœç¶äžå€ç²Ÿç¡®ïŒäœè®¡ç®åŸå¿«ïŒ
const box1 = new THREE.Box3().setFromObject(obj1);
const box2 = new THREE.Box3().setFromObject(obj2);
return box1.intersectsBox(box2);
}
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第ä¹è¯ŸãèŸå
¥å€ç
// çå¬é®çåéŒ æ äºä»¶
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
const keys = {};
const mousePos = { x: 0, y: 0 };
let mouseDown = false;
let lastShotTime = 0;
// ãå®éª 9-1ãä¿®æ¹å°å»éŽéïŒç§ïŒ
// è¶å°å°åŸè¶å¿«
// è¯è¯ïŒ0.05ïŒæºå
³æªïŒ 1ïŒç廿ªïŒ
const shotInterval = 0.15;
window.addEventListener('keydown', (e) => { keys[e.code] = true; });
window.addEventListener('keyup', (e) => { keys[e.code] = false; });
// â
â
â
ä¿®å€ïŒæäºä»¶çå¬åšç»å®å° window èäžæ¯ canvas â
â
â
// å 䞺 canvas å¯èœè¢« UI å±é®æ¡
window.addEventListener('mousemove', (e) => {
const rect = renderer.domElement.getBoundingClientRect();
mousePos.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mousePos.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
});
window.addEventListener('mousedown', () => { mouseDown = true; });
window.addEventListener('mouseup', () => { mouseDown = false; });
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// ã第åè¯Ÿãæžžæäž»åŸªç¯
// æžžæçå¿èïŒæ¯ç§éè·³ 60 次
// æ¯æ¬¡è·³åšåäžä»¶äºïŒå€çèŸå
¥ â æŽæ°æžžæ â ç»å¶ç»é¢
// âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
let player;
let bulletPool;
let enemyManager;
let explosions = [];
let gameState = 'menu'; // menu, playing, gameover
let score = 0;
let lives = 3;
const clock = new THREE.Clock();
function initGame() {
// æž
çæ§å¯¹è±¡
if (player) scene.remove(player.mesh);
if (bulletPool) {
bulletPool.pool.forEach(b => {
scene.remove(b.mesh);
scene.remove(b.trail);
});
}
if (enemyManager) enemyManager.clear();
explosions.forEach(e => e.destroy());
explosions = [];
// å建æ°å¯¹è±¡
player = new Player(scene);
bulletPool = new BulletPool(50);
enemyManager = new EnemyManager();
score = 0;
lives = 3;
gameState = 'playing';
// â
â
â
ä¿®å€ïŒçžæºåšé£è¹åæ¹
camera.position.set(0, 5, -15);
document.getElementById('scoreValue').textContent = score;
document.getElementById('livesValue').textContent = lives;
document.getElementById('startScreen').classList.add('hidden');
document.getElementById('gameOverScreen').classList.add('hidden');
}
// FPS 计ç®
let frameCount = 0;
let lastTime = performance.now();
let fps = 0;
function gameLoop() {
requestAnimationFrame(gameLoop);
const delta = Math.min(clock.getDelta(), 0.1);
// è®¡ç® FPS
frameCount++;
const now = performance.now();
if (now - lastTime >= 1000) {
fps = frameCount;
frameCount = 0;
lastTime = now;
document.getElementById('fpsDisplay').textContent = `FPS: ${fps}`;
}
// æèœ¬æç©ºïŒè®©å€ªç©ºçèµ·æ¥åšåšïŒ
// ãå®éª 10-1ãä¿®æ¹æç©ºæèœ¬é床
// è¯è¯ïŒ0.01ïŒåŸæ
¢ïŒ 0.1ïŒåŸå¿«ïŒ
starfield.rotation.y += delta * 0.02;
starfield.rotation.x += delta * 0.01;
if (gameState === 'playing') {
// æŽæ°ç©å®¶
player.update(delta, keys, mousePos);
// çžæºè·éç©å®¶ïŒå¹³æ»æåŒïŒ
// ãå®éª 10-2ãä¿®æ¹çžæºè·éé床
// 0.05 衚瀺æ¯åž§ç§»åš 5% çè·çŠ»
// è¯è¯ïŒ0.01ïŒåŸæ
¢ïŒ 0.2ïŒåŸå¿«ïŒå¯èœæïŒ
// â
â
â
ä¿®å€ïŒçžæºåšé£è¹åæ¹ïŒ-z æ¹åïŒ
const targetCamPos = new THREE.Vector3(
player.mesh.position.x * 0.3,
player.mesh.position.y * 0.3 + 5,
player.mesh.position.z - 15
);
camera.position.lerp(targetCamPos, 0.05);
camera.lookAt(player.mesh.position);
// å°å»
if (mouseDown && now / 1000 - lastShotTime >= shotInterval) {
lastShotTime = now / 1000;
bulletPool.get(player.getPosition());
}
// æŽæ°å匹
bulletPool.update(delta);
// æŽæ°æäºº
enemyManager.update(delta, player.getPosition());
// âââââââââââââââââââââââââââââââââââââââââââââââââââ
// ç¢°ææ£æµ
// âââââââââââââââââââââââââââââââââââââââââââââââââââ
// å匹ææäºº
const activeBullets = bulletPool.getActive();
const activeEnemies = enemyManager.getActive();
for (const bullet of activeBullets) {
for (const enemy of activeEnemies) {
if (checkCollision(bullet.mesh, enemy.mesh)) {
bullet.destroy();
enemy.destroy();
// ãå®éª 10-3ãä¿®æ¹ççžé¢è²
// è¯è¯ïŒ0x00ff00ïŒç»¿è²ççžïŒ 0xff00ffïŒçŽ«è²ççžïŒ
explosions.push(new Explosion(enemy.mesh.position, 0xff6600));
// ãå®éª 10-4ãä¿®æ¹åŸå
// è¯è¯ïŒ1ïŒåŸå°ïŒ 100ïŒåŸå€ïŒ
score += 10;
document.getElementById('scoreValue').textContent = score;
break;
}
}
}
// æäººæç©å®¶
for (const enemy of activeEnemies) {
if (checkCollision(player.mesh, enemy.mesh)) {
enemy.destroy();
explosions.push(new Explosion(enemy.mesh.position, 0xff0000));
lives--;
document.getElementById('livesValue').textContent = lives;
// å±å¹éåš
camera.position.x += (Math.random() - 0.5) * 2;
camera.position.y += (Math.random() - 0.5) * 2;
if (lives <= 0) {
gameState = 'gameover';
setTimeout(() => {
document.getElementById('finalScore').textContent = score;
document.getElementById('gameOverScreen').classList.remove('hidden');
}, 500);
}
break;
}
}
}
// æŽæ°ççž
explosions = explosions.filter(e => {
if (e.active) {
e.update(delta);
return true;
}
return false;
});
// ç»å¶ç»é¢
renderer.render(scene, camera);
}
// çªå£å€§å°è°æŽ
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// å¯åšæžžæåŸªç¯
gameLoop();
// â
â
â
ä¿®å€ïŒçš addEventListener ç»å®æé®ïŒèäžæ¯ onclick â
â
â
// å 䞺 module äœçšåäžçåœæ°åš HTML onclick äžäžå¯è§
document.getElementById('startBtn').addEventListener('click', initGame);
document.getElementById('restartBtn').addEventListener('click', initGame);
</script>
</body>
</html>
Game Source: ð 3D倪空å°å» - æåŠç
Creator: CyberComet32
Libraries: three
Complexity: complex (1094 lines, 45.6 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: 3d-cybercomet32" to link back to the original. Then publish at arcadelab.ai/publish.