作为一名游戏开发爱好者,我最近尝试了一个有趣的实验:用Three.js在极短时间内构建可立即验证的3D游戏原型。这个过程中,我从经典的2D Pac-Man实现开始,逐步升级到完整的3D版本,验证了Three.js在快速原型开发中的强大能力。
这个项目的核心目标是验证几个关键假设:
我选择Pac-Man作为原型对象有几个原因:首先,它的游戏机制简单明了;其次,它包含了角色移动、碰撞检测、得分系统等基础游戏元素;最后,从2D到3D的转换能直观展示Three.js的能力。
最初的2D版本使用纯HTML5 Canvas实现,代码结构非常直接:
html复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pac-Man Game</title>
<style>
canvas {
border: 1px solid black;
display: block;
margin: 0 auto;
background-color: #000;
}
/* 其他样式... */
</style>
</head>
<body>
<div id="score">Score: 0</div>
<canvas id="gameCanvas" width="448" height="496"></canvas>
<script>
// 游戏逻辑代码...
</script>
</body>
</html>
这种结构的好处是零依赖,任何现代浏览器都能直接运行,非常适合快速验证想法。
2D版本实现了Pac-Man的几个关键元素:
javascript复制const map = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,2,2,2,2,2,2,2,2,2,2,2,2,1,1,2,2,2,2,2,2,2,2,2,2,2,2,1],
// 更多行...
];
javascript复制document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowRight': player.nextDirection = 'right'; break;
case 'ArrowLeft': player.nextDirection = 'left'; break;
// 其他方向...
}
});
javascript复制function canMove(x, y) {
const tileX = Math.floor(x);
const tileY = Math.floor(y);
if(tileX < 0 || tileX >= map[0].length || tileY < 0 || tileY >= map.length)
return false;
return map[tileY][tileX] !== 1;
}
javascript复制function collectDot() {
const tileX = Math.floor(player.x);
const tileY = Math.floor(player.y);
if(map[tileY][tileX] === 2) {
map[tileY][tileX] = 0;
score += 10;
scoreDisplay.textContent = `Score: ${score}`;
}
}
虽然2D版本实现了基本功能,但存在几个明显限制:
这些限制促使我考虑使用Three.js升级到3D版本。
3D版本首先需要设置Three.js的基本场景元素:
javascript复制// 初始化场景、相机和渲染器
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(cameraOffset.x, cameraOffset.y, cameraOffset.z);
camera.rotation.x = cameraRotation.x;
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 添加光源
const ambientLight = new THREE.AmbientLight(0x404040, 1);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(0, 20, 10);
scene.add(directionalLight);
}
3D版本的迷宫生成逻辑与2D类似,但使用了Three.js的3D对象:
javascript复制function createMaze() {
// 创建地面
const groundGeometry = new THREE.PlaneGeometry(map[0].length+2, map.length+2);
const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x333333, side: THREE.DoubleSide });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = Math.PI/2;
ground.position.set(map[0].length/2-0.5, -0.5, map.length/2-0.5);
scene.add(ground);
// 创建墙壁和豆子
for(let z = 0; z < map.length; z++) {
maze[z] = [];
for(let x = 0; x < map[z].length; x++) {
if(map[z][x] === 1) {
// 墙壁
const wallGeometry = new THREE.BoxGeometry(1, 1, 1);
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x0000ff });
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
wall.position.set(x, 0, z);
scene.add(wall);
maze[z][x] = { type: 'wall', object: wall };
} else if(map[z][x] === 2) {
// 豆子
const dotGeometry = new THREE.SphereGeometry(0.1, 8, 8);
const dotMaterial = new THREE.MeshLambertMaterial({ color: 0xffff00 });
const dot = new THREE.Mesh(dotGeometry, dotMaterial);
dot.position.set(x, 0, z);
scene.add(dot);
maze[z][x] = { type: 'dot', object: dot, collected: false };
dotsLeft++;
} else {
maze[z][x] = { type: 'empty' };
}
}
}
}
Pac-Man角色的3D实现有几个技术亮点:
javascript复制function animateMouth() {
const playerGeometry = player.geometry;
if(player.userData.mouthOpen) {
playerGeometry.parameters.thetaLength -= player.userData.mouthSpeed;
if(playerGeometry.parameters.thetaLength <= Math.PI) {
player.userData.mouthOpen = false;
}
} else {
playerGeometry.parameters.thetaLength += player.userData.mouthSpeed;
if(playerGeometry.parameters.thetaLength >= Math.PI*1.8) {
player.userData.mouthOpen = true;
}
}
player.geometry = new THREE.SphereGeometry(0.4, 32, 32, 0, playerGeometry.parameters.thetaLength);
}
javascript复制function movePlayer() {
// 尝试按下一个方向移动
if(player.userData.nextDirection.x !== 0 || player.userData.nextDirection.z !== 0) {
const nextX = Math.floor(player.position.x + player.userData.nextDirection.x * player.userData.speed * 2);
const nextZ = Math.floor(player.position.z + player.userData.nextDirection.z * player.userData.speed * 2);
if(nextX >= 0 && nextX < map[0].length &&
nextZ >= 0 && nextZ < map.length &&
map[nextZ][nextX] !== 1) {
player.userData.direction = { ...player.userData.nextDirection };
// 更新角色旋转方向
if(player.userData.direction.x === 1) player.rotation.y = Math.PI/2;
else if(player.userData.direction.x === -1) player.rotation.y = -Math.PI/2;
else if(player.userData.direction.z === 1) player.rotation.y = Math.PI;
else if(player.userData.direction.z === -1) player.rotation.y = 0;
}
}
// 在当前方向上移动
if(player.userData.direction.x !== 0 || player.userData.direction.z !== 0) {
const newX = player.position.x + player.userData.direction.x * player.userData.speed;
const newZ = player.position.z + player.userData.direction.z * player.userData.speed;
const nextTileX = Math.floor(newX + player.userData.direction.x * 0.4);
const nextTileZ = Math.floor(newZ + player.userData.direction.z * 0.4);
if(nextTileX >= 0 && nextTileX < map[0].length &&
nextTileZ >= 0 && nextTileZ < map.length &&
map[nextTileZ][nextTileX] !== 1) {
player.position.x = newX;
player.position.z = newZ;
// 隧道传送效果
if(player.position.x < 0) player.position.x = map[0].length-1;
if(player.position.x >= map[0].length) player.position.x = 0;
if(player.position.z < 0) player.position.z = map.length-1;
if(player.position.z >= map.length) player.position.z = 0;
collectDots();
}
}
}
3D版本的一个显著优势是可以自由控制相机视角:
javascript复制function updateCamera() {
// 相机跟随玩家但保持一定偏移
camera.position.x = player.position.x + cameraOffset.x;
camera.position.y = player.position.y + cameraOffset.y;
camera.position.z = player.position.z + cameraOffset.z;
// 相机始终看向玩家
camera.lookAt(player.position.x, player.position.y, player.position.z);
}
// 相机控制键位
function onKeyDown(event) {
switch(event.key) {
case 'w': cameraOffset.y += 0.5; break;
case 's': cameraOffset.y -= 0.5; break;
case 'a': cameraOffset.x -= 0.5; break;
case 'd': cameraOffset.x += 0.5; break;
case 'q': cameraOffset.z -= 0.5; break;
case 'e': cameraOffset.z += 0.5; break;
}
}
在开发3D游戏时,性能是需要特别关注的因素。以下是我总结的几个优化点:
在开发过程中,我遇到了几个典型问题:
碰撞检测不准确:
相机抖动:
性能下降:
这个基础原型可以进一步扩展:
通过这个项目,我验证了Three.js在快速游戏原型开发中的几个显著优势:
对于想要尝试游戏开发的新手,我强烈推荐从Three.js开始。它既能让开发者快速看到成果,又提供了足够的深度来探索更复杂的游戏开发技术。这个Pac-Man原型仅用了几百行代码就实现了可玩版本,证明了现代Web游戏开发的可行性。
在实际开发中,我建议先构建最简单的可玩版本,然后逐步添加功能。这种迭代式开发方法能保持动力,并让问题尽早暴露。例如,我先实现了基本的移动和碰撞,再添加得分系统,最后才完善视觉效果和动画。
Three.js的学习曲线相对平缓,但要想精通仍需理解3D图形学的基本概念,如坐标系、变换矩阵、光照模型等。这些知识不仅能帮助你解决开发中的问题,还能让你创造出更专业的3D体验。