-
流畅的飞行控制:使用鼠标平滑地控制纸飞机的飞行姿态。
-
无限程序化世界:建筑会在您前方无限生成,每次的飞行体验都独一无二。
-
碰撞与破坏:撞向建筑时,它们会碎裂成一堆粒子,带来满足感。
-
飞行辅助:当飞机倾斜角度过大时,会自动平滑地回正,让操控更简单。
-
动态视角:摄像机将始终跟在纸飞机后方,提供沉浸式的飞行体验。


ini
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>纸飞机飞行</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #0d0c1c; /* 深紫色背景 */
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
canvas {
display: block;
}
#info {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
background: rgba(0, 0, 0, 0.5);
border-radius: 15px;
text-align: center;
z-index: 100;
font-size: 16px;
pointer-events: none;
}
</style>
</head>
<body>
<div id="info">移动鼠标控制飞机</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// --- 核心组件 ---
let scene, camera, renderer, clock;
let plane, planeBoundingBox;
const buildings = new Map();
const activeExplosions = [];
// --- 游戏参数 ---
const PLANE_SPEED = 65; // 降低飞行速度
const PLANE_TURN_SPEED = 0.05;
const PLANE_BOUNDS = { x: 40, y: 30 };
const MAX_TILT = Math.PI / 6; // 最大倾斜角度 (30度)
const AUTO_CORRECT_SPEED = 0.04;
// --- 控制变量 ---
const mousePosition = new THREE.Vector2();
const targetPlanePosition = new THREE.Vector3();
// --- 世界生成参数 ---
const CELL_SIZE = 25; // 增大格子尺寸
const RENDER_DISTANCE = 35; // 渲染距离(以CELL为单位)
let lastPlayerCell = { x: 0, z: 0 };
// --- 美学参数 ---
const BUILDING_COLORS = [0x2c3e50, 0x34495e, 0x1f3a4e, 0x4a6fa5];
const EMISSIVE_COLORS = [0xffd700, 0x00f2ff, 0xff4f79, 0x7effa4];
// --- 初始化 ---
function init() {
// 场景和时钟
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0d0c1c, 100, 450); // 调整雾效颜色和距离
clock = new THREE.Clock();
// 相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 15, 30);
camera.lookAt(0, 0, 0);
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
// 光照
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xaaccff, 0.6);
directionalLight.position.set(10, 30, 20);
scene.add(directionalLight);
// 创建纸飞机
plane = createPaperPlane();
scene.add(plane);
planeBoundingBox = new THREE.Box3().setFromObject(plane);
// 事件监听
window.addEventListener('resize', onWindowResize);
document.addEventListener('mousemove', onMouseMove);
// 初始生成建筑
updateWorld();
// 开始动画
animate();
}
// --- 创建纸飞机模型 ---
function createPaperPlane() {
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0, 2, 0, -2, 0, 2, 2, 0, 2, 0, 0, -4, 0, 0, 3,
]);
const indices = [ 3, 2, 0, 3, 0, 1, 3, 1, 4, 3, 4, 2 ];
geometry.setIndex(indices);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.computeVertexNormals();
const material = new THREE.MeshStandardMaterial({
color: 0xffffff, side: THREE.DoubleSide, metalness: 0.2, roughness: 0.8
});
const mesh = new THREE.Mesh(geometry, material);
mesh.scale.set(1.5, 1.5, 1.5);
return mesh;
}
// --- 创建更好看的建筑 ---
function createCoolBuilding(x, z) {
const buildingGroup = new THREE.Group();
buildingGroup.position.set(x, 0, z);
const parts = Math.floor(Math.random() * 3) + 1; // 1到3个部分
let currentHeight = 0;
let lastWidth = Math.random() * 15 + 10;
let lastDepth = Math.random() * 15 + 10;
for (let i = 0; i < parts; i++) {
const height = Math.random() * 50 + 20;
const width = Math.max(lastWidth * (Math.random() * 0.4 + 0.6), 5); // 逐渐变窄
const depth = Math.max(lastDepth * (Math.random() * 0.4 + 0.6), 5);
const geometry = new THREE.BoxGeometry(width, height, depth);
const material = new THREE.MeshStandardMaterial({
color: BUILDING_COLORS[Math.floor(Math.random() * BUILDING_COLORS.length)],
metalness: 0.8,
roughness: 0.4,
emissive: EMISSIVE_COLORS[Math.floor(Math.random() * EMISSIVE_COLORS.length)],
emissiveIntensity: 0.3
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = currentHeight + height / 2;
buildingGroup.add(mesh);
currentHeight += height;
lastWidth = width;
lastDepth = depth;
}
buildingGroup.userData.boundingBox = new THREE.Box3().setFromObject(buildingGroup);
return buildingGroup;
}
// --- 动画循环 ---
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
// 1. 更新飞机位置
plane.position.z -= PLANE_SPEED * delta;
targetPlanePosition.x = mousePosition.x * PLANE_BOUNDS.x;
targetPlanePosition.y = -mousePosition.y * PLANE_BOUNDS.y + 10;
plane.position.lerp(new THREE.Vector3(targetPlanePosition.x, targetPlanePosition.y, plane.position.z), PLANE_TURN_SPEED);
// 2. 更新飞机姿态(倾斜)
const targetTiltZ = -mousePosition.x * MAX_TILT;
const targetTiltX = -mousePosition.y * MAX_TILT;
plane.rotation.z += (targetTiltZ - plane.rotation.z) * PLANE_TURN_SPEED;
plane.rotation.x += (targetTiltX - plane.rotation.x) * PLANE_TURN_SPEED;
// 3. 自动回正
if (Math.abs(plane.rotation.z) > MAX_TILT) {
plane.rotation.z = THREE.MathUtils.lerp(plane.rotation.z, Math.sign(plane.rotation.z) * MAX_TILT, AUTO_CORRECT_SPEED);
}
if (Math.abs(plane.rotation.x) > MAX_TILT) {
plane.rotation.x = THREE.MathUtils.lerp(plane.rotation.x, Math.sign(plane.rotation.x) * MAX_TILT, AUTO_CORRECT_SPEED);
}
plane.rotation.y = 0;
// 4. 更新相机位置
camera.position.x = plane.position.x * 0.2;
camera.position.y = plane.position.y + 10;
camera.position.z = plane.position.z + 25;
camera.lookAt(plane.position);
// 5. 更新世界
updateWorld();
// 6. 碰撞检测
checkCollisions();
// 7. 更新爆炸效果
updateExplosions(delta);
renderer.render(scene, camera);
}
// --- 碰撞检测 ---
function checkCollisions() {
planeBoundingBox.setFromObject(plane);
for (const [key, building] of buildings) {
if (plane.position.distanceTo(building.position) < 150) {
building.userData.boundingBox.setFromObject(building); // 确保包围盒最新
if (planeBoundingBox.intersectsBox(building.userData.boundingBox)) {
// 从建筑群组中获取颜色和尺寸信息
const mainPart = building.children[0];
if (mainPart) {
createExplosion(building.position, mainPart.geometry.parameters.width, mainPart.geometry.parameters.height, mainPart.material.color);
}
scene.remove(building);
buildings.delete(key);
break;
}
}
}
}
// --- 创建爆炸效果 ---
function createExplosion(position, width, height, color) {
const particleCount = 50;
const particles = [];
const particleMaterial = new THREE.MeshBasicMaterial({ color: color });
for (let i = 0; i < particleCount; i++) {
const size = Math.random() * 1.5 + 0.5;
const particleGeometry = new THREE.BoxGeometry(size, size, size);
const particle = new THREE.Mesh(particleGeometry, particleMaterial);
particle.position.copy(position);
particle.userData.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 50, (Math.random() - 0.5) * 50, (Math.random() - 0.5) * 50
);
particle.userData.life = Math.random() * 1.5 + 0.5;
scene.add(particle);
particles.push(particle);
}
activeExplosions.push(particles);
}
// --- 更新爆炸粒子 ---
function updateExplosions(delta) {
for (let i = activeExplosions.length - 1; i >= 0; i--) {
const particles = activeExplosions[i];
for (let j = particles.length - 1; j >= 0; j--) {
const particle = particles[j];
particle.userData.life -= delta;
if (particle.userData.life <= 0) {
scene.remove(particle);
particles.splice(j, 1);
} else {
particle.position.add(particle.userData.velocity.clone().multiplyScalar(delta));
particle.userData.velocity.y -= 20 * delta; // 重力
particle.scale.setScalar(particle.userData.life);
}
}
if (particles.length === 0) {
activeExplosions.splice(i, 1);
}
}
}
// --- 世界生成与销毁 ---
function updateWorld() {
const playerCellX = Math.round(plane.position.x / CELL_SIZE / 2);
const playerCellZ = Math.round(plane.position.z / CELL_SIZE);
if (playerCellX === lastPlayerCell.x && playerCellZ === lastPlayerCell.z) return;
lastPlayerCell = { x: playerCellX, z: playerCellZ };
const visibleCells = new Set();
for (let x = -RENDER_DISTANCE; x <= RENDER_DISTANCE; x++) {
for (let z = -RENDER_DISTANCE; z <= RENDER_DISTANCE; z++) {
const cellX = playerCellX + x;
const cellZ = playerCellZ + z;
const key = `${cellX},${cellZ}`;
visibleCells.add(key);
if (!buildings.has(key)) {
// 降低生成几率,让建筑更稀疏
if (Math.random() > 0.85) {
const posX = cellX * CELL_SIZE * 2 + (Math.random() - 0.5) * 15;
const posZ = cellZ * CELL_SIZE + (Math.random() - 0.5) * 10;
const building = createCoolBuilding(posX, posZ);
buildings.set(key, building);
scene.add(building);
}
}
}
}
for (const [key, building] of buildings) {
if (!visibleCells.has(key)) {
scene.remove(building);
buildings.delete(key);
}
}
}
// --- 事件处理 ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function onMouseMove(event) {
mousePosition.x = (event.clientX / window.innerWidth) * 2 - 1;
mousePosition.y = (event.clientY / window.innerHeight) * 2 - 1;
}
// --- 启动 ---
init();
</script>
</body>
</html>