Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json. Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser Stand als normaler Ordner im Repo, ohne Verweis auf GitHub. dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher lokal gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1024 lines
42 KiB
TypeScript
1024 lines
42 KiB
TypeScript
import * as THREE from "three";
|
|
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
|
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
|
|
import { MobileControls } from "./utils/MobileControls";
|
|
import type { PlayerControllerOptions, PlayerModelOptions, VehicleInstance, VehicleOptions, KinematicColliderEntry, KeyMap } from "./types";
|
|
import type { PlayerPlugin } from "./plugins/types";
|
|
import { AnimationSystem } from "./systems/AnimationSystem";
|
|
import { CameraSystem } from "./systems/CameraSystem";
|
|
import { InputSystem } from "./systems/InputSystem";
|
|
import { VehicleSystem } from "./systems/VehicleSystem";
|
|
import { DynamicBodySystem } from "./systems/DynamicBodySystem";
|
|
import { ColliderRegistry } from "./systems/ColliderRegistry";
|
|
import type { DynamicBody, DynamicBodyKind } from "./collision/DynamicBody";
|
|
import type { ColliderDesc, ColliderHandle } from "./collision/colliderDesc";
|
|
import type { MotionType } from "./collision/CollisionWorld";
|
|
import { applyCapsuleCollision, createCollisionTemps, type CollisionTemps } from "./utils/capsuleCollision";
|
|
import { getBbox } from "./utils/bbox";
|
|
import { BvhWorkerPool } from "./utils/BvhWorkerPool";
|
|
import { CollisionWorld } from "./collision/CollisionWorld";
|
|
import { CHARACTER_QUERY_MASK } from "./collision/groups";
|
|
import { CharacterMovement } from "./systems/CharacterMovement";
|
|
import { PlayerDebug } from "./systems/PlayerDebug";
|
|
|
|
/** 未传入delta时计算帧间隔。 */
|
|
let _lastUpdateTime = performance.now();
|
|
|
|
function isMobileDevice() {
|
|
return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
|
|
}
|
|
|
|
export class playerController {
|
|
|
|
// ==================== 场景引用 ====================
|
|
/** GLTF加载器缓存。 */
|
|
private _loader: GLTFLoader | null = null;
|
|
/** GLTF加载器。 */
|
|
get loader(): GLTFLoader {
|
|
return this.initLoader();
|
|
}
|
|
set loader(loader: GLTFLoader) {
|
|
this._loader = loader;
|
|
}
|
|
/** 三维场景。 */
|
|
scene!: THREE.Scene;
|
|
/** 透视相机。 */
|
|
camera!: THREE.PerspectiveCamera;
|
|
/** 轨道控制器。 */
|
|
controls!: OrbitControls;
|
|
|
|
// ==================== 玩家配置 ====================
|
|
/** 模型配置项。 */
|
|
playerModelConfig!: PlayerModelOptions;
|
|
/** 初始出生位置。 */
|
|
private initPos: THREE.Vector3 = new THREE.Vector3(0, 0, 0);
|
|
/** 重力加速度。 */
|
|
gravity = -2400;
|
|
/** 跳跃初速度。 */
|
|
jumpHeight = 600;
|
|
/** 行走速度。 */
|
|
playerSpeed = 200;
|
|
/** 跑步速度。 */
|
|
playerRunSpeed = 600;
|
|
/** 飞行速度。 */
|
|
playerFlySpeed = 2100;
|
|
/** 当前实际速度。 */
|
|
curPlayerSpeed = 0;
|
|
/** 越肩视角开关。 */
|
|
enableOverShoulderView = false;
|
|
/** 显示移动端控件。 */
|
|
isShowMobileControls = true;
|
|
|
|
// ==================== 玩家胶囊体 ====================
|
|
/** 胶囊体半径。 */
|
|
private playerCapsuleRadius = 30;
|
|
/** 半径缩放比。 */
|
|
private playerCapsuleRadiusRatio = 1;
|
|
/** 胶囊体高度。 */
|
|
private playerCapsuleHeight = 180;
|
|
/** 第一人称状态。 */
|
|
isFirstPerson = false;
|
|
|
|
// ==================== 运行状态 ====================
|
|
/** 0步行 1载具。 */
|
|
controllerMode: 0 | 1 = 0;
|
|
/** 是否在地面。 */
|
|
playerIsOnGround = false;
|
|
/** 帧更新开关。 */
|
|
isupdate = true;
|
|
/** 时间缩放系数。 */
|
|
timeScale = 1;
|
|
/** 本帧实际使用的 delta(已钳制 + timeScale)。 */
|
|
currentDelta = 0;
|
|
/** 飞行状态。 */
|
|
isFlying = false;
|
|
/** 临时跳过玩家胶囊碰撞检测。 */
|
|
skipCapsuleCollision = false;
|
|
/** 启用朝向输入。 */
|
|
enableToward = true;
|
|
|
|
// ==================== 玩家物体 ====================
|
|
/** 玩家碰撞胶囊。 */
|
|
playerCapsule!: THREE.Mesh & { capsuleInfo?: any };
|
|
/** 模型根节点。 */
|
|
playerModel: THREE.Object3D | null = null;
|
|
/** 头骨节点。 */
|
|
playerModelHead: THREE.Object3D | null = null;
|
|
|
|
// ==================== 碰撞体 ====================
|
|
/** 碰撞体注册表。 */
|
|
collisionWorld = new CollisionWorld();
|
|
/** 统一创建 / 运动学跟随。 */
|
|
private colliders = new ColliderRegistry(this);
|
|
/** 人物胶囊与地面探测调试。 */
|
|
private playerDebug = new PlayerDebug(this);
|
|
/** 人物移动(胶囊推出)。 */
|
|
private character = new CharacterMovement(this, this.playerDebug);
|
|
/** 当前站立的运动学碰撞体。 */
|
|
activeKinematicCollider: KinematicColliderEntry | null = null;
|
|
/** 当前站立的动态刚体。 */
|
|
activeDynamicBody: DynamicBody | null = null;
|
|
/** 移动系统本帧最终采用的地面支撑;供 IK 等插件与角色接地逻辑保持一致。 */
|
|
private hasGroundSupport = false;
|
|
private readonly groundSupportHit = {
|
|
point: new THREE.Vector3(),
|
|
normal: new THREE.Vector3(0, 1, 0),
|
|
};
|
|
/** 异步 BVH 构建队列。 */
|
|
private readonly bvhWorkerPool = new BvhWorkerPool();
|
|
|
|
// ==================== 碰撞阈值 ====================
|
|
/** 悬空胶囊离地高度。 */
|
|
readonly rideHeight = 40;
|
|
// 站立 / 落地阈值
|
|
/** 站立时胶囊原点应离地的高度。 */
|
|
snapH = 0;
|
|
/** 离地超过此值判为悬空、施加重力。 */
|
|
maxH = 0;
|
|
|
|
// ==================== 台阶视觉平滑 ====================
|
|
/** 插值追赶速度,越大追得越快。 */
|
|
private stepSmoothFactor = 10;
|
|
/** 模型相对胶囊的基准 Y。 */
|
|
private modelBaseY = 0;
|
|
/** 最小法线 Y 分量,地面法线与竖直夹角 ≤ 8° 视为台阶/平地(注入平滑)。 */
|
|
readonly minFloorNormalY = Math.cos(8 * Math.PI / 180);
|
|
|
|
// ==================== 移动端 ====================
|
|
/** 移动端控件。 */
|
|
mobileControls: MobileControls | null = null;
|
|
/** 靠近车辆。 */
|
|
isNearVehicle = false;
|
|
|
|
// ==================== 方向常量 & 复用向量 ====================
|
|
/** 朝向旋转速度。 */
|
|
rotationSpeed = 10;
|
|
/** 世界上方向。 */
|
|
upVector = new THREE.Vector3(0, 1, 0);
|
|
/** 前。 */
|
|
DIR_FWD = new THREE.Vector3(0, 0, -1);
|
|
/** 右。 */
|
|
DIR_RGT = new THREE.Vector3(1, 0, 0);
|
|
|
|
/** XZ 加速响应速度。 */
|
|
playerAcceleration = 30;
|
|
/** XZ 减速响应速度。 */
|
|
playerDeceleration = 30;
|
|
/** 减速基准速度。 */
|
|
decelBase = 300;
|
|
/** 玩家速度。 */
|
|
playerVelocity = new THREE.Vector3();
|
|
/** 相机方向缓存。 */
|
|
camDir = new THREE.Vector3();
|
|
/** 移动方向缓存。 */
|
|
moveDir = new THREE.Vector3();
|
|
/** 步进方向缓存。 */
|
|
xzDir = new THREE.Vector3();
|
|
/** 本步移动前的胶囊位置。 */
|
|
moveStepOrigin = new THREE.Vector3();
|
|
/** 单帧胶囊碰撞子步上限。 */
|
|
maxMoveSteps = 8;
|
|
/** 目标四元数。 */
|
|
targetQuat = new THREE.Quaternion();
|
|
/** 目标变换矩阵。 */
|
|
targetMat = new THREE.Matrix4();
|
|
/** 静态碰撞临时对象。 */
|
|
staticTemps: CollisionTemps = createCollisionTemps();
|
|
/** 运动学碰撞临时对象。 */
|
|
kinematicTemps: CollisionTemps = createCollisionTemps();
|
|
/** 地面检测射线。 */
|
|
groundRaycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0));
|
|
|
|
// ==================== 插件 ====================
|
|
/** 后处理等可选插件。 */
|
|
private plugins: PlayerPlugin[] = [];
|
|
|
|
// ==================== 事件回调 ====================
|
|
/** 动画切换回调。 */
|
|
onAnimationChange?: (name: string, action: THREE.AnimationAction) => void;
|
|
/** 视角切换前回调。 */
|
|
onBeforeViewChange?: (isFirstPerson: boolean) => void;
|
|
/** 视角切换后回调。 */
|
|
onViewChange?: (isFirstPerson: boolean) => void;
|
|
/** 落地状态回调。 */
|
|
onGroundChange?: (onGround: boolean) => void;
|
|
/** 上车回调。 */
|
|
onVehicleEnter?: (vehicle: VehicleInstance) => void;
|
|
/** 下车回调。 */
|
|
onVehicleExit?: (vehicle: VehicleInstance) => void;
|
|
/** 朝向变化回调。 */
|
|
onTowardChange?: (dx: number, dy: number, speed: number) => void;
|
|
|
|
// ==================== 子系统 ====================
|
|
/** 动画系统。 */
|
|
animation = new AnimationSystem(this);
|
|
/** 相机系统。 */
|
|
cam = new CameraSystem(this);
|
|
/** 输入系统。 */
|
|
input = new InputSystem(this);
|
|
/** 载具系统。 */
|
|
vehicle = new VehicleSystem(this);
|
|
/** 动态刚体系统。 */
|
|
dynamics = new DynamicBodySystem(this);
|
|
|
|
constructor() {
|
|
(this.groundRaycaster as any).firstHitOnly = true;
|
|
}
|
|
|
|
// ==================== 初始化 ====================
|
|
|
|
/** 主初始化入口。 */
|
|
async init(opts: PlayerControllerOptions, callback?: () => void) {
|
|
const m = opts.playerModelConfig;
|
|
const s = m.scale ?? 1;
|
|
|
|
this.scene = opts.scene;
|
|
this.camera = opts.camera;
|
|
this.camera.rotation.order = "YXZ";
|
|
this.controls = opts.controls;
|
|
|
|
this.playerModelConfig = m;
|
|
this.initPos = opts.initPos ? opts.initPos.clone() : this.initPos;
|
|
|
|
// 应用玩家参数
|
|
const pm = this.playerModelConfig;
|
|
this.gravity = (pm.gravity ?? this.gravity) * s;
|
|
this.jumpHeight = (pm.jumpHeight ?? this.jumpHeight) * s;
|
|
this.playerSpeed = (pm.speed ?? this.playerSpeed) * s;
|
|
this.playerRunSpeed = (pm.runSpeed ?? this.playerRunSpeed) * s;
|
|
this.playerFlySpeed = (pm.flySpeed ?? this.playerFlySpeed) * s;
|
|
this.curPlayerSpeed = this.playerSpeed;
|
|
this.playerCapsuleRadiusRatio = pm.capsuleRadiusRatio ?? this.playerCapsuleRadiusRatio;
|
|
this.playerAcceleration = pm.acceleration ?? this.playerAcceleration;
|
|
this.playerDeceleration = pm.deceleration ?? this.playerDeceleration;
|
|
this.decelBase = this.playerSpeed;
|
|
|
|
// 应用相机参数
|
|
this.cam.sensitivity = opts.mouseSensitivity ?? this.cam.sensitivity;
|
|
this.cam.mouseMode = opts.thirdMouseMode ?? this.cam.mouseMode;
|
|
this.cam.enableSpringCamera = opts.enableSpringCamera ?? this.cam.enableSpringCamera;
|
|
this.cam.springCameraTime = opts.springCameraTime ?? this.cam.springCameraTime;
|
|
this.cam.zoomEnabled = opts.enableZoom ?? this.cam.zoomEnabled;
|
|
this.cam.minDist = (opts.minCamDistance ?? this.cam.minDist) * s;
|
|
this.cam.maxDist = (opts.maxCamDistance ?? this.cam.maxDist) * s;
|
|
this.cam.lookAtHeightRatio = opts.camLookAtHeightRatio ?? this.cam.lookAtHeightRatio;
|
|
this.cam.overShoulderOffsetRatio = opts.camOverShoulderOffsetRatio ?? this.cam.overShoulderOffsetRatio;
|
|
this.cam.originMaxDist = this.cam.maxDist;
|
|
this.cam.epsilon = this.cam.epsilon * s;
|
|
|
|
this.isShowMobileControls = (opts.isShowMobileControls ?? this.isShowMobileControls) && isMobileDevice();
|
|
this.enableOverShoulderView = opts.enableOverShoulderView ?? this.enableOverShoulderView;
|
|
this.isFirstPerson = opts.isFirstPerson ?? this.isFirstPerson;
|
|
this.timeScale = opts.timeScale ?? this.timeScale;
|
|
|
|
// 自定义键位
|
|
if (opts.keyMap) this.input.buildKeyMap(opts.keyMap);
|
|
|
|
// 初始化移动端控件
|
|
if (this.isShowMobileControls) {
|
|
this.mobileControls = new MobileControls(i => this.input.setInput(i), this.controls);
|
|
await this.mobileControls.init(opts.mobileControls);
|
|
}
|
|
|
|
if (opts.colliders?.length) {
|
|
for (const desc of opts.colliders) this.addCollider(desc);
|
|
}
|
|
await this.loadPlayerModel();
|
|
|
|
this.input.bindEvents();
|
|
this.cam.setCamPos();
|
|
this.cam.initControls();
|
|
this.cam.setOverShoulder(this.isFirstPerson ? false : this.enableOverShoulderView);
|
|
callback?.();
|
|
}
|
|
|
|
/** 初始化加载器。 */
|
|
private initLoader(): GLTFLoader {
|
|
if (this._loader) return this._loader;
|
|
|
|
const loader = new GLTFLoader();
|
|
const dracoLoader = new DRACOLoader();
|
|
dracoLoader.setDecoderPath("https://unpkg.com/three@0.182.0/examples/jsm/libs/draco/gltf/");
|
|
loader.setDRACOLoader(dracoLoader);
|
|
this._loader = loader;
|
|
return loader;
|
|
}
|
|
|
|
// ==================== 玩家模型 ====================
|
|
|
|
/** 加载模型与动画。 */
|
|
private async loadPlayerModel() {
|
|
try {
|
|
const config = this.playerModelConfig;
|
|
let model: THREE.Object3D;
|
|
let animations: THREE.AnimationClip[];
|
|
|
|
if (config.model) {
|
|
model = config.model;
|
|
animations = config.animations;
|
|
} else {
|
|
const gltf = await this.loader.loadAsync(config.url);
|
|
model = gltf.scene;
|
|
animations = gltf.animations ?? [];
|
|
}
|
|
|
|
this.playerModel = model;
|
|
|
|
// 初始化动画混合器
|
|
this.animation.mixer = new THREE.AnimationMixer(this.playerModel);
|
|
this.animation.clips = animations;
|
|
this.animation.actions = new Map();
|
|
|
|
// 构建动作映射表
|
|
const mc = this.playerModelConfig;
|
|
const isThreePartJump = Array.isArray(mc.jumpAnim);
|
|
this.animation.hasThreePartJump = isThreePartJump;
|
|
const mappings: [string, string][] = [
|
|
[mc.idleAnim, "idle"],
|
|
[mc.walkAnim, "walking"],
|
|
[mc.leftWalkAnim || mc.walkAnim, "left_walking"],
|
|
[mc.rightWalkAnim || mc.walkAnim, "right_walking"],
|
|
[mc.backwardAnim || mc.walkAnim, "walking_backward"],
|
|
...(typeof mc.jumpAnim === "string"
|
|
? [[mc.jumpAnim, "jumping"]] as [string, string][]
|
|
: [] as [string, string][]),
|
|
[mc.runAnim, "running"],
|
|
[mc.flyIdleAnim || mc.idleAnim, "flyidle"],
|
|
[mc.flyAnim || mc.idleAnim, "flying"],
|
|
[mc.flyHoverForwardAnim || mc.flyAnim || mc.idleAnim, "flyHoverForward"],
|
|
[mc.flyHoverBackAnim || mc.flyIdleAnim || mc.idleAnim, "flyHoverBack"],
|
|
[mc.flyHoverLeftAnim || mc.flyIdleAnim || mc.idleAnim, "flyHoverLeft"],
|
|
[mc.flyHoverRightAnim || mc.flyIdleAnim || mc.idleAnim, "flyHoverRight"],
|
|
[mc.flyHoverUpAnim || mc.flyIdleAnim || mc.idleAnim, "flyHoverUp"],
|
|
[mc.flyHoverDownAnim || mc.flyIdleAnim || mc.idleAnim, "flyHoverDown"],
|
|
[mc.drivingAnim || mc.idleAnim, "driving"],
|
|
];
|
|
|
|
for (const [clipName, actionName] of mappings) {
|
|
const clip = animations.find(a => a.name === clipName);
|
|
if (!clip) continue;
|
|
const action = this.animation.mixer.clipAction(clip);
|
|
if (actionName === "jumping") {
|
|
action.setLoop(THREE.LoopOnce, 1);
|
|
action.clampWhenFinished = true;
|
|
action.setEffectiveTimeScale(1.2);
|
|
} else {
|
|
action.setLoop(THREE.LoopRepeat, Infinity);
|
|
action.setEffectiveTimeScale(1);
|
|
}
|
|
action.enabled = true;
|
|
action.setEffectiveWeight(0);
|
|
this.animation.actions.set(actionName, action);
|
|
}
|
|
|
|
// 注册三段跳跃动画
|
|
if (Array.isArray(mc.jumpAnim)) {
|
|
const [startClip, loopClip, endClip] = mc.jumpAnim;
|
|
const jumpDefs: [string, string, number, boolean][] = [
|
|
[startClip, "jumpStart", THREE.LoopOnce, true],
|
|
[loopClip, "jumpLoop", THREE.LoopRepeat, false],
|
|
[endClip, "jumpEnd", THREE.LoopOnce, true],
|
|
];
|
|
for (const [clipName, key, loop, clamp] of jumpDefs) {
|
|
const clip = animations.find(a => a.name === clipName);
|
|
if (!clip) { console.warn(`找不到跳跃动画 clip: "${clipName}"`); continue; }
|
|
const action = this.animation.mixer!.clipAction(clip);
|
|
action.setLoop(loop as THREE.AnimationActionLoopStyles, loop === THREE.LoopOnce ? 1 : Infinity);
|
|
action.clampWhenFinished = clamp;
|
|
action.setEffectiveTimeScale(key === "jumpStart" ? 1.2 : 1);
|
|
action.enabled = true;
|
|
action.setEffectiveWeight(0);
|
|
this.animation.actions.set(key, action);
|
|
}
|
|
}
|
|
|
|
// 注册默认动作组
|
|
const defaultSet = new Map<string, THREE.AnimationAction>();
|
|
for (const key of ["idle", "walking", "walking_backward", "running", "jumping", "flyidle", "flying"]) {
|
|
const action = this.animation.actions.get(key);
|
|
if (action) defaultSet.set(key, action);
|
|
}
|
|
this.animation.sets.set("default", defaultSet);
|
|
|
|
this.animation.actions.get("idle")?.setEffectiveWeight(1);
|
|
this.animation.actions.get("idle")?.play();
|
|
this.animation.state = this.animation.actions.get("idle")!;
|
|
|
|
// 监听动画完成事件
|
|
this.animation.mixerCb = (ev: any) => {
|
|
const done: THREE.AnimationAction = ev.action;
|
|
const resolveGroundAnim = () => {
|
|
if (this.input.fwd) { this.animation.playByName(this.input.shift ? "running" : "walking"); return; }
|
|
if (this.input.bkd) { this.animation.playByName("walking_backward"); return; }
|
|
if (this.input.rgt || this.input.lft) { this.animation.playByName("walking"); return; }
|
|
this.animation.playByName("idle");
|
|
};
|
|
if (done === this.animation.actions?.get("jumping")) {
|
|
if (this.animation.state === done) resolveGroundAnim();
|
|
return;
|
|
}
|
|
if (done === this.animation.actions?.get("jumpStart")) {
|
|
if (this.animation.state === done) this.animation.playByName("jumpLoop");
|
|
return;
|
|
}
|
|
if (done === this.animation.actions?.get("jumpEnd")) {
|
|
if (this.animation.state === done) resolveGroundAnim();
|
|
return;
|
|
}
|
|
};
|
|
this.animation.mixer.addEventListener("finished", this.animation.mixerCb);
|
|
|
|
this.animation.mixer.update(0);
|
|
this.playerModel.updateMatrixWorld(true);
|
|
|
|
// 计算胶囊体尺寸
|
|
const { size } = getBbox(this.playerModel);
|
|
const modelScale = this.playerCapsuleHeight / size.y;
|
|
|
|
const s = this.playerModelConfig.scale;
|
|
const r = this.playerCapsuleRadius * s * this.playerCapsuleRadiusRatio;
|
|
const h = this.playerCapsuleHeight * s;
|
|
|
|
// 悬空胶囊:碰撞椭球底部永久离脚 rideHeight
|
|
const rideHeightScaled = this.rideHeight * s; // 当前缩放下的实际悬空高度
|
|
const colliderHeight = h - rideHeightScaled; // 缩短后胶囊总高
|
|
|
|
const segmentLength = colliderHeight - 2 * r;
|
|
|
|
// 创建胶囊体网格(原点对齐 segment 上端;depthTest 与其它调试线框一致)
|
|
this.playerCapsule = new THREE.Mesh(
|
|
new THREE.CapsuleGeometry(r, Math.max(segmentLength, 1e-6), 4, 8),
|
|
new THREE.MeshBasicMaterial({
|
|
color: 0xc45cff,
|
|
wireframe: true,
|
|
transparent: true,
|
|
opacity: 0.7,
|
|
side: THREE.DoubleSide,
|
|
}),
|
|
);
|
|
this.playerCapsule.geometry.translate(0, -segmentLength / 2, 0);
|
|
this.playerCapsule.capsuleInfo = {
|
|
radius: r,
|
|
segment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -segmentLength, 0)),
|
|
// 动态体用接到脚底的完整胶囊,避免球从悬空间隙钻过
|
|
dynamicsSegment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -segmentLength - rideHeightScaled, 0)),
|
|
};
|
|
this.recomputeGroundThresholds();
|
|
this.playerCapsule.name = "capsule";
|
|
(this.playerCapsule.material as THREE.Material).visible = false;
|
|
this.scene.add(this.playerCapsule);
|
|
this.reset();
|
|
this.playerCapsule.rotateY(this.playerModelConfig.rotateY ?? 0);
|
|
|
|
// 挂载模型到胶囊
|
|
this.playerModel.scale.multiplyScalar(modelScale * s);
|
|
this.modelBaseY = -segmentLength - r - rideHeightScaled;
|
|
this.playerModel.position.set(0, this.modelBaseY, 0);
|
|
this.playerModel.traverse((child: any) => {
|
|
if (child.name === this.playerModelConfig?.headBoneName) this.playerModelHead = child;
|
|
});
|
|
this.playerCapsule.add(this.playerModel);
|
|
this.reset();
|
|
} catch (e) {
|
|
console.error("加载玩家模型失败:", e);
|
|
}
|
|
}
|
|
|
|
/** 切换玩家模型。 */
|
|
async switchPlayerModel(newPlayerModel: PlayerModelOptions) {
|
|
// 保存当前状态
|
|
const savedPos = this.playerCapsule.position.clone();
|
|
const savedQuat = this.playerCapsule.quaternion.clone();
|
|
const wasFirstPerson = this.isFirstPerson;
|
|
|
|
if (wasFirstPerson) this.scene.attach(this.camera);
|
|
if (this.playerCapsule) this.scene.remove(this.playerCapsule);
|
|
if (this.playerModel) { this.playerCapsule.remove(this.playerModel); this.playerModel = null; this.playerModelHead = null; }
|
|
|
|
// 清除旧动画资源
|
|
const anim = this.animation;
|
|
if (anim.mixer) {
|
|
if (anim.mixerCb) { anim.mixer.removeEventListener("finished", anim.mixerCb); anim.mixerCb = undefined; }
|
|
anim.mixer.stopAllAction();
|
|
anim.mixer.uncacheRoot(anim.mixer.getRoot());
|
|
anim.mixer = undefined;
|
|
anim.actions = undefined;
|
|
}
|
|
|
|
// 更新比例相关参数
|
|
const ratio = newPlayerModel.scale / this.playerModelConfig.scale;
|
|
this.playerModelConfig = {
|
|
...this.playerModelConfig,
|
|
url: undefined,
|
|
model: undefined,
|
|
animations: undefined,
|
|
...newPlayerModel,
|
|
} as PlayerModelOptions;
|
|
|
|
this.applyGameplayScaleRatio(ratio);
|
|
|
|
await this.loadPlayerModel();
|
|
this.playerCapsule.position.copy(savedPos);
|
|
this.playerCapsule.quaternion.copy(savedQuat);
|
|
if (wasFirstPerson) this.cam.setFirstPerson();
|
|
this.syncDebugVisibility();
|
|
for (const plugin of this.plugins) plugin.onPlayerModelChange?.();
|
|
}
|
|
|
|
// ==================== 插件 ====================
|
|
|
|
/** 注册后处理插件。 */
|
|
use(plugin: PlayerPlugin): this {
|
|
if (this.plugins.includes(plugin)) return this;
|
|
if (plugin.name) {
|
|
const existing = this.plugins.find(p => p.name === plugin.name);
|
|
if (existing) this.unuse(existing);
|
|
}
|
|
this.plugins.push(plugin);
|
|
plugin.onAttach?.(this);
|
|
return this;
|
|
}
|
|
|
|
/** 卸载已注册的插件。 */
|
|
unuse(plugin: PlayerPlugin): this {
|
|
const index = this.plugins.indexOf(plugin);
|
|
if (index < 0) return this;
|
|
this.plugins.splice(index, 1);
|
|
plugin.onDetach?.();
|
|
return this;
|
|
}
|
|
|
|
/** 获取当前插件列表的只读副本。 */
|
|
getPlugins(): PlayerPlugin[] {
|
|
return this.plugins.slice();
|
|
}
|
|
|
|
// ==================== 碰撞体构建和查询 ====================
|
|
|
|
/** @internal BVH worker。 */
|
|
getBvhWorkerPool() { return this.bvhWorkerPool; }
|
|
/** @internal 静态 / 运动学 mesh 线框开关。 */
|
|
getDisplayCollider() { return this.colliders.getDebugVisible(); }
|
|
/** @internal 动态刚体线框开关。 */
|
|
getDisplayDynamicBody() { return this.dynamics.getDebugVisible(); }
|
|
|
|
/** 统一创建碰撞体。 */
|
|
addCollider(desc: ColliderDesc): ColliderHandle {
|
|
return this.colliders.add(desc);
|
|
}
|
|
|
|
/** 按句柄移除碰撞体。 */
|
|
removeCollider(handle: ColliderHandle | number): void {
|
|
this.colliders.remove(handle);
|
|
}
|
|
|
|
/** 运行时等比缩放已烘焙的运动学 mesh 碰撞(不重建 BVH)。 */
|
|
scaleKinematicColliderContent(source: THREE.Object3D, ratio: number): void {
|
|
this.colliders.scaleKinematicContent(source, ratio);
|
|
}
|
|
|
|
/** 运行时平移已烘焙的运动学 mesh 碰撞(本地空间,不重建 BVH)。 */
|
|
translateKinematicColliderContent(source: THREE.Object3D, localOffset: THREE.Vector3): void {
|
|
this.colliders.translateKinematicContent(source, localOffset);
|
|
}
|
|
|
|
/** 清除碰撞体;可按 motion 过滤。 */
|
|
clearColliders(filter?: { motion?: MotionType }): void {
|
|
this.colliders.clear(filter);
|
|
}
|
|
|
|
/** 人物胶囊 / 相机查询用的静态与运动学网格。 */
|
|
getColliderMeshes(options?: { skipIds?: number[] }): THREE.Mesh[] {
|
|
return this.collisionWorld.queryMeshes({ mask: CHARACTER_QUERY_MASK }, options);
|
|
}
|
|
|
|
/** 车辆轮射线 / 车身接触使用的网格。跳过全部车辆外观,车车改走底盘盒。 */
|
|
getVehicleGroundMeshes(v: VehicleInstance): THREE.Mesh[] {
|
|
const chassis = v.chassisColliderId != null ? this.collisionWorld.get(v.chassisColliderId) : null;
|
|
const skipIds = this.vehicle.meshSkipIds;
|
|
skipIds.length = 0;
|
|
for (const other of this.vehicle.list) {
|
|
if (other.meshColliderId != null) skipIds.push(other.meshColliderId);
|
|
}
|
|
if (!chassis) return [];
|
|
return this.collisionWorld.queryMeshes(chassis, { skipIds });
|
|
}
|
|
|
|
/** 静态碰撞体是否已有至少一份就绪的 mesh。 */
|
|
isStaticColliderUsable() {
|
|
return this.colliders.isStaticUsable();
|
|
}
|
|
|
|
/** 运动学碰撞体是否已就绪。 */
|
|
isKinematicColliderUsable(entry: KinematicColliderEntry) {
|
|
return this.colliders.isKinematicUsable(entry);
|
|
}
|
|
|
|
/** 更新运动学碰撞体网格,并计算本帧位移增量。 */
|
|
private updateKinematicColliders() {
|
|
this.colliders.updateKinematicFollow();
|
|
}
|
|
|
|
/** 本帧碰撞用完后再提交平台矩阵,供车辆按 prev→current 带走。 */
|
|
private commitKinematicPrevMatrices() {
|
|
this.colliders.commitKinematicPrev();
|
|
}
|
|
|
|
// ==================== 主循环 ====================
|
|
|
|
/** 主循环。 */
|
|
async update(delta?: number) {
|
|
if (delta === undefined) {
|
|
const now = performance.now();
|
|
delta = (now - _lastUpdateTime) / 1000;
|
|
_lastUpdateTime = now;
|
|
}
|
|
if (!this.isupdate || !this.playerCapsule) return;
|
|
delta = Math.min(delta, 1 / 40) * this.timeScale;
|
|
this.currentDelta = delta;
|
|
this.updateKinematicColliders();
|
|
if (this.controllerMode === 1) {
|
|
this.vehicle.preparePhysics(delta);
|
|
this.vehicle.finishPhysics(delta);
|
|
if (!this.isFirstPerson) this.cam.updateThirdPersonVehicle(delta);
|
|
this.runAnimationPass(delta);
|
|
} else {
|
|
this.updatePlayer(delta);
|
|
// 停着的车也每帧步进,避免球砸/叠车冲量攒着、视觉冻住
|
|
this.vehicle.finishPhysics(delta);
|
|
}
|
|
this.dynamics.step(delta);
|
|
this.character.applyDynamicSupportCarry();
|
|
this.commitKinematicPrevMatrices();
|
|
}
|
|
|
|
/** 玩家帧更新。 */
|
|
updatePlayer(delta: number) {
|
|
this.character.update(delta);
|
|
}
|
|
|
|
/** 在插件的恢复与应用钩子之间更新动画混合器。 */
|
|
runAnimationPass(delta: number): void {
|
|
for (const plugin of this.plugins) plugin.onBeforeAnimationUpdate?.(delta);
|
|
this.animation.updateMixers(delta);
|
|
for (const plugin of this.plugins) plugin.onAfterAnimationUpdate?.(delta);
|
|
}
|
|
|
|
// ==================== 内部辅助 ====================
|
|
|
|
/** 同步全部 debug 可见性。 */
|
|
syncDebugVisibility() {
|
|
this.playerDebug.syncVisibility();
|
|
this.colliders.syncDebugVisibility();
|
|
this.dynamics.syncDebugVisibility();
|
|
this.vehicle.syncDebugVisibility();
|
|
}
|
|
|
|
/** 设置落地状态。 */
|
|
setOnGround(val: boolean) {
|
|
if (!val) this.clearGroundSupport(); // 离地时立即作废上一帧支撑。
|
|
if (this.playerIsOnGround === val) return;
|
|
this.playerIsOnGround = val;
|
|
this.onGroundChange?.(val);
|
|
if (val) this.animation.onLand();
|
|
else this.animation.onBecomeAirborne();
|
|
}
|
|
|
|
/** 应用重力。 */
|
|
applyGravity(delta: number) {
|
|
this.playerVelocity.y += delta * this.gravity;
|
|
this.setOnGround(false);
|
|
}
|
|
|
|
/** 判断脚下地面是否为水平台面(法线接近竖直)。 */
|
|
isFlatFloor(hit: THREE.Intersection): boolean {
|
|
const n = hit.face?.normal;
|
|
if (!n) return true;
|
|
return n.y >= this.minFloorNormalY; // 大于等于最小法线 Y 分量时为水平台面
|
|
}
|
|
|
|
/** 吸附到地面。 */
|
|
snapToGround(groundY: number, smooth = false, delta = 0) {
|
|
this.playerVelocity.y = 0;
|
|
const dy = groundY - this.playerCapsule.position.y;
|
|
if (smooth && Math.abs(dy) <= this.rideHeight * this.playerModelConfig.scale) {
|
|
// 平滑吸附
|
|
this.playerCapsule.position.y += dy * Math.min(1, this.stepSmoothFactor * delta);
|
|
} else {
|
|
// 瞬时吸附
|
|
this.playerCapsule.position.y = groundY;
|
|
}
|
|
this.setOnGround(true);
|
|
}
|
|
|
|
/** 按比例同步重力、速度、相机距离等玩法参数。 */
|
|
private applyGameplayScaleRatio(ratio: number) {
|
|
this.gravity *= ratio;
|
|
this.jumpHeight *= ratio;
|
|
this.playerSpeed *= ratio;
|
|
this.playerRunSpeed *= ratio;
|
|
this.playerFlySpeed *= ratio;
|
|
this.curPlayerSpeed *= ratio;
|
|
this.decelBase *= ratio;
|
|
this.cam.epsilon *= ratio;
|
|
this.cam.minDist *= ratio;
|
|
this.cam.maxDist *= ratio;
|
|
this.cam.originMaxDist *= ratio;
|
|
this.controls.minDistance = this.cam.minDist;
|
|
}
|
|
|
|
/** 重算站立 / 落地阈值(snapH / maxH)。仅在胶囊创建、缩放后调用。 */
|
|
private recomputeGroundThresholds() {
|
|
const info = this.playerCapsule?.capsuleInfo;
|
|
if (!info) return;
|
|
const sy = this.playerCapsule.scale.y || 1;
|
|
const rideHeightScaled = this.rideHeight * this.playerModelConfig.scale;
|
|
this.snapH = (-info.segment.end.y) * sy + info.radius + rideHeightScaled;
|
|
this.maxH = this.snapH + rideHeightScaled;
|
|
}
|
|
|
|
/** 动态修改玩家缩放。 */
|
|
setPlayerScale(newScale: number) {
|
|
if (newScale <= 0) return;
|
|
const ratio = newScale / this.playerModelConfig.scale;
|
|
this.playerModelConfig.scale = newScale;
|
|
|
|
// 更新比例相关参数
|
|
this.applyGameplayScaleRatio(ratio);
|
|
|
|
if (this.isFirstPerson) this.scene.attach(this.camera);
|
|
this.playerCapsule?.scale.multiplyScalar(ratio);
|
|
if (this.playerCapsule?.capsuleInfo) {
|
|
this.playerCapsule.capsuleInfo.radius *= ratio;
|
|
this.recomputeGroundThresholds();
|
|
}
|
|
if (this.isFirstPerson) this.cam.setFirstPerson();
|
|
}
|
|
|
|
/** 动态修改单辆车的缩放(绝对 scale,与加载时 opts.scale 同一语义)。 */
|
|
setVehicleScale(vehicle: VehicleInstance, newScale: number) {
|
|
this.vehicle.setScale(vehicle, newScale);
|
|
}
|
|
|
|
/** 动态修改单辆车的底盘离地间隙(scale=1 基准值)。 */
|
|
setVehicleClearance(vehicle: VehicleInstance, clearance: number) {
|
|
this.vehicle.setClearance(vehicle, clearance);
|
|
}
|
|
|
|
/** 动态修改单辆车的底盘碰撞盒三轴尺寸比例。 */
|
|
setVehicleChassisSizeScale(
|
|
vehicle: VehicleInstance,
|
|
sizeScale: { x?: number; y?: number; z?: number },
|
|
) {
|
|
this.vehicle.setChassisSizeScale(vehicle, sizeScale);
|
|
}
|
|
|
|
/** 将全部已加载车辆缩放到同一绝对 scale。 */
|
|
setAllVehiclesScale(newScale: number) {
|
|
this.vehicle.setScaleAll(newScale);
|
|
}
|
|
|
|
/** 重置玩家位置。 */
|
|
reset(position?: THREE.Vector3) {
|
|
if (!this.playerCapsule) return;
|
|
if (this.controllerMode === 1) {
|
|
this.vehicle.stopActive();
|
|
this.controllerMode = 0;
|
|
this.mobileControls?.syncControllerModeBtn(0);
|
|
this.scene.attach(this.playerCapsule);
|
|
this.animation.playByName("idle");
|
|
this.syncDebugVisibility();
|
|
}
|
|
this.playerVelocity.set(0, 0, 0);
|
|
this.activeDynamicBody = null;
|
|
this.clearGroundSupport();
|
|
this.playerCapsule.position.copy(position ?? this.initPos);
|
|
}
|
|
|
|
/** 站立时胶囊原点应离地的高度。 */
|
|
getCapsuleGroundHeight() { return this.snapH; }
|
|
|
|
/** 运动学碰撞体列表(供车辆下车检测使用)。 */
|
|
getKinematicColliderEntries() { return this.colliders.kinematicColliders; }
|
|
|
|
/** 将人物模型挂载到车辆座位点。 */
|
|
syncMountedPlayer(vehicle: VehicleInstance) {
|
|
const cap = this.playerCapsule;
|
|
if (!cap) return;
|
|
this.clearGroundSupport();
|
|
if (cap.parent !== vehicle.vehicleGroup) vehicle.vehicleGroup.attach(cap);
|
|
cap.position.copy(vehicle.driverSeatPosition).multiplyScalar(vehicle.scale);
|
|
cap.quaternion.setFromAxisAngle(this.upVector, vehicle.driverSeatRotation);
|
|
}
|
|
|
|
/** 在车辆下车位置恢复人物控制。 */
|
|
leaveVehicleAt(position: THREE.Vector3, forward: THREE.Vector3) {
|
|
const cap = this.playerCapsule;
|
|
if (!cap) return;
|
|
this.scene.attach(cap);
|
|
cap.position.copy(position);
|
|
const lookTarget = position.clone().add(forward);
|
|
this.targetMat.lookAt(position, lookTarget, this.upVector);
|
|
cap.quaternion.setFromRotationMatrix(this.targetMat);
|
|
this.playerVelocity.set(0, 0, 0);
|
|
this.setOnGround(false);
|
|
if (this.isFirstPerson) this.cam.setFirstPerson();
|
|
}
|
|
|
|
// ==================== API ====================
|
|
|
|
/** 获取当前位置。 */
|
|
getPosition() { return this.playerCapsule?.position; }
|
|
/** 获取速度。 */
|
|
getVelocity() { return this.playerVelocity.clone(); }
|
|
/** 获取第一人称状态。 */
|
|
getIsFirstPerson() { return this.isFirstPerson; }
|
|
/** 获取飞行状态。 */
|
|
getIsFlying() { return this.isFlying; }
|
|
/** 获取落地状态。 */
|
|
getIsOnGround() { return this.playerIsOnGround; }
|
|
/** 获取移动系统本帧最终采用的地面支撑。 */
|
|
getGroundSupport() { return this.hasGroundSupport ? this.groundSupportHit : null; }
|
|
/** 获取本帧实际使用的 delta(已钳制 + timeScale)。 */
|
|
getCurrentDelta() { return this.currentDelta; }
|
|
/** 获取控制器模式。 */
|
|
getControllerMode() { return this.controllerMode; }
|
|
/** 获取玩家模型。 */
|
|
getPlayerModel() { return this.playerModel; }
|
|
/** 获取胶囊体。 */
|
|
getPlayerCapsule() { return this.playerCapsule; }
|
|
/** 获取当前载具。 */
|
|
getActiveVehicle() { return this.vehicle.active; }
|
|
/** 获取所有载具。 */
|
|
getAllVehicles() { return this.vehicle.list; }
|
|
/** 获取当前站立的运动学碰撞体。 */
|
|
getActiveKinematicCollider() { return this.activeKinematicCollider; }
|
|
/** 获取当前站立的动态刚体。 */
|
|
getActiveDynamicBody() { return this.activeDynamicBody; }
|
|
|
|
/** 写入移动系统最终采用的地面点和世界空间法线。 */
|
|
setGroundSupport(point: THREE.Vector3, normal: THREE.Vector3) {
|
|
this.groundSupportHit.point.copy(point);
|
|
this.groundSupportHit.normal.copy(normal).normalize();
|
|
this.hasGroundSupport = true;
|
|
}
|
|
|
|
/** 清除已经失效的地面支撑。 */
|
|
clearGroundSupport() { this.hasGroundSupport = false; }
|
|
|
|
/** 移除动态刚体。 */
|
|
removeDynamicBody(body: DynamicBody) { this.dynamics.remove(body); }
|
|
/** 从指定世界坐标向下查询动态刚体表面。 */
|
|
raycastDynamicGround(
|
|
origin: THREE.Vector3,
|
|
minNormalY?: number,
|
|
excludeKinds?: readonly DynamicBodyKind[],
|
|
) {
|
|
return this.dynamics.raycastGround(origin, minNormalY, excludeKinds);
|
|
}
|
|
/** 全部动态刚体。 */
|
|
getDynamicBodies(): DynamicBody[] { return this.dynamics.list; }
|
|
/** 清除全部动态刚体。 */
|
|
clearDynamicBodies() { this.dynamics.clear(); }
|
|
|
|
/** 设置鼠标灵敏度。 */
|
|
setMouseSensitivity(value: number) {
|
|
this.cam.sensitivity = value;
|
|
this.controls.rotateSpeed = value * 0.05;
|
|
}
|
|
|
|
// --- 玩家参数 ---
|
|
/** 设置重力。 */
|
|
setGravity(gravity: number) { this.gravity = gravity * this.playerModelConfig.scale; }
|
|
/** 设置跳跃高度。 */
|
|
setJumpHeight(jumpHeight: number) { this.jumpHeight = jumpHeight * this.playerModelConfig.scale; }
|
|
/** 设置行走速度。 */
|
|
setPlayerSpeed(speed: number) { this.playerSpeed = speed * this.playerModelConfig.scale; this.curPlayerSpeed = this.playerSpeed; }
|
|
/** 设置跑步速度。 */
|
|
setPlayerRunSpeed(runSpeed: number) { this.playerRunSpeed = runSpeed * this.playerModelConfig.scale; }
|
|
/** 设置飞行速度。 */
|
|
setPlayerFlySpeed(flySpeed: number) { this.playerFlySpeed = flySpeed * this.playerModelConfig.scale; }
|
|
/** 设置朝向开关。 */
|
|
setEnableToward(v: boolean) { this.enableToward = v; }
|
|
|
|
// --- 相机参数 ---
|
|
/** 设置相机最近距。 */
|
|
setMinCamDistance(dist: number) {
|
|
this.cam.minDist = dist * this.playerModelConfig.scale;
|
|
this.cam.originMaxDist = Math.max(this.cam.originMaxDist, this.cam.minDist);
|
|
this.cam.maxDist = Math.max(this.cam.maxDist, this.cam.minDist);
|
|
this.controls.minDistance = this.cam.minDist;
|
|
}
|
|
/** 设置相机最远距。 */
|
|
setMaxCamDistance(dist: number) {
|
|
this.cam.originMaxDist = Math.max(this.cam.minDist, dist * this.playerModelConfig.scale);
|
|
this.cam.maxDist = this.cam.originMaxDist;
|
|
this.cam.clearFlySprintMaxDistBoost();
|
|
}
|
|
/** 设置相机看向点高度比例。 */
|
|
setCamLookAtHeightRatio(ratio: number) { this.cam.lookAtHeightRatio = ratio; }
|
|
/** 设置相机过肩视角横向偏移比例。 */
|
|
setCamOverShoulderOffsetRatio(ratio: number) { this.cam.overShoulderOffsetRatio = ratio; this.cam.setOverShoulder(this.enableOverShoulderView && !this.isFirstPerson); }
|
|
|
|
/** 设置鼠标模式。 */
|
|
setThirdMouseMode(mode: 0 | 1 | 2 | 3 | 4 | 5) { this.cam.mouseMode = mode; this.cam.setPointerLock(); }
|
|
/** 设置缩放开关。 */
|
|
setEnableZoom(enable: boolean) { this.cam.setZoomEnabled(enable); }
|
|
|
|
// --- 调试 ---
|
|
/** 设置静态碰撞线框。 */
|
|
setColliderDebug(debug: boolean) {
|
|
this.colliders.setDebugVisible(debug);
|
|
}
|
|
/** 设置玩家胶囊线框。 */
|
|
setPlayerCapsuleDebug(debug: boolean) {
|
|
this.playerDebug.setVisible(debug);
|
|
}
|
|
/** 设置动态刚体碰撞线框。 */
|
|
setDynamicBodyDebug(debug: boolean) {
|
|
this.dynamics.setDebugVisible(debug);
|
|
}
|
|
/** 设置车辆底盘物理盒。 */
|
|
setVehiclePhysicsDebug(debug: boolean) {
|
|
this.vehicle.setPhysicsDebugVisible(debug);
|
|
}
|
|
/** 临时跳过玩家胶囊碰撞检测。 */
|
|
setSkipCapsuleCollision(skip: boolean) { this.skipCapsuleCollision = skip; }
|
|
|
|
// --- 动画 ---
|
|
/** 按名播放动画。 */
|
|
playPlayerAnimationByName(name: string, fade?: number) { this.animation.playByName(name, fade); }
|
|
/** 注册自定义动画。 */
|
|
registerAnimation(key: string, clipName: string, opts?: Parameters<AnimationSystem["register"]>[2]) { this.animation.register(key, clipName, opts); }
|
|
/** 播放已注册动画。 */
|
|
playAnimation(key: string, opts?: Parameters<AnimationSystem["play"]>[1]) { this.animation.play(key, opts); }
|
|
/** 注册移动动作组。 */
|
|
registerLocomotionSet(setName: string, map: Parameters<AnimationSystem["registerLocomotionSet"]>[1]) { this.animation.registerLocomotionSet(setName, map); }
|
|
/** 切换移动动作组。 */
|
|
switchLocomotionSet(setName: string, fade?: number) { this.animation.switchLocomotionSet(setName, fade); }
|
|
/** 获取当前动画名。 */
|
|
getCurrentPlayerAnimationName() { return this.animation.getCurrentName(); }
|
|
/** 获取当前移动动作组名。 */
|
|
getCurrentLocomotionSet() { return this.animation.currentLocomotionSet; }
|
|
|
|
// --- 相机 ---
|
|
/** 切换视角模式。 */
|
|
changeView() { this.cam.changeView(); }
|
|
/** 设置第一人称。 */
|
|
setFirstPersonCamera(v = 0) { this.cam.setFirstPerson(v); }
|
|
/** 设置越肩视角。 */
|
|
setOverShoulderView(v: boolean) { this.cam.setOverShoulder(v); this.enableOverShoulderView = v; }
|
|
/** 屏幕中心检测。 */
|
|
getCenterScreenRaycastHit() { return this.cam.getCenterHit(); }
|
|
|
|
// --- 输入 ---
|
|
/** 设置输入状态。 */
|
|
setInput(input: Parameters<InputSystem["setInput"]>[0]) { this.input.setInput(input); }
|
|
/** 运行时自定义键位。 */
|
|
setKeyMap(map?: KeyMap) { this.input.buildKeyMap(map); }
|
|
/** 绑定输入事件。 */
|
|
onAllEvent() { this.input.bindEvents(); }
|
|
/** 解绑输入事件。 */
|
|
offAllEvent() { this.input.unbindEvents(); }
|
|
|
|
// --- 载具 ---
|
|
/** 加载车辆模型。 */
|
|
loadVehicleModel(opts: VehicleOptions) { return this.vehicle.load(opts); }
|
|
/** 将当前驾驶车辆翻正复位。 */
|
|
resetVehicle() { this.vehicle.resetUpright(); }
|
|
|
|
// --- 销毁 ---
|
|
/** 销毁控制器并释放资源。 */
|
|
destroy() {
|
|
this.input.unbindEvents();
|
|
|
|
// 卸载插件
|
|
for (const plugin of this.plugins.slice()) {
|
|
this.unuse(plugin);
|
|
plugin.dispose?.();
|
|
}
|
|
this.plugins = [];
|
|
|
|
this.playerDebug.dispose();
|
|
|
|
// 清除玩家对象
|
|
if (this.playerCapsule) { this.playerCapsule.remove(this.camera); this.scene.remove(this.playerCapsule); }
|
|
(this.playerCapsule as any) = null;
|
|
if (this.playerModel) { this.scene.remove(this.playerModel); this.playerModel = null; }
|
|
|
|
// 清除碰撞体和相机
|
|
this.cam.resetControls();
|
|
this.clearColliders();
|
|
this.mobileControls?.destroy();
|
|
this.mobileControls = null;
|
|
this.bvhWorkerPool.dispose();
|
|
|
|
// 清除所有车辆
|
|
this.vehicle.destroy();
|
|
this.dynamics.clear();
|
|
this.collisionWorld.clear();
|
|
}
|
|
}
|