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