用Three.js和Cannon-es构建物理小游戏的实战指南记得第一次接触3D游戏开发时我被那些流畅的物理效果深深吸引——小球从斜坡滚落的自然轨迹角色跳跃时的重力反馈物体碰撞时的真实反应。作为JavaScript开发者我们完全可以用Three.js和Cannon-es这套组合来实现这些效果。不同于市面上那些简单的教程本文将带你从零开始完整走一遍3D物理小游戏的开发流程直到最终部署上线。1. 环境搭建与基础配置在开始编码之前我们需要搭建一个现代化的前端开发环境。推荐使用Vite作为构建工具它能提供极快的热更新速度特别适合Three.js这类需要频繁预览效果的项目。npm create vitelatest physics-game --template vanilla cd physics-game npm install three cannon-es dat.gui安装完成后在main.js中导入必要的库import * as THREE from three; import * as CANNON from cannon-es; import { OrbitControls } from three/examples/jsm/controls/OrbitControls.js; import { GUI } from dat.gui;创建一个基础场景需要几个核心组件渲染器、场景、相机和灯光。以下是一个最小化配置const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const scene new THREE.Scene(); scene.background new THREE.Color(0xf0f0f0); const camera new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set(0, 5, 10);提示在开发阶段添加OrbitControls可以方便地从不同角度查看场景const controls new OrbitControls(camera, renderer.domElement); controls.enableDamping true;2. 物理世界的创建与同步Cannon-es是Cannon.js的ES模块版本提供了强大的物理模拟能力。我们需要创建一个物理世界并设置重力等基本参数const world new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0), // 地球重力 broadphase: new CANNON.SAPBroadphase(world), // 优化碰撞检测 allowSleep: true // 允许物体休眠提高性能 });物理引擎和Three.js的同步是关键。我们需要为每个物理物体创建对应的可视化对象const physicsBodies []; const threeObjects []; function createBox(size, position) { // 物理实体 const boxShape new CANNON.Box( new CANNON.Vec3(size.x/2, size.y/2, size.z/2) ); const boxBody new CANNON.Body({ mass: 1, shape: boxShape }); boxBody.position.set(position.x, position.y, position.z); world.addBody(boxBody); // 可视化对象 const boxGeometry new THREE.BoxGeometry(size.x, size.y, size.z); const boxMaterial new THREE.MeshStandardMaterial({ color: 0x00ff00 }); const boxMesh new THREE.Mesh(boxGeometry, boxMaterial); scene.add(boxMesh); // 保存引用 physicsBodies.push(boxBody); threeObjects.push(boxMesh); }在动画循环中更新物理世界并同步位置const timeStep 1/60; // 物理模拟步长 function animate() { requestAnimationFrame(animate); // 更新物理世界 world.step(timeStep); // 同步物理和渲染 for(let i0; iphysicsBodies.length; i) { threeObjects[i].position.copy(physicsBodies[i].position); threeObjects[i].quaternion.copy(physicsBodies[i].quaternion); } renderer.render(scene, camera); }3. 构建游戏核心机制让我们实现一个简单的滚球收集物品游戏。首先创建玩家控制的球体function createPlayer() { // 物理实体 const radius 0.5; const sphereShape new CANNON.Sphere(radius); const sphereBody new CANNON.Body({ mass: 5, shape: sphereShape, linearDamping: 0.3, // 增加阻尼使控制更稳定 material: new CANNON.Material(player) }); sphereBody.position.set(0, 5, 0); world.addBody(sphereBody); // 可视化对象 const sphereGeometry new THREE.SphereGeometry(radius, 32, 32); const sphereMaterial new THREE.MeshStandardMaterial({ color: 0xff0000, roughness: 0.2, metalness: 0.3 }); const sphereMesh new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphereMesh); return { body: sphereBody, mesh: sphereMesh }; }添加键盘控制const keys { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false }; window.addEventListener(keydown, (e) keys[e.key] true); window.addEventListener(keyup, (e) keys[e.key] false); function handleControls(playerBody) { const force 10; if(keys.ArrowUp) playerBody.applyForce(new CANNON.Vec3(0, 0, -force), playerBody.position); if(keys.ArrowDown) playerBody.applyForce(new CANNON.Vec3(0, 0, force), playerBody.position); if(keys.ArrowLeft) playerBody.applyForce(new CANNON.Vec3(-force, 0, 0), playerBody.position); if(keys.ArrowRight) playerBody.applyForce(new CANNON.Vec3(force, 0, 0), playerBody.position); }创建收集物品function createCollectible(position) { const radius 0.3; // 物理实体 const shape new CANNON.Sphere(radius); const body new CANNON.Body({ mass: 0, // 静态物体 shape: shape, isTrigger: true, // 设置为触发器 position: new CANNON.Vec3(position.x, position.y, position.z) }); world.addBody(body); // 可视化对象 const geometry new THREE.SphereGeometry(radius); const material new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xffff00, emissiveIntensity: 0.5 }); const mesh new THREE.Mesh(geometry, material); scene.add(mesh); return { body, mesh, collected: false }; }4. 碰撞检测与游戏逻辑设置碰撞检测系统来处理玩家与收集物品的交互// 创建接触材料 const playerMaterial new CANNON.Material(player); const collectibleMaterial new CANNON.Material(collectible); const contactMaterial new CANNON.ContactMaterial( playerMaterial, collectibleMaterial, { restitution: 0.3 } ); world.addContactMaterial(contactMaterial); // 碰撞事件处理 world.addEventListener(beginContact, (event) { const bodies [event.bodyA, event.bodyB]; // 检查是否是玩家碰到了收集物品 if(bodies.includes(player.body) bodies.some(b b.material collectibleMaterial)) { const collectibleBody bodies.find(b b ! player.body); const index collectibles.findIndex(c c.body collectibleBody); if(index ! -1 !collectibles[index].collected) { collectibles[index].collected true; scene.remove(collectibles[index].mesh); world.removeBody(collectibles[index].body); score; updateScore(); // 随机生成新收集物品 if(collectibles.every(c c.collected)) { createRandomCollectibles(5); } } } });添加简单的UI显示分数let score 0; const scoreElement document.createElement(div); scoreElement.style.position absolute; scoreElement.style.top 20px; scoreElement.style.left 20px; scoreElement.style.color white; scoreElement.style.fontFamily Arial; scoreElement.style.fontSize 24px; document.body.appendChild(scoreElement); function updateScore() { scoreElement.textContent Score: ${score}; }5. 场景设计与视觉效果创建一个有趣的游戏场景能大大提升游戏体验。让我们构建一个有平台和障碍物的环境function createGround() { // 物理实体 const groundShape new CANNON.Plane(); const groundBody new CANNON.Body({ mass: 0 }); groundBody.addShape(groundShape); groundBody.quaternion.setFromAxisAngle( new CANNON.Vec3(1, 0, 0), -Math.PI / 2 ); world.addBody(groundBody); // 可视化对象 const groundGeometry new THREE.PlaneGeometry(20, 20); const groundMaterial new THREE.MeshStandardMaterial({ color: 0x888888, roughness: 0.8, metalness: 0.2 }); const groundMesh new THREE.Mesh(groundGeometry, groundMaterial); groundMesh.rotation.x -Math.PI / 2; groundMesh.receiveShadow true; scene.add(groundMesh); }添加一些平台和障碍物function createPlatform(size, position) { // 物理实体 const shape new CANNON.Box( new CANNON.Vec3(size.x/2, size.y/2, size.z/2) ); const body new CANNON.Body({ mass: 0, // 静态物体 shape: shape, position: new CANNON.Vec3(position.x, position.y, position.z) }); world.addBody(body); // 可视化对象 const geometry new THREE.BoxGeometry(size.x, size.y, size.z); const material new THREE.MeshStandardMaterial({ color: 0x4CAF50, roughness: 0.7 }); const mesh new THREE.Mesh(geometry, material); mesh.position.set(position.x, position.y, position.z); mesh.castShadow true; scene.add(mesh); }添加灯光效果提升视觉体验// 环境光 const ambientLight new THREE.AmbientLight(0x404040); scene.add(ambientLight); // 方向光 const directionalLight new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(5, 10, 7); directionalLight.castShadow true; directionalLight.shadow.mapSize.width 1024; directionalLight.shadow.mapSize.height 1024; scene.add(directionalLight); // 启用阴影 renderer.shadowMap.enabled true; renderer.shadowMap.type THREE.PCFSoftShadowMap;6. 性能优化与调试技巧随着场景复杂度增加性能优化变得至关重要。以下是一些实用技巧物理模拟优化使用SAPBroadphase替代默认的NaiveBroadphase对静态物体设置mass0启用allowSleep让静止物体进入休眠状态渲染优化对远处物体使用低多边形模型合并相似的几何体减少draw call使用frustumCulled自动剔除视野外的物体添加调试面板方便调整参数const gui new GUI(); const physicsFolder gui.addFolder(Physics); physicsFolder.add(world.gravity, y, -20, 0).name(Gravity); physicsFolder.add(player.body, mass, 1, 10).name(Player Mass); const renderFolder gui.addFolder(Rendering); renderFolder.add(directionalLight, intensity, 0, 2).name(Light Intensity); renderFolder.addColor({ color: 0xf0f0f0 }, color).onChange(val { scene.background new THREE.Color(val); }).name(Background);使用stats.js监控性能import Stats from stats.js; const stats new Stats(); stats.showPanel(0); // 0: fps, 1: ms, 2: mb document.body.appendChild(stats.dom); function animate() { stats.begin(); // ...原有动画代码... stats.end(); }7. 部署与发布完成开发后我们需要将游戏部署到线上。Vite提供了简单的构建命令npm run build这会生成优化过的静态文件到dist目录。你可以将这些文件部署到任何静态主机服务如VercelNetlifyGitHub Pages传统Web服务器对于更专业的部署可以考虑CDN加速将静态资源上传到CDN代码分割如果项目很大可以拆分代码PWA支持添加manifest和service worker实现离线访问添加简单的加载界面提升用户体验const loadingManager new THREE.LoadingManager( () { // 所有资源加载完成 document.getElementById(loading).style.display none; }, (item, loaded, total) { // 加载进度更新 const progress (loaded / total) * 100; document.getElementById(progress).style.width ${progress}%; } ); // 使用这个loadingManager加载纹理等资源 const textureLoader new THREE.TextureLoader(loadingManager);在HTML中添加加载界面div idloading div classprogress-bar div idprogress/div /div pLoading game assets.../p /div8. 进阶扩展与创意方向基础游戏完成后你可以考虑以下扩展方向多关卡系统设计不同难度的关卡实现关卡切换逻辑添加关卡解锁机制粒子效果收集物品时的爆炸效果玩家移动时的尾迹环境特效如雨雪声音反馈背景音乐收集物品的音效碰撞声音移动端适配触摸控制屏幕摇杆实现响应式布局实现简单的粒子系统示例function createParticles(position, color, count 100) { const particlesGeometry new THREE.BufferGeometry(); const particlesMaterial new THREE.PointsMaterial({ color: color, size: 0.1, transparent: true, opacity: 0.8 }); const positions new Float32Array(count * 3); for(let i 0; i count * 3; i 3) { positions[i] (Math.random() - 0.5) * 2; positions[i1] (Math.random() - 0.5) * 2; positions[i2] (Math.random() - 0.5) * 2; } particlesGeometry.setAttribute( position, new THREE.BufferAttribute(positions, 3) ); const particles new THREE.Points(particlesGeometry, particlesMaterial); particles.position.copy(position); scene.add(particles); // 自动移除 setTimeout(() { scene.remove(particles); particlesGeometry.dispose(); particlesMaterial.dispose(); }, 1000); }在收集物品时调用if(index ! -1 !collectibles[index].collected) { // ...原有代码... createParticles( collectibles[index].mesh.position, 0xffff00 ); }