Skip to content

213. 综合实战:开放世界(十四)

Published:

上节实现了上下飞机:

2025-12-28 22.05.08.gif

这节我们来做下上飞机之后开飞机的功能。

它和汽车的区别是除了前后移动之外,还可以上下的飞。

我们加一下飞机控制的代码:

image.png

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import * as CANNON from 'cannon-es';
import { world } from './mesh.js';
import { camera, isPlaneView } from './main.js';

const loader = new GLTFLoader();

const group = new THREE.Group();

// 飞机的位置和尺寸
const planeSize = { width: 2, height: 1, depth: 3 }; // 飞机的碰撞盒尺寸
const planePosition = { x: -10, y: 1.15, z: 10 };

// 创建飞机的物理碰撞体
const planeBody = new CANNON.Body({
    mass: 2000, // 飞机的质量(千克),可以移动
    position: new CANNON.Vec3(planePosition.x, planePosition.y, planePosition.z),
    linearDamping: 0.9, // 增加线性阻尼
    angularDamping: 0.9, // 增加角度阻尼
    fixedRotation: false, // 允许旋转
    linearFactor: new CANNON.Vec3(1, 1, 1), // 允许在XYZ三个方向移动
    angularFactor: new CANNON.Vec3(0, 1, 0), // 只允许绕Y轴旋转
    type: CANNON.Body.KINEMATIC // 使用运动学物体,不受重力影响
});
planeBody.addShape(new CANNON.Box(new CANNON.Vec3(
    planeSize.width / 2,
    planeSize.height / 2,
    planeSize.depth / 2
)));
world.addBody(planeBody);

export { planeBody };

// 可视化碰撞盒(调试用)
// const planeBoxGeo = new THREE.BoxGeometry(planeSize.width, planeSize.height, planeSize.depth);
// const planeBoxMat = new THREE.MeshPhongMaterial({
//     color: 0x00ff00,
//     transparent: true,
//     opacity: 0.3,
//     wireframe: true
// });
// const planeBoxMesh = new THREE.Mesh(planeBoxGeo, planeBoxMat);
// planeBoxMesh.position.set(planePosition.x, planePosition.y, planePosition.z);
// group.add(planeBoxMesh);

export const loadPromise = loader.loadAsync("./toy_plane.glb");

export let planeModel = null;

loadPromise.then(gltf => {
    planeModel = gltf.scene;
    group.add(planeModel);
    console.log(gltf);

    gltf.scene.traverse((child) => {
        if (child.isMesh) {
            child.castShadow = true;
            child.receiveShadow = true;
        }
    });

    gltf.scene.position.set(-10, 1.15, 10);
})

// 飞机控制参数
const planeSpeed = 15;
const planeRotationSpeed = 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 || !planeModel || !isPlaneView) return;

  // 控制飞机左右旋转(通过物理体)
  const rotationChange = -e.movementX / 500;
  const currentRotation = new CANNON.Quaternion();
  currentRotation.copy(planeBody.quaternion);
  const additionalRotation = new CANNON.Quaternion();
  additionalRotation.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), rotationChange);
  planeBody.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,
  shift: false
};

window.addEventListener('keydown', (e) => {
  if (!isPlaneView) return;
  const key = e.key.toLowerCase();
  if (key === ' ') {
    keyPressed.space = true;
  } else if (key === 'shift') {
    keyPressed.shift = true;
  } else if (key in keyPressed) {
    keyPressed[key] = true;
  }
});

window.addEventListener('keyup', (e) => {
  if (!isPlaneView) return;
  const key = e.key.toLowerCase();
  if (key === ' ') {
    keyPressed.space = false;
  } else if (key === 'shift') {
    keyPressed.shift = false;
  } else if (key in keyPressed) {
    keyPressed[key] = false;
  }
});

// 飞机移动更新函数
function updatePlaneMovement(deltaTime) {
  if (!planeModel || !isPlaneView) return;

  // 获取飞机当前朝向
  const forward = new THREE.Vector3();
  planeModel.getWorldDirection(forward);
  forward.y = 0;
  forward.normalize();

  // 重置角速度
  planeBody.angularVelocity.set(0, 0, 0);

  // 初始化速度
  let velocityX = 0;
  let velocityY = planeBody.velocity.y;
  let velocityZ = 0;

  // W/S - 前进/后退
  if (keyPressed.w) {
    velocityX = forward.x * planeSpeed;
    velocityZ = forward.z * planeSpeed;

    // A/D - 左右转向(仅在前进时)
    if (keyPressed.a) {
      planeBody.angularVelocity.y = planeRotationSpeed;
    } else if (keyPressed.d) {
      planeBody.angularVelocity.y = -planeRotationSpeed;
    }
  } else if (keyPressed.s) {
    velocityX = -forward.x * planeSpeed;
    velocityZ = -forward.z * planeSpeed;

    // A/D - 左右转向(倒车时转向相反)
    if (keyPressed.a) {
      planeBody.angularVelocity.y = -planeRotationSpeed;
    } else if (keyPressed.d) {
      planeBody.angularVelocity.y = planeRotationSpeed;
    }
  }

  // 空格键 - 上升
  if (keyPressed.space) {
    velocityY = 8; // 上升速度
  }
  // Shift键 - 下降
  else if (keyPressed.shift) {
    velocityY = -8; // 下降速度
  }
  // 没有按升降键时,保持当前高度
  else {
    velocityY = 0;
  }

  // 设置飞机速度
  planeBody.velocity.set(velocityX, velocityY, velocityZ);
}

// 动画循环 - 同步物理体和模型
function animatePlane() {
    requestAnimationFrame(animatePlane);

    if (planeModel) {
        // 限制飞机最低高度(不能低于地面)
        const minHeight = planeSize.height / 2;
        if (planeBody.position.y < minHeight) {
          planeBody.position.y = minHeight;
          if (planeBody.velocity.y < 0) {
            planeBody.velocity.y = 0;
          }
        }

        // 同步飞机模型位置和旋转
        planeModel.position.copy(planeBody.position);
        planeModel.quaternion.copy(planeBody.quaternion);

        // 同步可视化盒子
        // planeBoxMesh.position.copy(planeBody.position);
        // planeBoxMesh.quaternion.copy(planeBody.quaternion);
    }

    updatePlaneMovement(0.016); // 约60fps
}

animatePlane();

export default group;

首先,飞机的控制同样是需要鼠标锁定状态:

image.png

通过鼠标控制飞机的上下、左右旋转。

然后鼠标控制前后左右:

image.png

区别是多了 space 和 shift 来控制上下。

控制飞机移动依然是通过改变速度:

image.png

只不过多了竖直方向的速度的改变:

image.png

还有一点也很重要:

飞机有重量,当然会随着重力下降,我们希望它不要下降,停留在空中,所以要设置这个:

image.png

飞起来的时候,设置一个向上的浮力,刚好抵消重力就行。

试下效果:

2026-01-03 00.27.25.gif

2026-01-03 00.42.18.gif

这样,飞机的飞行、起飞、降落、上下飞机就完成了。

案例代码上传了小册仓库

总结

这节我们实现了开飞机的功能。

前后左右和开车时一样,只不过加了上下移动的功能。

同时还要注意飞在空中的时候要加一个抵消重力的浮力。

这样,我们就能在这个开放世界开飞机了。

评论