用Three.js和Cannon-es搞个物理小游戏:从零到上线的完整实战记录
用Three.js和Cannon-es构建物理小游戏的实战指南
记得第一次接触3D游戏开发时,我被那些流畅的物理效果深深吸引——小球从斜坡滚落的自然轨迹,角色跳跃时的重力反馈,物体碰撞时的真实反应。作为JavaScript开发者,我们完全可以用Three.js和Cannon-es这套组合来实现这些效果。不同于市面上那些简单的教程,本文将带你从零开始,完整走一遍3D物理小游戏的开发流程,直到最终部署上线。
1. 环境搭建与基础配置
在开始编码之前,我们需要搭建一个现代化的前端开发环境。推荐使用Vite作为构建工具,它能提供极快的热更新速度,特别适合Three.js这类需要频繁预览效果的项目。
npm create vite@latest 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 i=0; i<physicsBodies.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 - 对静态物体设置
mass=0 - 启用
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目录。你可以将这些文件部署到任何静态主机服务,如:
- Vercel
- Netlify
- GitHub 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 id="loading"> <div class="progress-bar"> <div id="progress"></div> </div> <p>Loading game assets...</p> </div>8. 进阶扩展与创意方向
基础游戏完成后,你可以考虑以下扩展方向:
多关卡系统:
- 设计不同难度的关卡
- 实现关卡切换逻辑
- 添加关卡解锁机制
粒子效果:
- 收集物品时的爆炸效果
- 玩家移动时的尾迹
- 环境特效如雨雪
声音反馈:
- 背景音乐
- 收集物品的音效
- 碰撞声音
移动端适配:
- 触摸控制
- 屏幕摇杆实现
- 响应式布局
实现简单的粒子系统示例:
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[i+1] = (Math.random() - 0.5) * 2; positions[i+2] = (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 ); }