上节实现了视角切换:

这节来做下车辆行驶的控制。
首先,我们在界面加个介绍:

<div id="viewTip">按 X 切换视角</div>

还有样式:
#viewTip {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 5px;
font-size: 14px;
z-index: 1000;
pointer-events: none;
}

这样,下面就有提示了。
然后来实现车辆行驶。
其实原理很简单,就和人走路一模一样,只不过现在是控制车了。
首先,我们要在 isCarView 为 true 的时候,把人的那些事件禁用掉:

然后先调一下车辆的一些参数:

加大一下旋转、移动的阻力,再就是限制下只能水平转方向。
import { camera, isCarView } from './main.js';
linearDamping: 0.9, // 增加线性阻尼
angularDamping: 0.9, // 增加角度阻尼
fixedRotation: false, // 允许旋转
linearFactor: new CANNON.Vec3(1, 0, 1), // 限制Y轴移动,只能在XZ平面移动
angularFactor: new CANNON.Vec3(0, 1, 0) // 只允许绕Y轴旋转
然后在下面加上车辆控制的逻辑:

其实和人的控制一样的,我们一点点看下:
这个鼠标锁定状态、mousemove 控制相机方向、车辆方向旋转:

按键的状态记录:

这里其实用不到 space,但保留也没啥,不处理就好了。
包括获取相机方向,计算和设置物体速度:

都和控制人的没区别,只不过速度可以稍微快一点。
export let carModel = null;
loadPromise.then(gltf => {
gltf.scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
carModel = gltf.scene;
carModel.position.set(0, 0, 10);
group.add(carModel);
console.log(gltf);
})
// 车辆控制参数
const carSpeed = 15;
const carRotationSpeed = 2;
const minCameraAngle = THREE.MathUtils.degToRad(-20);
const maxCameraAngle = THREE.MathUtils.degToRad(20);
let isPointerLocked = false;
// 监听指针锁定状态
document.addEventListener('pointerlockchange', () => {
isPointerLocked = document.pointerLockElement === document.body;
});
// 车辆鼠标控制
document.addEventListener('mousemove', (e) => {
if (!isPointerLocked || !carModel || !isCarView) return;
// 控制车辆左右旋转(通过物理体)
const rotationChange = -e.movementX / 500;
const currentRotation = new CANNON.Quaternion();
currentRotation.copy(carBody.quaternion);
const additionalRotation = new CANNON.Quaternion();
additionalRotation.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), rotationChange);
carBody.quaternion = additionalRotation.mult(currentRotation);
// 控制相机上下旋转
camera.rotation.x -= e.movementY / 500;
if (camera.rotation.x > maxCameraAngle) {
camera.rotation.x = maxCameraAngle;
}
if (camera.rotation.x < minCameraAngle) {
camera.rotation.x = minCameraAngle;
}
});
// 车辆键盘控制
const keyPressed = {
w: false,
a: false,
s: false,
d: false,
space: false
};
window.addEventListener('keydown', (e) => {
if (!isCarView) return;
const key = e.key.toLowerCase();
if (key === ' ') {
keyPressed.space = true;
} else if (key in keyPressed) {
keyPressed[key] = true;
}
});
window.addEventListener('keyup', (e) => {
if (!isCarView) return;
const key = e.key.toLowerCase();
if (key === ' ') {
keyPressed.space = false;
} else if (key in keyPressed) {
keyPressed[key] = false;
}
});
// 车辆移动更新函数
function updateCarMovement(deltaTime) {
if (!carModel || !isCarView) return;
// 获取车辆当前朝向
const forward = new THREE.Vector3();
carModel.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
// 重置角速度
carBody.angularVelocity.set(0, 0, 0);
// W/S - 前进/后退
if (keyPressed.w) {
carBody.velocity.x = forward.x * carSpeed;
carBody.velocity.y = carBody.velocity.y; // 保持y轴速度,避免重力影响
carBody.velocity.z = forward.z * carSpeed;
// A/D - 左右转向(仅在前进时)
if (keyPressed.a) {
carBody.angularVelocity.y = carRotationSpeed;
} else if (keyPressed.d) {
carBody.angularVelocity.y = -carRotationSpeed;
}
} else if (keyPressed.s) {
carBody.velocity.x = -forward.x * carSpeed;
carBody.velocity.y = carBody.velocity.y; // 保持y轴速度
carBody.velocity.z = -forward.z * carSpeed;
// A/D - 左右转向(倒车时转向相反)
if (keyPressed.a) {
carBody.angularVelocity.y = -carRotationSpeed;
} else if (keyPressed.d) {
carBody.angularVelocity.y = carRotationSpeed;
}
} else {
// 没有前进或后退时,停止移动
carBody.velocity.x = 0;
carBody.velocity.z = 0;
}
}
// 动画循环 - 同步物理体和模型
function animateCar() {
requestAnimationFrame(animateCar);
if (carModel) {
// 保持车辆在地面上,防止弹跳
if (carBody.position.y < carSize.height / 2) {
carBody.position.y = carSize.height / 2;
carBody.velocity.y = 0;
}
// 同步车辆模型位置和旋转
carModel.position.copy(carBody.position);
carModel.position.y -= carSize.height / 2;
carModel.quaternion.copy(carBody.quaternion);
}
updateCarMovement(0.016); // 约60fps
}
animateCar();
我们试一下:

人物行走的功能依然正常。
然后切换到车辆行驶:

也同样能控制车辆行驶。
包括撞到障碍物:

会被挡下来,因为这些障碍物质量为 0,不会移动。
这样,人物行走和车辆行驶的功能,两者的切换,就都完成了。
案例代码上传了小册仓库
总结
这节我们加上了车辆行驶的控制。
其实就是用一个变量来控制当前是人物还是车辆,如果是车辆的话,就是另一套事件监听和处理逻辑。
当然,具体的逻辑比如鼠标移动实现方向控制、键盘控制前进后退,这些都是一样的。
人和车辆的控制都完成了,那自然可以实现人走到车附近,上车实现开车的功能。