Skip to content

2. 第三人称飞行 ​

loading

threejs 提供了 FlyControls 控制器,能够真实模拟飞行场景,但官方示例 ( https://threejs.org/examples/?q=fly#misc_controls_fly ) 根本看不懂控制器在干啥,感觉完全失控了。其实 FlyControls 是一个非常好的控制器,可用于飞行游戏,用键盘或鼠标控制飞机,像飞机一样翻滚,只是作者没有写出好的示例,未能体现出它的价值。

将场景控制器更换为 FlyControls,添加一个飞机模型,仅需数十行代码即可完成一个第三人称飞行游戏。

1. 更换控制器 ​

three-tile 的地图模型不依赖特定 controls,所以可以更换任意控制器,跟上个示例第一人称射击一样,将控制器更换为 FlyControls ( https://threejs.org/docs/?q=fly#examples/zh/controls/FlyControls)

2. 飞行 ​

加载一个模型,如小鸟、飞机,固定在摄像机前面,FlyControls 会自动完成飞行控制功能。

ts
import { AnimationMixer, Group, Vector3 } from "three";
import * as tt from "three-tile";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { FLViewer, type FLViewerEventMap } from "./FLViewer";

/**
 * 飞行模型句柄
 */
export interface FlyHandle {
    /** 飞行模型 */
    model: Group;
    /** 清理:移除更新监听、停止动画、移除模型 */
    dispose: () => void;
}

/**
 * 初始化飞行模型
 * @param viewer 场景查看器
 * @param map 地图
 * @param onfly 飞行回调
 * @returns 飞行模型和清理方法
 */
export const initFly = async (
    viewer: FLViewer,
    map: tt.TileMap,
    onfly?: (height: number, evt: FLViewerEventMap["update"]) => void
): Promise<FlyHandle> => {
    const loader = new GLTFLoader();
    const gltf = await loader.loadAsync("../../model/acrobaticPlane_variants.glb");
    const model = gltf.scene;
    model.scale.setScalar(1000);
    const mixer = new AnimationMixer(model);
    const action = gltf.animations[0] ? mixer.clipAction(gltf.animations[0]) : null;
    action?.play();

    // 模型作为摄像机的子节点,自动跟随摄像机的移动与旋转
    viewer.camera.add(model);
    model.position.set(0, -300, -500);

    const tempVec = new Vector3();
    const onUpdate = (evt: FLViewerEventMap["update"]) => {
        // 确保相机矩阵已体现 controls 的最新位置与旋转
        viewer.camera.updateMatrixWorld(true);

        // 模型世界坐标(用于计算地面高度)
        const worldPos = model.getWorldPosition(tempVec);

        // 地面高度
        const groundHeight = map.getLocalInfoFromWorld(worldPos)?.point.y ?? 0;
        // 飞行高度
        const flyHeight = worldPos.y - groundHeight;

        if (flyHeight > -50) {
            // 调整机头方向
            const target = viewer.camera.localToWorld(tempVec.set(0, 0, -10000));
            model.lookAt(target);
            mixer.update(evt.delta);
        }

        // 飞行回调
        onfly?.(flyHeight, evt);
    };
    viewer.addEventListener("update", onUpdate);

    const dispose = () => {
        viewer.removeEventListener("update", onUpdate);
        mixer.stopAllAction();
        model.removeFromParent();
    };

    return { model, dispose };
};

Released under the MIT License.