'# 用three.js做一个3D汉诺塔游戏
一、背景与问题
汉诺塔(Tower of Hanoi)作为经典的递归算法问题,其3D可视化实现一直是三维图形技术的典型应用案例。在Web开发领域,使用Three.js构建3D汉诺塔游戏具有以下技术特点:
- 空间计算挑战:需要精确计算三维坐标系中物体的相对位置关系
- 交互性需求:需实现鼠标拖拽、碰撞检测等交互功能
- 动画控制:需要处理物体移动的平滑过渡和状态同步
- 性能优化:在Web端处理复杂3D场景时需要考虑渲染性能
与传统2D实现相比,3D版本需要处理更多空间计算和物理模拟问题,但同时也提供了更直观的视觉反馈,这对教学演示和游戏开发具有特殊价值。
二、基本原理
Three.js的3D渲染基于以下核心原理:
- 三维坐标系:使用X/Y/Z轴构成三维空间,其中Z轴指向屏幕外
- 渲染流程:通过
Renderer将3D场景渲染为2D像素 - 光照模型:通过
Light对象模拟现实光效,影响物体的明暗变化 - 交互控制:通过
PointerLockControls或射线检测实现用户交互
在实现汉诺塔游戏时,需要特别注意以下技术细节:
- 盘子的中心点计算需要考虑高度差
- 柱子的排列需要符合几何约束
- 拖拽操作需要处理三维空间中的坐标转换
- 游戏状态需要实时同步
三、环境准备
1. 项目依赖
npm install three2. 基础环境配置
// 初始化Three.js场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // 浅蓝色天空
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth/window.innerHeight,
0.1,
1000
);
camera.position.set(0, 10, 20);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);3. 灯光配置
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
scene.add(directionalLight);四、核心实现
1. 柱子创建(3D空间布局)
function createPillar(x, y, z) {
const geometry = new THREE.CylinderGeometry(0.1, 0.1, 2, 32);
const material = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const pillar = new THREE.Mesh(geometry, material);
// 粗略调整位置
pillar.position.set(x, y, z);
return pillar;
}
// 创建三个柱子
const pillars = [
createPillar(-5, 0, 0),
createPillar(0, 0, 0),
createPillar(5, 0, 0)
];
pillars.forEach(p => scene.add(p));2. 盘子生成(动态尺寸)
function createDisk(radius, height, color) {
const geometry = new THREE.CylinderGeometry(radius, radius, height, 32);
const material = new THREE.MeshStandardMaterial({ color });
const disk = new THREE.Mesh(geometry, material);
// 设置旋转避免透视变形
disk.rotation.x = Math.PI / 2;
return disk;
}
// 生成盘子
const disks = [];
for (let i = 0; i < 5; i++) {
const radius = 1 - i * 0.2;
const color = 0x8B4513;
const disk = createDisk(radius, 0.5, color);
disks.push(disk);
}3. 碰撞检测算法
function checkCollision(disk, pillar) {
// 计算盘子中心点坐标
const diskPos = new THREE.Vector3();
disk.getWorldPosition(diskPos);
// 计算柱子中心点坐标
const pillarPos = new THREE.Vector3();
pillar.getWorldPosition(pillarPos);
// 计算距离
const distance = diskPos.distanceTo(pillarPos);
// 判断是否碰撞(简单阈值)
return distance < 0.3;
}五、完整案例
1. 游戏主循环
let isDragging = false;
let selectedDisk = null;
let selectedPillar = null;
function animate() {
requestAnimationFrame(animate);
// 更新物体位置
disks.forEach(disk => {
// 简单模拟重力下落
disk.position.z -= 0.01;
});
renderer.render(scene, camera);
}
animate();2. 交互逻辑
document.addEventListener('mousedown', (event) => {
const mouse = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(event.clientX, event.clientY), camera);
// 检测点击的盘子
const intersects = raycaster.intersectObjects(disks);
if (intersects.length > 0) {
selectedDisk = intersects[0].object;
isDragging = true;
}
});
document.addEventListener('mousemove', (event) => {
if (!isDragging) return;
const mouse = new THREE.Vector3();
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(new THREE.Vector2(event.clientX, event.clientY), camera);
// 计算拖拽位置
const position = raycaster.ray.intersectPlane(
new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
);
if (position) {
selectedDisk.position.copy(position);
}
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
selectedDisk = null;
// 检查是否合法放置
const targetPillar = pillars.find(p =>
checkCollision(selectedDisk, p)
);
if (targetPillar) {
targetPillar.add(selectedDisk);
}
});六、源码解析
1. 盘子创建逻辑
function createDisk(radius, height, color) {
const geometry = new THREE.CylinderGeometry(radius, radius, height, 32);
const material = new THREE.MeshStandardMaterial({ color });
// 创建网格
const disk = new THREE.Mesh(geometry, material);
// 旋转避免透视变形
disk.rotation.x = Math.PI / 2;
// 设置碰撞体积
disk.userData = {
radius: radius,
height: height
};
return disk;
}关键点:
- 使用
CylinderGeometry创建圆柱形盘子 - 通过旋转消除透视变形效果
- 添加
userData存储物理参数
2. 碰撞检测优化
function checkCollision(disk, pillar) {
// 获取世界坐标
const diskPos = new THREE.Vector3();
disk.getWorldPosition(diskPos);
const pillarPos = new THREE.Vector3();
pillar.getWorldPosition(pillarPos);
// 计算距离
const distance = diskPos.distanceTo(pillarPos);
// 判断是否碰撞(简单阈值)
return distance < 0.3;
}优化点:
- 使用
getWorldPosition获取准确坐标 - 采用简单阈值判断碰撞
- 可扩展为更复杂的碰撞检测算法
七、进阶使用
1. 动态难度调整
function generateDisks(count) {
const disks = [];
for (let i = 0; i < count; i++) {
const radius = 1 - i * 0.2;
const color = 0x8B4513;
const disk = createDisk(radius, 0.5, color);
disks.push(disk);
}
return disks;
}2. 动画控制
function animateDisk(disk, target) {
const start = disk.position.clone();
const end = target.position.clone();
const duration = 1000;
const startTime = performance.now();
function step(time) {
const elapsed = time - startTime;
const t = Math.min(elapsed / duration, 1);
disk.position.lerpVectors(start, end, t);
if (t < 1) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}3. 游戏状态同步
function checkWinCondition() {
const pillarDisks = pillars.map(p => p.children);
const allDisks = disks;
// 检查是否所有盘子都在右侧柱子
const rightPillar = pillars[1];
const rightDisks = rightPillar.children;
// 简单判断逻辑
return rightDisks.length === allDisks.length;
}八、性能与工程实践
1. 渲染优化策略
减少不必要的渲染:
// 只在位置变化时更新 if (disk.position.hasChanged) { renderer.render(scene, camera); }使用WebGL2特性:
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' });对象池管理:
const objectPool = []; function getDisk() { if (objectPool.length > 0) { return objectPool.pop(); } return createDisk(); }
2. 异常处理机制
try {
// 可能抛出异常的代码
} catch (error) {
console.error('渲染异常:', error);
// 恢复默认状态
scene.remove(disk);
}3. 安全考虑
用户输入过滤:
function sanitizeInput(input) { return input.replace(/[^a-zA-Z0-9]/g, ''); }防止XSS攻击:
function safeHTML(html) { return DOMPurify.sanitize(html); }
九、常见问题与踩坑
1. 坐标系转换错误
// 错误示例:直接使用屏幕坐标
const mouse = new THREE.Vector2(event.clientX, event.clientY);// 正确示例:转换为三维坐标
const mouse = new THREE.Vector3(
event.clientX,
event.clientY,
1
);
mouse.unproject(camera);2. 动画卡顿问题
// 错误示例:固定帧率
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}// 正确示例:使用性能计时
function animate() {
const delta = clock.getDelta();
renderer.render(scene, camera);
}3. 碰撞检测失效
// 错误示例:忽略物体世界坐标
function checkCollision(disk, pillar) {
const distance = disk.position.distanceTo(pillar.position);
return distance < 0.3;
}// 正确示例:使用世界坐标
function checkCollision(disk, pillar) {
const diskPos = new THREE.Vector3();
disk.getWorldPosition(diskPos);
const pillarPos = new THREE.Vector3();
pillar.getWorldPosition(pillarPos);
return diskPos.distanceTo(pillarPos) < 0.3;
}十、最佳实践
1. 代码组织建议
src/
├── main.js // 主程序入口
├── scenes/ // 场景管理
│ ├── game.js // 游戏逻辑
│ └── ui.js // 用户界面
├── models/ // 3D模型
│ └── disk.js // 盘子模型
├── utils/ // 工具函数
│ └── helpers.js // 辅助函数
└── assets/ // 资源文件2. 性能优化建议
- 对象池管理:复用对象减少GC压力
- LOD技术:远距离使用简化模型
- Web Workers:将计算密集型任务移出主线程
3. 安全实践
- 输入验证:对所有用户输入进行校验
- 内容安全策略:设置CSP头防止XSS
- 最小权限原则:仅加载必要的资源
十一、总结
通过Three.js实现3D汉诺塔游戏,我们深入探讨了三维图形技术的核心原理和实现细节。该方案适用于以下场景:
✅ 适用场景:
- 教学演示系统
- 游戏开发实验
- 算法可视化工具
- 空间认知训练
❌ 不适用场景:
- 高并发实时交互系统
- 超大规模场景渲染
- 需要物理引擎的复杂模拟
在开发过程中需要注意:
- 性能平衡:在视觉效果和性能之间找到最佳点
- 兼容性处理:考虑不同设备的渲染能力
- 可维护性:保持代码结构清晰
- 安全性:防止潜在的Web攻击
通过合理设计,Three.js可以成为一个强大的3D交互工具,但需要开发者对核心原理有深入理解,并结合具体场景进行优化调整。