一个完整的Three.js角色控制器,应该包含角色移动、物理碰撞检测和交互功能。这个实现使用Cannon.js作为物理引擎,并提供直观的UI控制。
创建基础Three.js场景(场景、相机、渲染器、灯光)
集成Cannon.js物理世界
创建角色控制器(WASD/方向键控制)
实现角色物理(重力、碰撞、跳跃)
添加交互对象(可拾取物品、障碍物)
实现UI控制面板
下面是完整实现代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js 角色控制器 - 包含交互与物理</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
background-color: #1a1a2e;
color: #fff;
}
#container {
position: relative;
width: 100vw;
height: 100vh;
}
#infoPanel {
position: absolute;
top: 20px;
left: 20px;
background-color: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
z-index: 100;
max-width: 300px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
#infoPanel h2 {
color: #4cc9f0;
margin-bottom: 10px;
font-size: 1.4rem;
}
#infoPanel p {
margin: 8px 0;
line-height: 1.5;
font-size: 0.95rem;
}
.key {
display: inline-block;
background-color: #333;
padding: 3px 8px;
border-radius: 4px;
font-family: monospace;
margin: 0 3px;
border: 1px solid #555;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.control-group {
background-color: rgba(40, 40, 60, 0.8);
padding: 10px;
border-radius: 8px;
flex-grow: 1;
min-width: 120px;
}
.control-group h3 {
color: #f72585;
font-size: 1rem;
margin-bottom: 8px;
}
#stats {
position: absolute;
bottom: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 15px;
border-radius: 8px;
font-family: monospace;
font-size: 0.9rem;
z-index: 100;
}
#interactionPanel {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
z-index: 100;
max-width: 300px;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.collectible {
display: flex;
align-items: center;
margin: 8px 0;
padding: 5px;
background-color: rgba(30, 30, 50, 0.8);
border-radius: 5px;
}
.collectible-icon {
width: 20px;
height: 20px;
border-radius: 50%;
margin-right: 10px;
}
.red { background-color: #f72585; }
.blue { background-color: #4361ee; }
.green { background-color: #4cc9f0; }
.yellow { background-color: #ffd166; }
#gameOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
z-index: 200;
background-color: rgba(0, 0, 0, 0.8);
opacity: 1; /* 初始显示为可见 */
pointer-events: all;
transition: opacity 0.5s;
}
#gameOverlay.hidden {
opacity: 0;
pointer-events: none;
}
.overlay-content {
background-color: #162447;
padding: 30px;
border-radius: 15px;
text-align: center;
max-width: 500px;
border: 2px solid #4cc9f0;
}
.overlay-content h2 {
color: #f72585;
margin-bottom: 20px;
font-size: 2rem;
}
.overlay-content p {
margin-bottom: 20px;
font-size: 1.1rem;
line-height: 1.6;
}
button {
background-color: #4361ee;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
margin-top: 10px;
}
button:hover {
background-color: #3a56d4;
}
#canvas {
display: block;
}
@media (max-width: 768px) {
#infoPanel, #interactionPanel {
max-width: 250px;
padding: 10px;
font-size: 0.9rem;
}
.controls {
flex-direction: column;
}
}
</style>
</head>
<body>
<div id="container">
<canvas id="canvas"></canvas>
<div id="infoPanel">
<h2>角色控制器演示</h2>
<p>使用 <span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> 或方向键移动角色</p>
<p>按 <span class="key">空格键</span> 跳跃</p>
<p>按 <span class="key">E</span> 与物品交互</p>
<p>按 <span class="key">ESC</span> 显示/隐藏菜单</p>
<div class="controls">
<div class="control-group">
<h3>移动控制</h3>
<p>前进: <span class="key">W</span> 或 <span class="key">↑</span></p>
<p>后退: <span class="key">S</span> 或 <span class="key">↓</span></p>
<p>左移: <span class="key">A</span> 或 <span class="key">←</span></p>
<p>右移: <span class="key">D</span> 或 <span class="key">→</span></p>
</div>
<div class="control-group">
<h3>动作控制</h3>
<p>跳跃: <span class="key">空格</span></p>
<p>交互: <span class="key">E</span></p>
<p>菜单: <span class="key">ESC</span></p>
</div>
</div>
</div>
<div id="interactionPanel">
<h3>可收集物品</h3>
<div id="collectiblesList">
<!-- 收集物品列表将通过JS动态生成 -->
</div>
<p id="collectionStatus">已收集: 0/4</p>
</div>
<div id="stats">
<div>FPS: <span id="fpsCounter">0</span></div>
<div>位置: <span id="positionDisplay">(0, 0, 0)</span></div>
<div>速度: <span id="velocityDisplay">(0, 0, 0)</span></div>
</div>
<div id="gameOverlay">
<div class="overlay-content">
<h2>Three.js 角色控制器</h2>
<p>这是一个完整的角色控制器演示,包含物理引擎和交互功能。</p>
<p>使用WASD键移动角色,空格键跳跃,E键与物品交互。</p>
<p>收集所有彩色球体来完成游戏!</p>
<button id="startButton">开始游戏</button>
<button id="resetButton">重置游戏</button>
</div>
</div>
</div>
<!-- 使用正确的CDN链接 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cannon-es@0.20.0/dist/cannon-es.js"></script>
<script>
// 主应用程序
class CharacterController {
constructor() {
// 基本变量
this.collectedItems = 0;
this.totalItems = 4;
this.keys = {};
this.isJumping = false;
this.isMenuVisible = true;
this.clock = new THREE.Clock();
this.prevTime = 0;
this.fps = 0;
// 初始化
this.initScene();
this.initPhysics();
this.initCharacter();
this.initEnvironment();
this.initCollectibles();
this.initUI();
this.initEventListeners();
// 开始动画循环
this.animate();
}
initScene() {
// 创建场景
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x1a1a2e);
this.scene.fog = new THREE.Fog(0x1a1a2e, 10, 50);
// 创建相机
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.set(0, 10, 15);
// 创建渲染器
const canvas = document.getElementById('canvas');
this.renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// 添加环境光
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
this.scene.add(ambientLight);
// 添加方向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 5);
directionalLight.castShadow = true;
directionalLight.shadow.camera.left = -30;
directionalLight.shadow.camera.right = 30;
directionalLight.shadow.camera.top = 30;
directionalLight.shadow.camera.bottom = -30;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
this.scene.add(directionalLight);
// 添加辅助网格
const gridHelper = new THREE.GridHelper(100, 100, 0x444444, 0x222222);
this.scene.add(gridHelper);
}
initPhysics() {
// 创建物理世界
this.world = new CANNON.World();
this.world.gravity.set(0, -9.82, 0);
this.world.broadphase = new CANNON.NaiveBroadphase();
// 创建物理接触材料
const defaultMaterial = new CANNON.Material('default');
const defaultContactMaterial = new CANNON.ContactMaterial(defaultMaterial, defaultMaterial, {
friction: 0.1,
restitution: 0.3
});
this.world.addContactMaterial(defaultContactMaterial);
this.world.defaultContactMaterial = defaultContactMaterial;
}
initCharacter() {
// 创建角色视觉对象 - 使用【圆柱体+球】或者【three/addons/math/Capsule.js】
const group = new THREE.Group();
// 圆柱体部分
const cylinderGeometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 8);
const sphereGeometry = new THREE.SphereGeometry(0.5, 8, 8);
const characterMaterial = new THREE.MeshStandardMaterial({
color: 0x4cc9f0,
roughness: 0.7,
metalness: 0.1
});
// 创建胶囊体
const cylinder = new THREE.Mesh(cylinderGeometry, characterMaterial);
cylinder.position.y = 0.5;
const topSphere = new THREE.Mesh(sphereGeometry, characterMaterial);
topSphere.position.y = 1;
const bottomSphere = new THREE.Mesh(sphereGeometry, characterMaterial);
bottomSphere.position.y = 0;
group.add(cylinder);
group.add(topSphere);
group.add(bottomSphere);
this.characterMesh = group;
this.characterMesh.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
}
});
this.scene.add(this.characterMesh);
// 创建角色物理体 - 使用胶囊体【
// 注意: Cannon不支持胶囊体,用圆柱体代替,或者用圆柱体+球体替代胶囊体】
const shape = new CANNON.Cylinder(0.5, 0.5, 1, 8);
this.characterBody = new CANNON.Body({
mass: 5,
position: new CANNON.Vec3(0, 5, 0),
shape: shape,
material: this.world.defaultContactMaterial,
fixedRotation: true, // 锁定旋转,防止翻倒
linearDamping: 0.9,
angularDamping: 0.9
});
// 设置刚体方向,使其与世界坐标系对齐
// this.characterBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
this.characterBody.linearDamping = 0.9;
this.characterBody.angularDamping = 0.9;
this.world.addBody(this.characterBody);
// 角色控制器参数
this.moveSpeed = 10;
this.jumpForce = 8;
this.characterBody.allowSleep = false;
}
initEnvironment() {
// 创建地面
const groundGeometry = new THREE.PlaneGeometry(100, 100);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x2d3047,
roughness: 0.8,
metalness: 0.2
});
this.groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
this.groundMesh.rotation.x = -Math.PI / 2;
this.groundMesh.receiveShadow = true;
this.scene.add(this.groundMesh);
// 创建地面物理体
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({
mass: 0,
shape: groundShape,
material: this.world.defaultContactMaterial
});
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
this.world.addBody(groundBody);
// 创建一些障碍物
this.createObstacles();
}
createObstacles() {
// 障碍物配置
const obstacles = [
{ pos: [5, 1, 5], size: [2, 2, 2], color: 0x4361ee },
{ pos: [-8, 1.5, 3], size: [3, 3, 1], color: 0xf72585 },
{ pos: [0, 0.5, -8], size: [5, 1, 2], color: 0x7209b7 },
{ pos: [10, 2, -5], size: [1, 4, 1], color: 0xffd166 }
];
this.obstacles = [];
obstacles.forEach((obs, i) => {
// 视觉对象
const geometry = new THREE.BoxGeometry(...obs.size);
const material = new THREE.MeshStandardMaterial({
color: obs.color,
roughness: 0.6,
metalness: 0.2
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(...obs.pos);
mesh.castShadow = true;
mesh.receiveShadow = true;
this.scene.add(mesh);
// 物理体
const shape = new CANNON.Box(new CANNON.Vec3(obs.size[0]/2, obs.size[1]/2, obs.size[2]/2));
const body = new CANNON.Body({
mass: 0,
position: new CANNON.Vec3(...obs.pos),
shape: shape,
material: this.world.defaultContactMaterial
});
this.world.addBody(body);
this.obstacles.push({ mesh, body });
});
}
initCollectibles() {
// 可收集物品配置
const collectibles = [
{ pos: [8, 1, 8], color: 0xf72585, name: "红色能量球" },
{ pos: [-8, 1, 8], color: 0x4361ee, name: "蓝色能量球" },
{ pos: [8, 1, -8], color: 0x4cc9f0, name: "绿色能量球" },
{ pos: [-8, 1, -8], color: 0xffd166, name: "黄色能量球" }
];
this.collectibles = [];
collectibles.forEach((item, i) => {
// 视觉对象
const geometry = new THREE.SphereGeometry(0.5, 16, 16);
const material = new THREE.MeshStandardMaterial({
color: item.color,
emissive: item.color,
emissiveIntensity: 0.2,
roughness: 0.3,
metalness: 0.7
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(...item.pos);
mesh.castShadow = true;
this.scene.add(mesh);
// 物理体
const shape = new CANNON.Sphere(0.5);
const body = new CANNON.Body({
mass: 0,
position: new CANNON.Vec3(...item.pos),
shape: shape,
material: this.world.defaultContactMaterial
});
this.world.addBody(body);
this.collectibles.push({
mesh,
body,
collected: false,
name: item.name,
colorClass: ['red', 'blue', 'green', 'yellow'][i]
});
});
// 初始化收集物品UI
this.updateCollectiblesUI();
}
initUI() {
// 初始化UI元素
this.fpsCounter = document.getElementById('fpsCounter');
this.positionDisplay = document.getElementById('positionDisplay');
this.velocityDisplay = document.getElementById('velocityDisplay');
this.collectionStatus = document.getElementById('collectionStatus');
this.collectiblesList = document.getElementById('collectiblesList');
// 游戏开始/重置按钮
document.getElementById('startButton').addEventListener('click', () => {
this.hideMenu();
});
document.getElementById('resetButton').addEventListener('click', () => {
this.resetGame();
});
// 显示初始菜单
this.showMenu();
}
initEventListeners() {
// 键盘事件
window.addEventListener('keydown', (e) => {
this.keys[e.key.toLowerCase()] = true;
// 空格键跳跃
if (e.key === ' ' && !this.isJumping) {
this.jump();
}
// E键交互
if (e.key === 'e' || e.key === 'E') {
this.interact();
}
// ESC键显示/隐藏菜单
if (e.key === 'Escape') {
e.preventDefault();
this.toggleMenu();
}
});
window.addEventListener('keyup', (e) => {
this.keys[e.key.toLowerCase()] = false;
});
// 窗口大小调整
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
}
jump() {
// 简单的跳跃检测
if (this.characterBody.position.y < 2.5) {
this.characterBody.velocity.y = this.jumpForce;
this.isJumping = true;
// 0.5秒后重置跳跃状态
setTimeout(() => {
this.isJumping = false;
}, 500);
}
}
interact() {
// 检查附近的可收集物品
const interactionDistance = 2.5;
const charPos = this.characterBody.position;
this.collectibles.forEach(item => {
if (item.collected) return;
const itemPos = item.body.position;
const dx = charPos.x - itemPos.x;
const dy = charPos.y - itemPos.y;
const dz = charPos.z - itemPos.z;
const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (distance < interactionDistance) {
this.collectItem(item);
}
});
}
collectItem(item) {
if (item.collected) return;
item.collected = true;
// 视觉反馈:使物品消失
item.mesh.visible = false;
// 更新收集计数
this.collectedItems++;
// 更新UI
this.updateCollectiblesUI();
// 检查是否所有物品都已收集
if (this.collectedItems === this.totalItems) {
setTimeout(() => {
this.showMenu(true);
}, 500);
}
}
updateCollectiblesUI() {
// 清空列表
this.collectiblesList.innerHTML = '';
// 添加每个收集物品
this.collectibles.forEach(item => {
const div = document.createElement('div');
div.className = 'collectible';
const icon = document.createElement('div');
icon.className = `collectible-icon ${item.colorClass}`;
const text = document.createElement('span');
text.textContent = item.name;
text.style.textDecoration = item.collected ? 'line-through' : 'none';
text.style.opacity = item.collected ? '0.5' : '1';
div.appendChild(icon);
div.appendChild(text);
this.collectiblesList.appendChild(div);
});
// 更新收集状态
this.collectionStatus.textContent = `已收集: ${this.collectedItems}/${this.totalItems}`;
}
showMenu(gameComplete = false) {
const overlay = document.getElementById('gameOverlay');
const title = overlay.querySelector('h2');
const text = overlay.querySelector('p');
const startButton = document.getElementById('startButton');
if (gameComplete) {
title.textContent = "恭喜!";
text.textContent = "你已收集所有能量球!点击重置按钮重新开始游戏。";
startButton.style.display = 'none';
} else {
title.textContent = "Three.js 角色控制器";
text.textContent = "这是一个完整的角色控制器演示,包含物理引擎和交互功能。使用WASD键移动角色,空格键跳跃,E键与物品交互。收集所有彩色球体来完成游戏!";
startButton.style.display = 'block';
}
overlay.classList.remove('hidden');
this.isMenuVisible = true;
}
hideMenu() {
document.getElementById('gameOverlay').classList.add('hidden');
this.isMenuVisible = false;
}
toggleMenu() {
if (this.isMenuVisible) {
this.hideMenu();
} else {
this.showMenu();
}
}
resetGame() {
// 重置角色位置
this.characterBody.position.set(0, 5, 0);
this.characterBody.velocity.set(0, 0, 0);
this.characterBody.angularVelocity.set(0, 0, 0);
// 重置收集物品
this.collectedItems = 0;
this.collectibles.forEach(item => {
item.collected = false;
item.mesh.visible = true;
});
// 更新UI
this.updateCollectiblesUI();
// 隐藏菜单
this.hideMenu();
}
handleMovement(deltaTime) {
if (this.isMenuVisible) return;
const moveForce = this.moveSpeed * deltaTime * 60; // 乘以60以补偿时间步长
// 获取当前速度
const velocity = this.characterBody.velocity;
// 根据按键设置速度
if (this.keys['w'] || this.keys['arrowup']) {
velocity.z = -moveForce;
} else if (this.keys['s'] || this.keys['arrowdown']) {
velocity.z = moveForce;
} else {
velocity.z = 0;
}
if (this.keys['a'] || this.keys['arrowleft']) {
velocity.x = -moveForce;
} else if (this.keys['d'] || this.keys['arrowright']) {
velocity.x = moveForce;
} else {
velocity.x = 0;
}
// 保持垂直速度不变
velocity.y = this.characterBody.velocity.y;
// 应用速度
this.characterBody.velocity = velocity;
}
updateCamera() {
// 第三人称相机跟随
const charPos = this.characterBody.position;
// 相机目标位置(角色后方上方)
const targetPos = {
x: charPos.x,
y: charPos.y + 5,
z: charPos.z + 10
};
// 平滑相机移动
this.camera.position.x += (targetPos.x - this.camera.position.x) * 0.05;
this.camera.position.y += (targetPos.y - this.camera.position.y) * 0.05;
this.camera.position.z += (targetPos.z - this.camera.position.z) * 0.05;
// 相机看向角色
this.camera.lookAt(charPos.x, charPos.y + 2, charPos.z);
}
updateUI() {
// 更新FPS
const time = this.clock.getElapsedTime();
const delta = time - this.prevTime;
this.prevTime = time;
this.fps = Math.round(1 / delta);
this.fpsCounter.textContent = this.fps;
// 更新位置和速度
const pos = this.characterBody.position;
const vel = this.characterBody.velocity;
this.positionDisplay.textContent = `(${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`;
this.velocityDisplay.textContent = `(${vel.x.toFixed(1)}, ${vel.y.toFixed(1)}, ${vel.z.toFixed(1)})`;
}
animate() {
requestAnimationFrame(() => this.animate());
// 计算时间增量
const deltaTime = Math.min(this.clock.getDelta(), 0.1);
// 处理角色移动
this.handleMovement(deltaTime);
// 更新物理世界
this.world.step(1/60, deltaTime, 3);
// 同步角色视觉对象与物理体
this.characterMesh.position.copy(this.characterBody.position);
this.characterMesh.quaternion.copy(this.characterBody.quaternion);
// 更新相机
this.updateCamera();
// 更新UI
this.updateUI();
// 旋转可收集物品(如果未被收集)
this.collectibles.forEach(item => {
if (!item.collected) {
item.mesh.rotation.y += 0.02;
}
});
// 旋转障碍物
this.obstacles.forEach((obstacle, index) => {
obstacle.mesh.rotation.y += 0.005 * (index + 1);
});
// 渲染场景
this.renderer.render(this.scene, this.camera);
}
}
// 当页面加载时启动应用程序
window.addEventListener('load', () => {
try {
new CharacterController();
console.log("角色控制器已成功启动!");
} catch (error) {
console.error("启动失败:", error);
alert("加载失败,请检查控制台获取详细信息。");
}
});
</script>
</body>
</html>
修复了Cannon.js库链接:
cannon-es简化了胶囊体创建:
CapsuleGeometry(兼容性更好)修复了UI显示问题:
修改了CSS类名,确保菜单初始可见
修复了菜单显示/隐藏逻辑
改进了错误处理:
添加了try-catch块捕获初始化错误
添加了控制台日志便于调试
优化了移动控制:
将上面的完整代码复制到一个新的HTML文件中
使用现代浏览器(Chrome/Firefox/Edge)打开该文件
点击"开始游戏"按钮
使用WASD键移动角色,空格键跳跃,E键收集物品
如果仍然无法运行,请按F12打开浏览器开发者工具,查看控制台是否有错误信息,这将帮助我们进一步诊断问题。