Skip to content

208. 综合实战:开放世界(九)

Published:

上节实现了视角切换:

2025-12-20 18.04.06.gif

这节来做下车辆行驶的控制。

首先,我们在界面加个介绍:

image.png

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

image.png

还有样式:

#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;
}

2025-12-20 18.09.37.gif

这样,下面就有提示了。

然后来实现车辆行驶。

其实原理很简单,就和人走路一模一样,只不过现在是控制车了。

首先,我们要在 isCarView 为 true 的时候,把人的那些事件禁用掉:

image.png

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

image.png

加大一下旋转、移动的阻力,再就是限制下只能水平转方向。

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轴旋转

然后在下面加上车辆控制的逻辑:

image.png

其实和人的控制一样的,我们一点点看下:

这个鼠标锁定状态、mousemove 控制相机方向、车辆方向旋转:

image.png

按键的状态记录:

image.png

这里其实用不到 space,但保留也没啥,不处理就好了。

包括获取相机方向,计算和设置物体速度:

image.png

都和控制人的没区别,只不过速度可以稍微快一点。


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();

我们试一下:

2025-12-20 18.20.18.gif

人物行走的功能依然正常。

然后切换到车辆行驶:

2025-12-20 18.21.04.gif

也同样能控制车辆行驶。

包括撞到障碍物:

2025-12-20 18.22.02.gif

会被挡下来,因为这些障碍物质量为 0,不会移动。

这样,人物行走和车辆行驶的功能,两者的切换,就都完成了。

案例代码上传了小册仓库

总结

这节我们加上了车辆行驶的控制。

其实就是用一个变量来控制当前是人物还是车辆,如果是车辆的话,就是另一套事件监听和处理逻辑。

当然,具体的逻辑比如鼠标移动实现方向控制、键盘控制前进后退,这些都是一样的。

人和车辆的控制都完成了,那自然可以实现人走到车附近,上车实现开车的功能。

评论