three-player-controller 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/playerController.ts
2
- import * as THREE from "three";
2
+ import * as THREE3 from "three";
3
3
  import { acceleratedRaycast, MeshBVH, MeshBVHHelper } from "three-mesh-bvh";
4
4
  import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js";
5
5
  import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
@@ -15,17 +15,287 @@ var jump_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAA
15
15
  // assets/imgs/view.png
16
16
  var view_default = "data:image/png;base64,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";
17
17
 
18
+ // src/utils/pathPlanner.ts
19
+ import * as THREE from "three";
20
+ var PathNode = class {
21
+ constructor(position) {
22
+ this.g = Infinity;
23
+ // 实际代价
24
+ this.h = 0;
25
+ // 估计代价
26
+ this.f = Infinity;
27
+ // f = g + h
28
+ this.parent = null;
29
+ this.position = position.clone();
30
+ }
31
+ equals(other) {
32
+ return this.position.distanceTo(other.position) < 0.01;
33
+ }
34
+ };
35
+ var PriorityQueue = class {
36
+ constructor() {
37
+ this.elements = [];
38
+ }
39
+ enqueue(item, priority) {
40
+ this.elements.push({ priority, item });
41
+ this.elements.sort((a, b) => a.priority - b.priority);
42
+ }
43
+ dequeue() {
44
+ return this.elements.shift()?.item;
45
+ }
46
+ isEmpty() {
47
+ return this.elements.length === 0;
48
+ }
49
+ contains(item, compareFn) {
50
+ return this.elements.some((e) => compareFn(e.item, item));
51
+ }
52
+ update(item, newPriority, compareFn) {
53
+ const index = this.elements.findIndex((e) => compareFn(e.item, item));
54
+ if (index !== -1) {
55
+ this.elements[index].priority = newPriority;
56
+ this.elements.sort((a, b) => a.priority - b.priority);
57
+ }
58
+ }
59
+ };
60
+ var PathPlanner = class {
61
+ constructor(obstacleChecker, config = {}) {
62
+ this.debugLines = [];
63
+ this.debugPoints = [];
64
+ this.obstacleChecker = obstacleChecker;
65
+ this.config = {
66
+ debugEnabled: false,
67
+ scale: 1,
68
+ ...config
69
+ };
70
+ }
71
+ // 计算启发式距离
72
+ heuristic(a, b) {
73
+ return a.distanceTo(b);
74
+ }
75
+ // A*路径规划算法
76
+ findPath(start, goal) {
77
+ const startTime = performance.now();
78
+ if (!this.obstacleChecker.isBlocked(start, goal)) {
79
+ return [goal];
80
+ }
81
+ const navigationPoints = this.obstacleChecker.getNavigationNodes(start, goal);
82
+ const allNodes = [new PathNode(start), new PathNode(goal), ...navigationPoints.map((p) => new PathNode(p))];
83
+ if (allNodes.length < 2) {
84
+ console.warn("\u5BFC\u822A\u8282\u70B9\u4E0D\u8DB3\uFF0C\u8FD4\u56DE\u76F4\u7EBF\u8DEF\u5F84");
85
+ return [goal];
86
+ }
87
+ const startNode = allNodes[0];
88
+ const goalNode = allNodes[1];
89
+ startNode.g = 0;
90
+ startNode.h = this.heuristic(startNode.position, goalNode.position);
91
+ startNode.f = startNode.h;
92
+ const openList = new PriorityQueue();
93
+ const closedSet = /* @__PURE__ */ new Set();
94
+ openList.enqueue(startNode, startNode.f);
95
+ const nodeEquals = (a, b) => a.equals(b);
96
+ while (!openList.isEmpty()) {
97
+ const current = openList.dequeue();
98
+ if (!current) break;
99
+ if (current.equals(goalNode)) {
100
+ const path = this.reconstructPath(current);
101
+ const endTime = performance.now();
102
+ if (this.config.debugEnabled) {
103
+ this.visualizePath([start, ...path]);
104
+ }
105
+ return path;
106
+ }
107
+ closedSet.add(current);
108
+ for (const neighbor of allNodes) {
109
+ if (closedSet.has(neighbor)) continue;
110
+ if (this.obstacleChecker.isBlocked(current.position, neighbor.position)) {
111
+ continue;
112
+ }
113
+ const tentativeG = current.g + current.position.distanceTo(neighbor.position);
114
+ if (tentativeG < neighbor.g) {
115
+ neighbor.parent = current;
116
+ neighbor.g = tentativeG;
117
+ neighbor.h = this.heuristic(neighbor.position, goalNode.position);
118
+ neighbor.f = neighbor.g + neighbor.h;
119
+ if (openList.contains(neighbor, nodeEquals)) {
120
+ openList.update(neighbor, neighbor.f, nodeEquals);
121
+ } else {
122
+ openList.enqueue(neighbor, neighbor.f);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ console.warn("A*\u672A\u627E\u5230\u8DEF\u5F84\uFF0C\u4F7F\u7528\u76F4\u7EBF\u8DEF\u5F84");
128
+ return [goal];
129
+ }
130
+ /**
131
+ * 重建路径
132
+ */
133
+ reconstructPath(endNode) {
134
+ const path = [];
135
+ let current = endNode;
136
+ while (current !== null) {
137
+ path.unshift(current.position.clone());
138
+ current = current.parent;
139
+ }
140
+ if (path.length > 0) {
141
+ path.shift();
142
+ }
143
+ return this.smoothPath(path);
144
+ }
145
+ /**
146
+ * 路径平滑
147
+ */
148
+ smoothPath(path) {
149
+ if (path.length <= 2) return path;
150
+ const smoothed = [path[0]];
151
+ let current = 0;
152
+ while (current < path.length - 1) {
153
+ let farthest = current + 1;
154
+ for (let i = path.length - 1; i > current + 1; i--) {
155
+ if (!this.obstacleChecker.isBlocked(path[current], path[i])) {
156
+ farthest = i;
157
+ break;
158
+ }
159
+ }
160
+ smoothed.push(path[farthest]);
161
+ current = farthest;
162
+ }
163
+ return smoothed;
164
+ }
165
+ /**
166
+ * 可视化路径
167
+ */
168
+ visualizePath(path) {
169
+ if (!this.config.scene || !this.config.debugEnabled) return;
170
+ this.clearVisualization();
171
+ const scale = this.config.scale || 1;
172
+ if (path.length > 1) {
173
+ const points = path.map((p) => p.clone());
174
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
175
+ const material = new THREE.LineBasicMaterial({
176
+ color: 65280,
177
+ linewidth: 3
178
+ });
179
+ const line = new THREE.Line(geometry, material);
180
+ this.config.scene.add(line);
181
+ this.debugLines.push(line);
182
+ }
183
+ path.forEach((point, index) => {
184
+ const geometry = new THREE.SphereGeometry(20 * scale);
185
+ const material = new THREE.MeshBasicMaterial({
186
+ color: index === path.length - 1 ? 16711680 : 65280
187
+ });
188
+ const sphere = new THREE.Mesh(geometry, material);
189
+ sphere.position.copy(point);
190
+ this.config.scene.add(sphere);
191
+ this.debugPoints.push(sphere);
192
+ });
193
+ }
194
+ /**
195
+ * 清除路径可视化
196
+ */
197
+ clearVisualization() {
198
+ if (!this.config.scene) return;
199
+ this.debugLines.forEach((line) => {
200
+ this.config.scene.remove(line);
201
+ line.geometry.dispose();
202
+ line.material.dispose();
203
+ });
204
+ this.debugLines = [];
205
+ this.debugPoints.forEach((point) => {
206
+ this.config.scene.remove(point);
207
+ point.geometry.dispose();
208
+ point.material.dispose();
209
+ });
210
+ this.debugPoints = [];
211
+ }
212
+ /**
213
+ * 更新配置
214
+ */
215
+ updateConfig(config) {
216
+ this.config = { ...this.config, ...config };
217
+ }
218
+ /**
219
+ * 销毁
220
+ */
221
+ dispose() {
222
+ this.clearVisualization();
223
+ }
224
+ };
225
+
226
+ // src/utils/useVehicleController.ts
227
+ import * as THREE2 from "three";
228
+ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
229
+ if (!world || !chassisBody) return { vehicle: null, updateWheelVisuals: () => {
230
+ } };
231
+ const vehicle = world.createVehicleController(chassisBody);
232
+ const suspensionDirection = new THREE2.Vector3(0, -1, 0);
233
+ wheelsInfo.forEach((wheel, index) => {
234
+ vehicle.addWheel(wheel.position, suspensionDirection, wheel.axleCs, wheel.suspensionRestLength, wheel.radius);
235
+ vehicle.setWheelChassisConnectionPointCs(index, wheel.position);
236
+ vehicle.setWheelDirectionCs(index, suspensionDirection);
237
+ vehicle.setWheelAxleCs(index, wheel.axleCs);
238
+ vehicle.setWheelSuspensionRestLength(index, wheel.suspensionRestLength);
239
+ vehicle.setWheelRadius(index, wheel.radius);
240
+ vehicle.setWheelMaxSuspensionTravel(index, wheel.suspensionRestLength * 1);
241
+ vehicle.setWheelSuspensionStiffness(index, 250);
242
+ vehicle.setWheelSuspensionCompression(index, 6);
243
+ vehicle.setWheelSuspensionRelaxation(index, 6);
244
+ vehicle.setWheelMaxSuspensionForce(index, 1e4);
245
+ vehicle.setWheelBrake(index, 0);
246
+ vehicle.setWheelSteering(index, 0);
247
+ vehicle.setWheelEngineForce(index, 0);
248
+ vehicle.setWheelFrictionSlip(index, 20);
249
+ vehicle.setWheelSideFrictionStiffness(index, 2);
250
+ });
251
+ const up = new THREE2.Vector3(0, 1, 0);
252
+ const _wheelSteeringQuat = new THREE2.Quaternion();
253
+ const _wheelRotationQuat = new THREE2.Quaternion();
254
+ function updateWheelVisuals() {
255
+ for (const [index, wheelObj] of wheels.entries()) {
256
+ if (!wheelObj) continue;
257
+ try {
258
+ const wheelAxleCs = vehicle.wheelAxleCs(index) ?? new THREE2.Vector3(1, 0, 0);
259
+ const connection = vehicle.wheelChassisConnectionPointCs(index)?.y ?? 0;
260
+ const suspension = vehicle.wheelSuspensionLength(index) ?? 0;
261
+ const steering = vehicle.wheelSteering(index) ?? 0;
262
+ const rotationRad = vehicle.wheelRotation(index) ?? 0;
263
+ wheelObj.position.y = connection - suspension;
264
+ _wheelSteeringQuat.setFromAxisAngle(up, steering);
265
+ _wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad);
266
+ wheelObj.quaternion.copy(_wheelSteeringQuat).multiply(_wheelRotationQuat);
267
+ } catch (e) {
268
+ }
269
+ }
270
+ }
271
+ function destroy() {
272
+ try {
273
+ world.removeVehicleController(vehicle);
274
+ } catch {
275
+ }
276
+ }
277
+ return {
278
+ vehicle,
279
+ updateWheelVisuals,
280
+ destroy
281
+ };
282
+ }
283
+
18
284
  // src/playerController.ts
19
- THREE.Mesh.prototype.raycast = acceleratedRaycast;
285
+ THREE3.Mesh.prototype.raycast = acceleratedRaycast;
20
286
  var controllerInstance = null;
21
- var clock = new THREE.Clock();
287
+ var clock = new THREE3.Clock();
22
288
  var PlayerController = class {
23
289
  constructor() {
24
290
  // ==================== 基本配置与参数 ====================
25
291
  this.loader = new GLTFLoader();
292
+ this.controllerMode = 0;
293
+ // 0: 人物 1: 车辆
294
+ this.isChangeControllerTransitionTimer = null;
26
295
  // ==================== 玩家基本属性 ====================
27
296
  this.playerRadius = 45;
28
297
  this.playerHeight = 180;
298
+ // 玩家参考身高
29
299
  this.isFirstPerson = false;
30
300
  this.boundingBoxMinY = 0;
31
301
  // ==================== 测试参数 ====================
@@ -36,7 +306,59 @@ var PlayerController = class {
36
306
  this.collider = null;
37
307
  this.visualizer = null;
38
308
  this.person = null;
39
- this.vehicle = null;
309
+ this.personHead = null;
310
+ this.collected = [];
311
+ this.dynamicCollider = null;
312
+ this.dynamicCollected = [];
313
+ // ==================== 多车辆相关 ====================
314
+ this.vehicles = [];
315
+ // 所有已加载车辆
316
+ this.activeVehicle = null;
317
+ // 当前驾驶/交互的车辆
318
+ this.vehicleLength = 6;
319
+ // 车辆参考长度
320
+ this.wheelSteeringQuat = new THREE3.Quaternion();
321
+ this.wheelRotationQuat = new THREE3.Quaternion();
322
+ this.RAPIER = null;
323
+ this.world = null;
324
+ // 全局车辆共享参数
325
+ this.vehicleParams = {
326
+ debug: {
327
+ showPhysicsBox: false
328
+ },
329
+ chassis: {
330
+ linearDamping: 0.5,
331
+ angularDamping: 0.5
332
+ },
333
+ model: {
334
+ rotation: -Math.PI / 2
335
+ },
336
+ power: {
337
+ accelerateForce: 50,
338
+ // 推进
339
+ brakeForce: 200,
340
+ // 刹车
341
+ maxSpeed: 1e4
342
+ // 最大速度
343
+ },
344
+ steering: {
345
+ maxSteerAngle: Math.PI / 4,
346
+ steerSpeed: 0.5,
347
+ steerReturnSpeed: 1
348
+ }
349
+ };
350
+ this.camBehindDir = new THREE3.Vector3(0, 0, 1);
351
+ // ==================== 上车相关 ====================
352
+ this.isMovingToBoardingPoint = false;
353
+ this.boardingWaypoints = [];
354
+ this.currentWaypointIndex = 0;
355
+ this.boardingTargetDir = null;
356
+ this.boardingMoveSpeed = 300;
357
+ this.boardingRotateSpeed = 10;
358
+ this.flip180Quat = new THREE3.Quaternion().setFromAxisAngle(new THREE3.Vector3(0, 1, 0), Math.PI);
359
+ this.enterVehicleTimer = null;
360
+ this.closeVehicleDoorTimer = null;
361
+ this.boardingPointWorld = null;
40
362
  // ==================== 状态开关 ====================
41
363
  this.playerIsOnGround = false;
42
364
  this.isupdate = true;
@@ -64,46 +386,63 @@ var PlayerController = class {
64
386
  this.lastTouchY = 0;
65
387
  // ==================== 第三人称相机参数 ====================
66
388
  this._camCollisionLerp = 0.18;
67
- // 平滑系数
68
389
  this._camEpsilon = 0.35;
69
- // 摄像机与障碍物之间的安全距离
70
390
  this._minCamDistance = 1;
71
- // 摄像机最小距离
72
391
  this._maxCamDistance = 4.4;
73
- // 摄像机最大距离
74
392
  this.orginMaxCamDistance = 4.4;
75
393
  // ==================== 物理/运动 ====================
76
- this.playerVelocity = new THREE.Vector3();
77
- // 玩家速度向量
78
- this.upVector = new THREE.Vector3(0, 1, 0);
394
+ this.playerVelocity = new THREE3.Vector3();
395
+ this.upVector = new THREE3.Vector3(0, 1, 0);
79
396
  // ==================== 临时复用向量/矩阵 ====================
80
- this.tempVector = new THREE.Vector3();
81
- this.tempVector2 = new THREE.Vector3();
82
- this.tempBox = new THREE.Box3();
83
- this.tempMat = new THREE.Matrix4();
84
- this.tempSegment = new THREE.Line3();
397
+ this.tempVector = new THREE3.Vector3();
398
+ this.tempVector2 = new THREE3.Vector3();
399
+ this.tempBox = new THREE3.Box3();
400
+ this.tempMat = new THREE3.Matrix4();
401
+ this.tempSegment = new THREE3.Line3();
85
402
  this.recheckAnimTimer = null;
86
403
  // ==================== 相机朝向/移动复用向量 ====================
87
- this.camDir = new THREE.Vector3();
88
- this.moveDir = new THREE.Vector3();
89
- this.targetQuat = new THREE.Quaternion();
90
- this.targetMat = new THREE.Matrix4();
404
+ this.camDir = new THREE3.Vector3();
405
+ this.moveDir = new THREE3.Vector3();
406
+ this.targetQuat = new THREE3.Quaternion();
407
+ this.targetMat = new THREE3.Matrix4();
91
408
  this.rotationSpeed = 10;
92
- this.DIR_FWD = new THREE.Vector3(0, 0, -1);
93
- this.DIR_BKD = new THREE.Vector3(0, 0, 1);
94
- this.DIR_LFT = new THREE.Vector3(-1, 0, 0);
95
- this.DIR_RGT = new THREE.Vector3(1, 0, 0);
96
- this.DIR_UP = new THREE.Vector3(0, 1, 0);
409
+ this.DIR_FWD = new THREE3.Vector3(0, 0, -1);
410
+ this.DIR_BKD = new THREE3.Vector3(0, 0, 1);
411
+ this.DIR_LFT = new THREE3.Vector3(-1, 0, 0);
412
+ this.DIR_RGT = new THREE3.Vector3(1, 0, 0);
413
+ this.DIR_UP = new THREE3.Vector3(0, 1, 0);
97
414
  // ==================== 射线检测 ====================
98
- this._personToCam = new THREE.Vector3();
99
- this._originTmp = new THREE.Vector3();
100
- this._raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0));
101
- this._raycasterPersonToCam = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3());
102
- /**
103
- * 根据按键设置人物动画
104
- */
415
+ this._personToCam = new THREE3.Vector3();
416
+ this._originTmp = new THREE3.Vector3();
417
+ this._raycaster = new THREE3.Raycaster(new THREE3.Vector3(), new THREE3.Vector3(0, -1, 0));
418
+ this._raycasterPersonToCam = new THREE3.Raycaster(new THREE3.Vector3(), new THREE3.Vector3());
419
+ // ==================== 物理与碰撞检测 ====================
420
+ this.ensureAttributesMinimal = (geom) => {
421
+ if (!geom.attributes.position) return null;
422
+ if (!geom.attributes.normal) geom.computeVertexNormals();
423
+ if (!geom.attributes.uv) {
424
+ const count = geom.attributes.position.count;
425
+ const dummyUV = new Float32Array(count * 2);
426
+ geom.setAttribute("uv", new THREE3.BufferAttribute(dummyUV, 2));
427
+ }
428
+ return geom;
429
+ };
105
430
  this.setAnimationByPressed = () => {
106
431
  this._maxCamDistance = this.orginMaxCamDistance;
432
+ if (this.isMovingToBoardingPoint) {
433
+ this.isMovingToBoardingPoint = false;
434
+ this.boardingWaypoints = [];
435
+ this.currentWaypointIndex = 0;
436
+ this.boardingTargetDir = null;
437
+ }
438
+ if (this.enterVehicleTimer) {
439
+ clearTimeout(this.enterVehicleTimer);
440
+ this.enterVehicleTimer = null;
441
+ }
442
+ if (this.closeVehicleDoorTimer) {
443
+ clearTimeout(this.closeVehicleDoorTimer);
444
+ this.closeVehicleDoorTimer = null;
445
+ }
107
446
  if (this.isFlying) {
108
447
  if (!this.fwdPressed) {
109
448
  this.playPersonAnimationByName("flyidle");
@@ -148,31 +487,33 @@ var PlayerController = class {
148
487
  }, 200);
149
488
  };
150
489
  // ==================== 事件处理 ====================
151
- /**
152
- * 键盘按下事件
153
- */
154
490
  this._boundOnKeydown = async (e) => {
155
491
  if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) {
156
492
  e.preventDefault();
157
493
  }
158
494
  switch (e.code) {
159
495
  case "KeyW":
496
+ case "ArrowUp":
160
497
  this.fwdPressed = true;
161
498
  this.setAnimationByPressed();
162
499
  break;
163
500
  case "KeyS":
501
+ case "ArrowDown":
164
502
  this.bkdPressed = true;
165
503
  this.setAnimationByPressed();
166
504
  break;
167
505
  case "KeyD":
506
+ case "ArrowRight":
168
507
  this.rgtPressed = true;
169
508
  this.setAnimationByPressed();
170
509
  break;
171
510
  case "KeyA":
511
+ case "ArrowLeft":
172
512
  this.lftPressed = true;
173
513
  this.setAnimationByPressed();
174
514
  break;
175
515
  case "ShiftLeft":
516
+ case "ShiftRight":
176
517
  this.shiftPressed = true;
177
518
  this.setAnimationByPressed();
178
519
  this.controls.mouseButtons = { LEFT: 2, MIDDLE: 1, RIGHT: 0 };
@@ -193,6 +534,7 @@ var PlayerController = class {
193
534
  this.changeView();
194
535
  break;
195
536
  case "KeyF":
537
+ if (this.controllerMode == 1) return;
196
538
  this.isFlying = !this.isFlying;
197
539
  this.setAnimationByPressed();
198
540
  if (!this.isFlying && !this.playerIsOnGround) {
@@ -200,32 +542,39 @@ var PlayerController = class {
200
542
  }
201
543
  break;
202
544
  case "KeyE":
203
- this.setDrive();
545
+ if (this.isFlying) return;
546
+ if (this.controllerMode == 0) {
547
+ this.enterVehicle();
548
+ } else {
549
+ this.exitVehicle();
550
+ }
204
551
  break;
205
552
  }
206
553
  };
207
- /**
208
- * 键盘抬起事件
209
- */
210
554
  this._boundOnKeyup = (e) => {
211
555
  switch (e.code) {
212
556
  case "KeyW":
557
+ case "ArrowUp":
213
558
  this.fwdPressed = false;
214
559
  this.setAnimationByPressed();
215
560
  break;
216
561
  case "KeyS":
562
+ case "ArrowDown":
217
563
  this.bkdPressed = false;
218
564
  this.setAnimationByPressed();
219
565
  break;
220
566
  case "KeyD":
567
+ case "ArrowRight":
221
568
  this.rgtPressed = false;
222
569
  this.setAnimationByPressed();
223
570
  break;
224
571
  case "KeyA":
572
+ case "ArrowLeft":
225
573
  this.lftPressed = false;
226
574
  this.setAnimationByPressed();
227
575
  break;
228
576
  case "ShiftLeft":
577
+ case "ShiftRight":
229
578
  this.shiftPressed = false;
230
579
  this.setAnimationByPressed();
231
580
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
@@ -238,23 +587,14 @@ var PlayerController = class {
238
587
  break;
239
588
  }
240
589
  };
241
- /**
242
- * 鼠标移动事件
243
- */
244
590
  this._mouseMove = (e) => {
245
591
  if (document.pointerLockElement !== document.body) return;
246
592
  this.setToward(e.movementX, e.movementY, 1e-4);
247
593
  };
248
- /**
249
- * 鼠标点击事件
250
- */
251
- this._mouseClick = (e) => {
594
+ this._mouseClick = (_e) => {
252
595
  this.setPointerLock();
253
596
  };
254
597
  // ==================== 移动端控制 ====================
255
- /**
256
- * 指针按下事件
257
- */
258
598
  this.onPointerDown = (e) => {
259
599
  if (e.pointerType !== "touch") return;
260
600
  this.isLookDown = true;
@@ -264,9 +604,6 @@ var PlayerController = class {
264
604
  this.lookAreaEl?.setPointerCapture?.(e.pointerId);
265
605
  e.preventDefault();
266
606
  };
267
- /**
268
- * 指针移动事件
269
- */
270
607
  this.onPointerMove = (e) => {
271
608
  if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
272
609
  const dx = e.clientX - this.lastTouchX;
@@ -276,9 +613,6 @@ var PlayerController = class {
276
613
  this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
277
614
  e.preventDefault();
278
615
  };
279
- /**
280
- * 指针抬起事件
281
- */
282
616
  this.onPointerUp = (e) => {
283
617
  if (e.pointerId !== this.lookPointerId) return;
284
618
  this.isLookDown = false;
@@ -289,19 +623,15 @@ var PlayerController = class {
289
623
  this._raycasterPersonToCam.firstHitOnly = true;
290
624
  }
291
625
  // ==================== 初始化相关方法 ====================
292
- /**
293
- * 初始化控制器
294
- */
295
626
  async init(opts, callback) {
296
627
  this.scene = opts.scene;
297
628
  this.camera = opts.camera;
298
629
  this.camera.rotation.order = "YXZ";
299
630
  this.controls = opts.controls;
300
631
  this.playerModel = opts.playerModel;
301
- this.initPos = opts.initPos ?? new THREE.Vector3(0, 0, 0);
632
+ this.initPos = opts.initPos ?? new THREE3.Vector3(0, 0, 0);
302
633
  this.mouseSensity = opts.mouseSensity ?? 5;
303
634
  const s = this.playerModel.scale;
304
- this.visualizeDepth = 0 * s;
305
635
  this.gravity = (opts.playerModel.gravity ?? -2400) * s;
306
636
  this.jumpHeight = (opts.playerModel.jumpHeight ?? 800) * s;
307
637
  this.originPlayerSpeed = (opts.playerModel.speed ?? 400) * s;
@@ -320,46 +650,97 @@ var PlayerController = class {
320
650
  await this.initMobileControls();
321
651
  }
322
652
  await this.createBVH(opts.colliderMeshUrl);
323
- this.createPlayer();
324
653
  await this.loadPersonGLB();
325
- if (this.isFirstPerson && this.person) {
326
- this.person.add(this.camera);
327
- }
328
654
  this.onAllEvent();
329
655
  this.setCameraPos();
330
656
  this.setControls();
331
657
  if (callback) callback();
332
658
  }
333
- /**
334
- * 初始化加载器
335
- */
336
659
  async initLoader() {
337
660
  const dracoLoader = new DRACOLoader();
338
661
  dracoLoader.setDecoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/gltf/");
339
662
  dracoLoader.setDecoderConfig({ type: "js" });
340
663
  this.loader.setDRACOLoader(dracoLoader);
341
664
  }
665
+ async initRapier() {
666
+ if (this.RAPIER) return;
667
+ this.RAPIER = await import("./rapier.es-XQHNYU2P.mjs");
668
+ await this.RAPIER.init();
669
+ const gravity = new this.RAPIER.Vector3(0, -9.81, 0);
670
+ this.world = new this.RAPIER.World(gravity);
671
+ this.world.maxCcdSubsteps = 2;
672
+ const addGeometryAsTrimesh = (RAPIER, world, geom) => {
673
+ let geometry = geom.index ? geom.clone().toNonIndexed() : geom.clone();
674
+ const posAttr = geometry.attributes.position;
675
+ const vertexCount = posAttr.count;
676
+ if (vertexCount % 3 !== 0) {
677
+ console.warn("\u9876\u70B9\u6570\u4E0D\u662F3\u7684\u500D\u6570\uFF0C\u4E09\u89D2\u5F62\u53EF\u80FD\u4E0D\u5B8C\u6574");
678
+ }
679
+ const vertices = new Float32Array(vertexCount * 3);
680
+ const tmp = new THREE3.Vector3();
681
+ for (let i = 0; i < vertexCount; i++) {
682
+ tmp.fromBufferAttribute(posAttr, i);
683
+ vertices[i * 3 + 0] = tmp.x;
684
+ vertices[i * 3 + 1] = tmp.y;
685
+ vertices[i * 3 + 2] = tmp.z;
686
+ }
687
+ const indices = vertexCount > 65535 ? new Uint32Array(vertexCount) : new Uint16Array(vertexCount);
688
+ for (let i = 0; i < vertexCount; i++) indices[i] = i;
689
+ const bodyDesc = RAPIER.RigidBodyDesc.fixed();
690
+ const body = world.createRigidBody(bodyDesc);
691
+ const colliderDesc = RAPIER.ColliderDesc.trimesh(vertices, indices).setRestitution(0).setFriction(0.8);
692
+ world.createCollider(colliderDesc, body);
693
+ };
694
+ for (const g of this.collected) {
695
+ addGeometryAsTrimesh(this.RAPIER, this.world, g);
696
+ }
697
+ const groundDesc = this.RAPIER.RigidBodyDesc.fixed();
698
+ const groundBody = this.world.createRigidBody(groundDesc);
699
+ groundBody.userData = { outOfBounds: true };
700
+ }
342
701
  // ==================== 玩家模型相关方法 ====================
343
- /**
344
- * 加载玩家模型与动画
345
- */
346
702
  async loadPersonGLB() {
347
703
  try {
348
704
  const gltf = await this.loader.loadAsync(this.playerModel.url);
349
705
  this.person = gltf.scene;
350
- const sc = this.playerModel.scale;
351
- const h = this.playerHeight * sc;
352
- this.person.scale.set(sc, sc, sc);
706
+ const { size } = this.getBbox(this.person);
707
+ const ratio = this.playerHeight / size.y;
708
+ const power = Math.round(Math.log10(ratio));
709
+ const modelScale = Math.pow(10, power);
710
+ this.playerRadius = Number(Math.min(size.x, size.z).toFixed(0)) * modelScale;
711
+ this.playerHeight = Number(size.y.toFixed(0)) * modelScale;
712
+ const scale = this.playerModel.scale;
713
+ const material = new THREE3.MeshStandardMaterial({
714
+ color: new THREE3.Color(1, 0, 0),
715
+ shadowSide: THREE3.DoubleSide,
716
+ depthTest: false,
717
+ transparent: true,
718
+ opacity: this.displayPlayer ? 0.5 : 0,
719
+ wireframe: true,
720
+ depthWrite: false
721
+ });
722
+ const r = this.playerRadius * scale;
723
+ const h = this.playerHeight * scale;
724
+ this.player = new THREE3.Mesh(new RoundedBoxGeometry(r * 2, h, r * 2, 1, 75), material);
725
+ this.player.geometry.translate(0, -h * 0.25, 0);
726
+ this.player.capsuleInfo = {
727
+ radius: r,
728
+ segment: new THREE3.Line3(new THREE3.Vector3(), new THREE3.Vector3(0, -h * 0.5, 0))
729
+ };
730
+ this.player.name = "capsule";
731
+ this.scene.add(this.player);
732
+ this.reset();
733
+ this.player.rotateY(this.playerModel.rotateY ?? 0);
734
+ this.person.scale.multiplyScalar(modelScale * scale);
353
735
  this.person.position.set(0, -h * 0.75, 0);
354
736
  this.person.traverse((child) => {
355
- if (child.isMesh) {
356
- child.castShadow = true;
357
- child.receiveShadow = true;
737
+ if (child.name == this.playerModel?.headObjName) {
738
+ this.personHead = child;
358
739
  }
359
740
  });
360
741
  this.player.add(this.person);
361
742
  this.reset();
362
- this.personMixer = new THREE.AnimationMixer(this.person);
743
+ this.personMixer = new THREE3.AnimationMixer(this.person);
363
744
  const animations = gltf.animations ?? [];
364
745
  this.personActions = /* @__PURE__ */ new Map();
365
746
  const animationMappings = [
@@ -371,7 +752,9 @@ var PlayerController = class {
371
752
  [this.playerModel.jumpAnim, "jumping"],
372
753
  [this.playerModel.runAnim, "running"],
373
754
  [this.playerModel.flyIdleAnim || this.playerModel.idleAnim, "flyidle"],
374
- [this.playerModel.flyAnim || this.playerModel.idleAnim, "flying"]
755
+ [this.playerModel.flyAnim || this.playerModel.idleAnim, "flying"],
756
+ [this.playerModel.enterCarAnim || this.playerModel.idleAnim, "enterCar"],
757
+ [this.playerModel.exitCarAnim || this.playerModel.idleAnim, "exitCar"]
375
758
  ];
376
759
  const findClip = (name) => animations.find((a) => a.name === name);
377
760
  for (const [clipName, actionName] of animationMappings) {
@@ -379,11 +762,11 @@ var PlayerController = class {
379
762
  if (!clip) continue;
380
763
  const action = this.personMixer.clipAction(clip);
381
764
  if (actionName === "jumping") {
382
- action.setLoop(THREE.LoopOnce, 1);
765
+ action.setLoop(THREE3.LoopOnce, 1);
383
766
  action.clampWhenFinished = true;
384
767
  action.setEffectiveTimeScale(1.2);
385
768
  } else {
386
- action.setLoop(THREE.LoopRepeat, Infinity);
769
+ action.setLoop(THREE3.LoopRepeat, Infinity);
387
770
  action.clampWhenFinished = false;
388
771
  action.setEffectiveTimeScale(1);
389
772
  }
@@ -425,16 +808,20 @@ var PlayerController = class {
425
808
  console.error("\u52A0\u8F7D\u73A9\u5BB6\u6A21\u578B\u5931\u8D25:", error);
426
809
  }
427
810
  }
428
- /**
429
- * 平滑切换人物动画
430
- */
431
811
  playPersonAnimationByName(name, fade = 0.18) {
432
812
  if (!this.personActions || this.ctPressed) return;
433
813
  const next = this.personActions.get(name);
434
814
  if (!next || this.actionState === next) return;
815
+ const duration = next.getClip().duration;
435
816
  const prev = this.actionState;
436
817
  next.reset();
437
818
  next.setEffectiveWeight(1);
819
+ if (name == "enterCar" || name == "exitCar") {
820
+ const enterTime = this.activeVehicle?.enterVehicleTime ?? 1.5;
821
+ next.setEffectiveTimeScale(duration / enterTime);
822
+ next.setLoop(THREE3.LoopOnce, 1);
823
+ next.clampWhenFinished = true;
824
+ }
438
825
  next.play();
439
826
  if (prev && prev !== next) {
440
827
  prev.fadeOut(fade);
@@ -444,106 +831,438 @@ var PlayerController = class {
444
831
  }
445
832
  this.actionState = next;
446
833
  }
447
- /**
448
- * 创建玩家胶囊体
449
- */
450
- createPlayer() {
451
- const material = new THREE.MeshStandardMaterial({
452
- color: new THREE.Color(1, 0, 0),
453
- shadowSide: THREE.DoubleSide,
454
- depthTest: false,
455
- transparent: true,
456
- opacity: this.displayPlayer ? 0.5 : 0,
457
- wireframe: true,
458
- depthWrite: false
459
- });
460
- const r = this.playerRadius * this.playerModel.scale;
461
- const h = this.playerHeight * this.playerModel.scale;
462
- this.player = new THREE.Mesh(new RoundedBoxGeometry(r * 2, h, r * 2, 1, 75), material);
463
- this.player.geometry.translate(0, -h * 0.25, 0);
464
- this.player.capsuleInfo = {
465
- radius: r,
466
- segment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -h * 0.5, 0))
467
- };
468
- this.player.name = "capsule";
469
- this.scene.add(this.player);
470
- this.reset();
471
- this.player.rotateY(this.playerModel.rotateY ?? 0);
472
- }
473
834
  // ==================== 车辆模型相关 ====================
474
835
  /**
475
- * 加载车辆模型与动画
836
+ * 加载车辆模型
476
837
  */
477
- async loadVehicleModel(params) {
838
+ async loadVehicleModel(opts) {
478
839
  try {
479
- const { url, position, scale = 1 } = params;
480
- const gltf = await this.loader.loadAsync(url);
481
- this.vehicle = gltf.scene;
482
- this.vehicle.scale.set(scale, scale, scale);
483
- this.vehicle.position.copy(position);
484
- this.vehicle.traverse((child) => {
485
- if (child.isMesh) {
486
- child.castShadow = true;
487
- child.receiveShadow = true;
488
- }
489
- });
490
- this.scene.add(this.vehicle);
491
- const animations = gltf.animations ?? [];
492
- this.vehicleActions = /* @__PURE__ */ new Map();
493
- this.vehicleMixer = new THREE.AnimationMixer(this.vehicle);
494
- const animationMappings = [
495
- [params.animations?.openDoorAnim ?? "", "open_door"],
496
- [params.animations?.wheelsTurnAnim ?? "", "wheels_turn"],
497
- [params.animations?.turnLeftAnim ?? "", "turn_left"],
498
- [params.animations?.turnRightAnim ?? "", "turn_right"]
499
- ];
840
+ if (!this.playerModel.enterCarAnim) {
841
+ return console.warn("\u672A\u914D\u7F6E\u4E0A\u8F66\u52A8\u753B\uFF0C\u4E0D\u6267\u884C\u8F66\u8F86\u76F8\u5173\u903B\u8F91");
842
+ }
843
+ await this.initRapier();
844
+ if (!this.world) return;
845
+ const scale = opts.scale ?? 1;
846
+ const chassisRatio = opts.chassisRatio ?? 0.2;
847
+ const suspensionRestLengthRatio = opts.suspensionRestLengthRatio ?? 0.2;
848
+ this.vehicleParams.power.accelerateForce = 50 * scale;
849
+ this.vehicleParams.power.brakeForce = 200 * scale;
850
+ this.vehicleParams.power.maxSpeed = 1e4 * scale;
851
+ const vehicleModel = await this.loader.loadAsync(opts.url);
852
+ const { size: originalSize } = this.getBbox(vehicleModel.scene);
853
+ const ratio = this.vehicleLength / Math.max(originalSize.x, originalSize.y, originalSize.z);
854
+ const power = Math.round(Math.log10(ratio));
855
+ const modelScale = Math.pow(10, power);
856
+ const vehicleMixer = new THREE3.AnimationMixer(vehicleModel.scene);
857
+ const animations = vehicleModel.animations ?? [];
858
+ const vehicleActions = /* @__PURE__ */ new Map();
500
859
  const findClip = (name) => animations.find((a) => a.name === name);
501
- for (const [clipName, actionName] of animationMappings) {
502
- const clip = findClip(clipName);
503
- if (!clip) continue;
504
- const action = this.vehicleMixer.clipAction(clip);
505
- action.setLoop(THREE.LoopOnce, 1);
860
+ const openDoorClip = findClip(opts.animations?.openDoorAnim || "");
861
+ if (openDoorClip) {
862
+ const action = vehicleMixer.clipAction(openDoorClip);
863
+ action.setLoop(THREE3.LoopOnce, 1);
506
864
  action.clampWhenFinished = true;
507
- action.setEffectiveTimeScale(2);
865
+ action.setEffectiveTimeScale(openDoorClip.duration);
508
866
  action.enabled = true;
509
867
  action.setEffectiveWeight(0);
510
- this.vehicleActions.set(actionName, action);
868
+ vehicleActions.set("openDoor", action);
511
869
  }
512
- console.log("\u5F00\u95E8\u52A8\u753B", this.vehicleActions.get("open_door"));
870
+ const wheelObjects = [];
871
+ for (const wheelName of opts.wheelsNames) {
872
+ let found = false;
873
+ vehicleModel.scene.traverse((child) => {
874
+ if (child.name === wheelName && !found) {
875
+ wheelObjects.push(child);
876
+ found = true;
877
+ }
878
+ });
879
+ if (!found) console.warn(`\u672A\u627E\u5230\u8F6E\u5B50: ${wheelName}`);
880
+ }
881
+ const tempGroup = new THREE3.Group();
882
+ this.scene.add(tempGroup);
883
+ vehicleModel.scene.scale.multiplyScalar(modelScale * scale);
884
+ vehicleModel.scene.rotateY(this.vehicleParams.model.rotation);
885
+ const { size, bbox, center } = this.getBbox(vehicleModel.scene);
886
+ vehicleModel.scene.position.set(-center.x, -center.y, -center.z);
887
+ tempGroup.add(vehicleModel.scene);
888
+ tempGroup.updateMatrixWorld(true);
889
+ const wheelsInfo = [];
890
+ let wheelRadius = 0;
891
+ let wheelWidth = 0;
892
+ let suspensionRestLength = 0;
893
+ let chassisHeight = 0;
894
+ let wheelSizeInit = false;
895
+ for (let i = 0; i < wheelObjects.length; i++) {
896
+ const wheel = wheelObjects[i];
897
+ const worldPos = new THREE3.Vector3();
898
+ const worldQuat = new THREE3.Quaternion();
899
+ const worldScale = new THREE3.Vector3();
900
+ wheel.getWorldPosition(worldPos);
901
+ wheel.getWorldQuaternion(worldQuat);
902
+ wheel.getWorldScale(worldScale);
903
+ if (!wheelSizeInit) {
904
+ const { size: ws } = this.getBbox(wheel);
905
+ wheelRadius = Number((Math.max(ws.x, ws.y, ws.z) / 2).toFixed(2));
906
+ wheelWidth = Number(Math.min(ws.x, ws.y, ws.z).toFixed(2));
907
+ suspensionRestLength = Number((wheelRadius * 2 * suspensionRestLengthRatio).toFixed(2));
908
+ chassisHeight = Number((wheelRadius * 2 * chassisRatio).toFixed(2));
909
+ wheelSizeInit = true;
910
+ }
911
+ wheelsInfo.push({
912
+ axleCs: new THREE3.Vector3(0, 0, -1),
913
+ position: worldPos,
914
+ quaternion: worldQuat,
915
+ scale: worldScale,
916
+ radius: wheelRadius,
917
+ width: wheelWidth,
918
+ suspensionRestLength,
919
+ object: wheel
920
+ });
921
+ }
922
+ tempGroup.remove(vehicleModel.scene);
923
+ this.scene.remove(tempGroup);
924
+ const vehicleGroup = new THREE3.Group();
925
+ this.scene.add(vehicleGroup);
926
+ vehicleGroup.add(vehicleModel.scene);
927
+ vehicleGroup.updateMatrixWorld(true);
928
+ const wheelWrappers = [];
929
+ for (let i = 0; i < wheelsInfo.length; i++) {
930
+ const wheel = wheelsInfo[i];
931
+ const localPos = vehicleGroup.worldToLocal(wheel.position.clone());
932
+ const wheelWrapper = new THREE3.Group();
933
+ wheelWrapper.position.copy(localPos);
934
+ const wheelObj = wheelsInfo[i].object;
935
+ if (wheelObj.parent) wheelObj.parent.remove(wheelObj);
936
+ wheelObj.position.set(0, 0, 0);
937
+ wheelObj.quaternion.copy(wheel.quaternion);
938
+ wheelObj.scale.copy(wheel.scale);
939
+ wheelObj.updateMatrixWorld();
940
+ wheelWrapper.add(wheelObj);
941
+ vehicleGroup.add(wheelWrapper);
942
+ wheelWrappers.push(wheelWrapper);
943
+ }
944
+ const halfExtents = size.clone().multiplyScalar(0.5);
945
+ halfExtents.y -= chassisHeight / 2;
946
+ vehicleModel.scene.position.y -= chassisHeight / 2;
947
+ halfExtents.x *= 0.95;
948
+ halfExtents.z *= 0.95;
949
+ const chassisDesc = this.RAPIER.RigidBodyDesc.dynamic().setTranslation(opts.position.x, opts.position.y, opts.position.z).setLinearDamping(this.vehicleParams.chassis.linearDamping).setAngularDamping(this.vehicleParams.chassis.angularDamping).setCanSleep(false).setAdditionalMass(10);
950
+ const chassisBody = this.world.createRigidBody(chassisDesc);
951
+ const chassisCollider = this.RAPIER.ColliderDesc.cuboid(halfExtents.x, halfExtents.y, halfExtents.z);
952
+ this.world.createCollider(chassisCollider, chassisBody);
953
+ if (this.vehicleParams.debug.showPhysicsBox) {
954
+ const debugBox = new THREE3.Mesh(
955
+ new THREE3.BoxGeometry(halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2),
956
+ new THREE3.MeshBasicMaterial({
957
+ color: 16711680,
958
+ wireframe: true,
959
+ transparent: true,
960
+ opacity: 0.3
961
+ })
962
+ );
963
+ vehicleGroup.add(debugBox);
964
+ }
965
+ vehicleGroup.position.copy(opts.position);
966
+ vehicleGroup.updateMatrixWorld(true);
967
+ const { vehicle, updateWheelVisuals } = createVehicleController(this.world, chassisBody, wheelWrappers, wheelsInfo);
968
+ const vehicleInstance = {
969
+ vehicleGroup,
970
+ chassisBody,
971
+ vehicleController: vehicle,
972
+ updateWheelVisuals,
973
+ vehicleMixer,
974
+ vehicleActions,
975
+ vehiclIsOpenDoor: false,
976
+ vehicleBBox: bbox.clone(),
977
+ pathPlanner: new PathPlanner(this._createObstacleCheckerFor(vehicleGroup, bbox, scale), {
978
+ debugEnabled: false,
979
+ scene: this.scene,
980
+ scale: this.playerModel.scale
981
+ }),
982
+ scale,
983
+ boardingPoint: opts.boardingPoint,
984
+ seatOffset: opts.seatOffset ?? new THREE3.Vector3(0, 0, 0),
985
+ enterVehicleTime: 1.5,
986
+ chassisRatio,
987
+ suspensionRestLengthRatio,
988
+ size: {
989
+ l: Math.max(size.x, size.z),
990
+ w: Math.min(size.x, size.z),
991
+ h: size.y
992
+ }
993
+ };
994
+ this.vehicles.push(vehicleInstance);
995
+ console.log(`\u8F66\u8F86\u52A0\u8F7D\u5B8C\u6210\uFF0C\u5F53\u524D\u5171 ${this.vehicles.length} \u8F86\u8F66`, vehicleInstance);
996
+ this.setControllerTransition();
513
997
  } catch (error) {
514
998
  console.error("\u52A0\u8F7D\u8F66\u8F86\u6A21\u578B\u5931\u8D25:", error);
515
999
  }
516
1000
  }
517
- // ==================== 相机与视角控制 ====================
1001
+ getBbox(object) {
1002
+ const bbox = new THREE3.Box3().setFromObject(object);
1003
+ const center = new THREE3.Vector3();
1004
+ const size = new THREE3.Vector3();
1005
+ bbox.getCenter(center);
1006
+ bbox.getSize(size);
1007
+ return { bbox, center, size };
1008
+ }
1009
+ /**
1010
+ * 为指定车辆创建障碍物检测器
1011
+ */
1012
+ _createObstacleCheckerFor(vehicleGroup, bbox, scale) {
1013
+ return {
1014
+ isBlocked: (start, end) => {
1015
+ const vehiclePos = vehicleGroup.position;
1016
+ const vehicleQuat = vehicleGroup.quaternion;
1017
+ const center = new THREE3.Vector3();
1018
+ const size = new THREE3.Vector3();
1019
+ bbox.getCenter(center);
1020
+ bbox.getSize(size);
1021
+ center.applyQuaternion(vehicleQuat).add(vehiclePos);
1022
+ const halfSize = size.clone().multiplyScalar(0.5 * scale);
1023
+ const corners = [];
1024
+ for (let x = -1; x <= 1; x += 2) {
1025
+ for (let y = -1; y <= 1; y += 2) {
1026
+ for (let z = -1; z <= 1; z += 2) {
1027
+ const localCorner = new THREE3.Vector3(halfSize.x * x, halfSize.y * y, halfSize.z * z);
1028
+ const worldCorner = localCorner.applyQuaternion(vehicleQuat).add(center);
1029
+ corners.push(worldCorner);
1030
+ }
1031
+ }
1032
+ }
1033
+ const expandedBBox = new THREE3.Box3();
1034
+ corners.forEach((corner) => expandedBBox.expandByPoint(corner));
1035
+ expandedBBox.expandByScalar(100 * this.playerModel.scale);
1036
+ const direction = new THREE3.Vector3().subVectors(end, start);
1037
+ const length = direction.length();
1038
+ direction.normalize();
1039
+ const ray = new THREE3.Ray(start, direction);
1040
+ const intersection = new THREE3.Vector3();
1041
+ const intersects = ray.intersectBox(expandedBBox, intersection);
1042
+ return intersects !== null && start.distanceTo(intersection) < length;
1043
+ },
1044
+ getNavigationNodes: (start, _goal) => {
1045
+ const nodes = [];
1046
+ const vehiclePos = vehicleGroup.position;
1047
+ const vehicleQuat = vehicleGroup.quaternion;
1048
+ const vehicleForward = new THREE3.Vector3(0, 0, 1).applyQuaternion(vehicleQuat);
1049
+ const vehicleRight = new THREE3.Vector3(1, 0, 0).applyQuaternion(vehicleQuat);
1050
+ const bboxSize = new THREE3.Vector3();
1051
+ bbox.getSize(bboxSize);
1052
+ const halfLength = bboxSize.z / 2 * scale;
1053
+ const halfWidth = bboxSize.x / 2 * scale;
1054
+ const bypassMargin = 300 * this.playerModel.scale;
1055
+ const extendedMargin = 500 * this.playerModel.scale;
1056
+ const groundY = start.y;
1057
+ for (const margin of [bypassMargin, extendedMargin]) {
1058
+ nodes.push(
1059
+ vehiclePos.clone().add(vehicleForward.clone().multiplyScalar(halfLength + margin)).add(vehicleRight.clone().multiplyScalar(-halfWidth - margin)).setY(groundY)
1060
+ );
1061
+ nodes.push(
1062
+ vehiclePos.clone().add(vehicleForward.clone().multiplyScalar(halfLength + margin)).add(vehicleRight.clone().multiplyScalar(halfWidth + margin)).setY(groundY)
1063
+ );
1064
+ nodes.push(
1065
+ vehiclePos.clone().add(vehicleForward.clone().multiplyScalar(-halfLength - margin)).add(vehicleRight.clone().multiplyScalar(-halfWidth - margin)).setY(groundY)
1066
+ );
1067
+ nodes.push(
1068
+ vehiclePos.clone().add(vehicleForward.clone().multiplyScalar(-halfLength - margin)).add(vehicleRight.clone().multiplyScalar(halfWidth + margin)).setY(groundY)
1069
+ );
1070
+ }
1071
+ return nodes;
1072
+ }
1073
+ };
1074
+ }
1075
+ /**
1076
+ * 开关车门动画(操作当前 activeVehicle)
1077
+ */
1078
+ openVehicleDoor(isOpen = true) {
1079
+ const v = this.activeVehicle;
1080
+ if (!v?.vehicleActions) return;
1081
+ const next = v.vehicleActions.get("openDoor");
1082
+ if (!next) return;
1083
+ const duration = next.getClip().duration;
1084
+ next.reset();
1085
+ next.setEffectiveWeight(1);
1086
+ if (isOpen) {
1087
+ next.setEffectiveTimeScale(duration * 2);
1088
+ next.time = 0;
1089
+ v.vehiclIsOpenDoor = true;
1090
+ } else {
1091
+ next.setEffectiveTimeScale(-duration * 2);
1092
+ next.time = duration;
1093
+ v.vehiclIsOpenDoor = false;
1094
+ }
1095
+ next.setLoop(THREE3.LoopOnce, 1);
1096
+ next.clampWhenFinished = true;
1097
+ next.play();
1098
+ }
1099
+ /**
1100
+ * 上车:自动寻找最近的车辆
1101
+ */
1102
+ enterVehicle() {
1103
+ if (this.vehicles.length === 0) return;
1104
+ let nearestVehicle = null;
1105
+ let nearestDist = Infinity;
1106
+ let nearBoardingPointWorld = null;
1107
+ for (const v2 of this.vehicles) {
1108
+ const boardingPointLocal = v2.boardingPoint.clone().multiplyScalar(v2.scale);
1109
+ const boardingPointWorld = new THREE3.Vector3();
1110
+ v2.vehicleGroup.localToWorld(boardingPointWorld.copy(boardingPointLocal));
1111
+ const dist = this.player.position.distanceTo(boardingPointWorld);
1112
+ if (dist < 800 * this.playerModel.scale && dist < nearestDist) {
1113
+ nearestDist = dist;
1114
+ nearestVehicle = v2;
1115
+ nearBoardingPointWorld = boardingPointWorld;
1116
+ }
1117
+ }
1118
+ if (!nearestVehicle || !nearBoardingPointWorld) return;
1119
+ this.activeVehicle = nearestVehicle;
1120
+ const v = nearestVehicle;
1121
+ this.boardingPointWorld = nearBoardingPointWorld;
1122
+ const vehicleForward = new THREE3.Vector3(0, 0, 1).applyQuaternion(v.vehicleGroup.quaternion).normalize();
1123
+ const path = v.pathPlanner.findPath(this.player.position.clone(), this.boardingPointWorld);
1124
+ this.boardingWaypoints = path;
1125
+ this.currentWaypointIndex = 0;
1126
+ this.boardingTargetDir = vehicleForward;
1127
+ this.isMovingToBoardingPoint = true;
1128
+ this.playPersonAnimationByName("walking");
1129
+ }
1130
+ /**
1131
+ * 走向上车点
1132
+ */
1133
+ updateMoveToBoardingPoint(delta) {
1134
+ if (!this.isMovingToBoardingPoint || !this.boardingTargetDir || this.boardingWaypoints.length === 0) {
1135
+ return;
1136
+ }
1137
+ if (this.currentWaypointIndex >= this.boardingWaypoints.length) {
1138
+ this.finalizeBoarding(delta);
1139
+ return;
1140
+ }
1141
+ const currentWaypoint = this.boardingWaypoints[this.currentWaypointIndex];
1142
+ const currentPos = this.player.position.clone();
1143
+ const horizontalDistance = new THREE3.Vector2(currentWaypoint.x - currentPos.x, currentWaypoint.z - currentPos.z).length();
1144
+ const isLastWaypoint = this.currentWaypointIndex === this.boardingWaypoints.length - 1;
1145
+ const waypointThreshold = isLastWaypoint ? 0 : 10 * this.playerModel.scale;
1146
+ if (horizontalDistance > waypointThreshold) {
1147
+ const moveDir = new THREE3.Vector3(currentWaypoint.x - currentPos.x, 0, currentWaypoint.z - currentPos.z).normalize();
1148
+ const moveDistance = Math.min(this.boardingMoveSpeed * this.playerModel.scale * delta, horizontalDistance);
1149
+ this.player.position.add(moveDir.multiplyScalar(moveDistance));
1150
+ const lookTarget = this.player.position.clone().add(moveDir);
1151
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1152
+ this.targetQuat.setFromRotationMatrix(this.targetMat);
1153
+ this.targetQuat.multiply(this.flip180Quat);
1154
+ const rotateAlpha = Math.min(1, this.boardingRotateSpeed * delta);
1155
+ this.player.quaternion.slerp(this.targetQuat, rotateAlpha);
1156
+ } else {
1157
+ this.currentWaypointIndex++;
1158
+ }
1159
+ }
518
1160
  /**
519
- * 第一/三人称视角切换
1161
+ * 完成上车
520
1162
  */
1163
+ finalizeBoarding(delta) {
1164
+ const v = this.activeVehicle;
1165
+ if (!this.boardingTargetDir || !v) return;
1166
+ const currentDir = new THREE3.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion).normalize();
1167
+ const targetDir = this.boardingTargetDir.clone().normalize();
1168
+ const angleDiff = currentDir.angleTo(targetDir);
1169
+ if (angleDiff > 0.01) {
1170
+ const lookTarget = this.player.position.clone().add(targetDir);
1171
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1172
+ this.targetQuat.setFromRotationMatrix(this.targetMat);
1173
+ const rotateAlpha = Math.min(1, this.boardingRotateSpeed * delta);
1174
+ this.player.quaternion.slerp(this.targetQuat, rotateAlpha);
1175
+ } else {
1176
+ this.boardingWaypoints = [];
1177
+ this.currentWaypointIndex = 0;
1178
+ this.boardingTargetDir = null;
1179
+ v.pathPlanner?.clearVisualization();
1180
+ this.playPersonAnimationByName("enterCar");
1181
+ if (!v.vehiclIsOpenDoor) this.openVehicleDoor();
1182
+ this.player.rotation.copy(v.vehicleGroup.rotation);
1183
+ this.player.quaternion.multiply(this.flip180Quat);
1184
+ this.closeVehicleDoorTimer = setTimeout(
1185
+ () => {
1186
+ this.openVehicleDoor(false);
1187
+ },
1188
+ v.enterVehicleTime * 1e3 - 500
1189
+ );
1190
+ this.enterVehicleTimer = setTimeout(() => {
1191
+ if (this.boardingPointWorld) {
1192
+ const offsetY = this.boardingPointWorld.y - this.player.position.y;
1193
+ this.controllerMode = 1;
1194
+ v.vehicleGroup.attach(this.player);
1195
+ this.player.position.add(
1196
+ v.seatOffset.clone().multiplyScalar(v.scale).add(new THREE3.Vector3(0, offsetY, 0))
1197
+ );
1198
+ this.isMovingToBoardingPoint = false;
1199
+ if (this.isChangeControllerTransitionTimer) {
1200
+ clearTimeout(this.isChangeControllerTransitionTimer);
1201
+ this.isChangeControllerTransitionTimer = null;
1202
+ }
1203
+ }
1204
+ }, v.enterVehicleTime * 1e3);
1205
+ }
1206
+ }
1207
+ /**
1208
+ * 下车
1209
+ */
1210
+ exitVehicle() {
1211
+ const v = this.activeVehicle;
1212
+ if (!v) return;
1213
+ this.isMovingToBoardingPoint = false;
1214
+ this.boardingWaypoints = [];
1215
+ this.currentWaypointIndex = 0;
1216
+ this.boardingTargetDir = null;
1217
+ const vel = v.chassisBody.linvel();
1218
+ const velSpeed = new THREE3.Vector3(vel.x, vel.y, vel.z).length();
1219
+ if (velSpeed > 0.1) {
1220
+ this.playPersonAnimationByName("idle");
1221
+ } else {
1222
+ this.playPersonAnimationByName("exitCar");
1223
+ }
1224
+ this.openVehicleDoor(true);
1225
+ this.closeVehicleDoorTimer = setTimeout(
1226
+ () => {
1227
+ this.openVehicleDoor(false);
1228
+ },
1229
+ v.enterVehicleTime * 1e3 - 500
1230
+ );
1231
+ this.controllerMode = 0;
1232
+ this.scene.attach(this.player);
1233
+ if (this.isFirstPerson) {
1234
+ this.setFirstPersonCamera();
1235
+ }
1236
+ this.setControllerTransition();
1237
+ }
1238
+ // ==================== 相机与视角控制 ====================
521
1239
  changeView() {
522
1240
  this.isFirstPerson = !this.isFirstPerson;
523
1241
  if (this.isFirstPerson) {
524
- this.player.attach(this.camera);
525
- this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
526
- this.camera.rotation.set(0, Math.PI, 0);
527
- this.controls.enableZoom = false;
1242
+ this.setFirstPersonCamera();
528
1243
  } else {
529
1244
  this.scene.attach(this.camera);
530
1245
  const worldPos = this.player.position.clone();
531
- const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1246
+ const dir = new THREE3.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
532
1247
  const angle = Math.atan2(dir.z, dir.x);
533
- const offset = new THREE.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, 200 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
1248
+ const offset = new THREE3.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, 200 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
534
1249
  this.camera.position.copy(worldPos).add(offset);
535
1250
  this.controls.target.copy(worldPos);
536
1251
  this.controls.enableZoom = this.enableZoom;
537
1252
  }
538
1253
  this.setPointerLock();
539
1254
  }
540
- setDrive() {
541
- this.controllerMode = 2;
542
- this.person?.attach(this.vehicle);
1255
+ setFirstPersonCamera() {
1256
+ if (this.personHead) {
1257
+ this.personHead?.attach(this.camera);
1258
+ this.camera.position.set(0, 10, 20);
1259
+ } else {
1260
+ this.player.attach(this.camera);
1261
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1262
+ }
1263
+ this.camera.rotation.set(0, Math.PI, 0);
1264
+ this.controls.enableZoom = false;
543
1265
  }
544
- /**
545
- * 设置指针锁定
546
- */
547
1266
  setPointerLock() {
548
1267
  if ((this.thirdMouseMode === 0 || this.thirdMouseMode === 1) && !this.isFirstPerson || this.isFirstPerson) {
549
1268
  document.body.requestPointerLock();
@@ -551,33 +1270,28 @@ var PlayerController = class {
551
1270
  document.exitPointerLock();
552
1271
  }
553
1272
  }
554
- /**
555
- * 设置摄像机初始位置
556
- */
557
1273
  setCameraPos() {
558
- if (this.isFirstPerson) {
559
- this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
560
- } else {
561
- const worldPos = this.player.position.clone();
562
- const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
563
- const angle = Math.atan2(dir.z, dir.x);
564
- const offset = new THREE.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, -100 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
565
- this.camera.position.copy(worldPos).add(offset);
566
- }
567
- this.camera.updateProjectionMatrix();
1274
+ setTimeout(() => {
1275
+ if (this.isFirstPerson) {
1276
+ this.person.add(this.camera);
1277
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1278
+ } else {
1279
+ const worldPos = this.player.position.clone();
1280
+ const dir = new THREE3.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1281
+ const angle = Math.atan2(dir.z, dir.x);
1282
+ const offset = new THREE3.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, -100 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
1283
+ this.camera.position.copy(worldPos).add(offset);
1284
+ this.controls.enableZoom = this.enableZoom;
1285
+ }
1286
+ this.camera.updateProjectionMatrix();
1287
+ }, 0);
568
1288
  }
569
- /**
570
- * 设置控制器
571
- */
572
1289
  setControls() {
573
1290
  this.controls.enableZoom = this.enableZoom;
574
1291
  this.controls.rotateSpeed = this.mouseSensity * 0.05;
575
1292
  this.controls.maxPolarAngle = Math.PI * (300 / 360);
576
1293
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
577
1294
  }
578
- /**
579
- * 重置控制器
580
- */
581
1295
  resetControls() {
582
1296
  if (!this.controls) return;
583
1297
  this.controls.enabled = true;
@@ -587,38 +1301,60 @@ var PlayerController = class {
587
1301
  this.controls.enableZoom = true;
588
1302
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
589
1303
  }
590
- /**
591
- * 设置朝向
592
- */
593
1304
  setToward(dx, dy, speed) {
594
- if (this.isFirstPerson) {
595
- const yaw = -dx * speed * this.mouseSensity;
596
- const pitch = -dy * speed * this.mouseSensity;
597
- this.player.rotateY(yaw);
598
- this.camera.rotation.x = THREE.MathUtils.clamp(this.camera.rotation.x + pitch, -1.1, 1.4);
1305
+ if (this.controllerMode == 0) {
1306
+ if (this.isFirstPerson) {
1307
+ if (this.isMovingToBoardingPoint) return;
1308
+ const yaw = -dx * speed * this.mouseSensity;
1309
+ const pitch = -dy * speed * this.mouseSensity;
1310
+ this.player.rotateY(yaw);
1311
+ this.camera.rotation.x = THREE3.MathUtils.clamp(this.camera.rotation.x + pitch, -1.1, 1.4);
1312
+ } else {
1313
+ const sensitivity = this.mouseSensity;
1314
+ const deltaX = -dx * speed * sensitivity;
1315
+ const deltaY = -dy * speed * sensitivity;
1316
+ const target = this.player.position.clone();
1317
+ const distance = this.camera.position.distanceTo(target);
1318
+ const currentPosition = this.camera.position.clone().sub(target);
1319
+ let theta = Math.atan2(currentPosition.x, currentPosition.z);
1320
+ let phi = Math.acos(currentPosition.y / distance);
1321
+ theta += deltaX;
1322
+ phi += deltaY;
1323
+ phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1324
+ const newX = distance * Math.sin(phi) * Math.sin(theta);
1325
+ const newY = distance * Math.cos(phi);
1326
+ const newZ = distance * Math.sin(phi) * Math.cos(theta);
1327
+ this.camera.position.set(target.x + newX, target.y + newY, target.z + newZ);
1328
+ this.camera.lookAt(target);
1329
+ }
599
1330
  } else {
600
- const sensitivity = this.mouseSensity;
601
- const deltaX = -dx * speed * sensitivity;
602
- const deltaY = -dy * speed * sensitivity;
603
- const target = this.player.position.clone();
604
- const distance = this.camera.position.distanceTo(target);
605
- const currentPosition = this.camera.position.clone().sub(target);
606
- let theta = Math.atan2(currentPosition.x, currentPosition.z);
607
- let phi = Math.acos(currentPosition.y / distance);
608
- theta += deltaX;
609
- phi += deltaY;
610
- phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
611
- const newX = distance * Math.sin(phi) * Math.sin(theta);
612
- const newY = distance * Math.cos(phi);
613
- const newZ = distance * Math.sin(phi) * Math.cos(theta);
614
- this.camera.position.set(target.x + newX, target.y + newY, target.z + newZ);
615
- this.camera.lookAt(target);
1331
+ const v = this.activeVehicle;
1332
+ if (!v) return;
1333
+ if (this.isFirstPerson) {
1334
+ const yaw = -dx * speed * this.mouseSensity;
1335
+ const pitch = -dy * speed * this.mouseSensity;
1336
+ this.camera.rotation.y = THREE3.MathUtils.clamp(this.camera.rotation.y + yaw, Math.PI * (3 / 4), Math.PI * (5 / 4));
1337
+ this.camera.rotation.x = THREE3.MathUtils.clamp(this.camera.rotation.x + pitch, 0, Math.PI * (1 / 3));
1338
+ } else {
1339
+ const sensitivity = this.mouseSensity;
1340
+ const deltaX = -dx * speed * sensitivity;
1341
+ const deltaY = -dy * speed * sensitivity;
1342
+ const target = v.vehicleGroup.position.clone();
1343
+ const distance = this.camera.position.distanceTo(target);
1344
+ const currentPosition = this.camera.position.clone().sub(target);
1345
+ let theta = Math.atan2(currentPosition.x, currentPosition.z);
1346
+ let phi = Math.acos(currentPosition.y / distance);
1347
+ theta += deltaX;
1348
+ phi += deltaY;
1349
+ phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1350
+ const newX = distance * Math.sin(phi) * Math.sin(theta);
1351
+ const newY = distance * Math.cos(phi);
1352
+ const newZ = distance * Math.sin(phi) * Math.cos(theta);
1353
+ this.camera.position.set(target.x + newX, target.y + newY, target.z + newZ);
1354
+ this.camera.lookAt(target);
1355
+ }
616
1356
  }
617
1357
  }
618
- // ==================== 物理与碰撞检测 ====================
619
- /**
620
- * 统一属性集合
621
- */
622
1358
  unifiedAttribute(collected) {
623
1359
  const attrMap = /* @__PURE__ */ new Map();
624
1360
  const attrConflict = /* @__PURE__ */ new Set();
@@ -665,28 +1401,14 @@ var PlayerController = class {
665
1401
  const meta = attrMap.get(name);
666
1402
  const len = count * meta.itemSize;
667
1403
  const array = new meta.arrayCtor(len);
668
- g.setAttribute(name, new THREE.BufferAttribute(array, meta.itemSize, meta.normalized));
1404
+ g.setAttribute(name, new THREE3.BufferAttribute(array, meta.itemSize, meta.normalized));
669
1405
  }
670
1406
  }
671
1407
  }
672
1408
  return collected;
673
1409
  }
674
- /**
675
- * BVH碰撞体构建
676
- */
677
1410
  async createBVH(meshUrl = "") {
678
1411
  await this.initLoader();
679
- const ensureAttributesMinimal = (geom) => {
680
- if (!geom.attributes.position) return null;
681
- if (!geom.attributes.normal) geom.computeVertexNormals();
682
- if (!geom.attributes.uv) {
683
- const count = geom.attributes.position.count;
684
- const dummyUV = new Float32Array(count * 2);
685
- geom.setAttribute("uv", new THREE.BufferAttribute(dummyUV, 2));
686
- }
687
- return geom;
688
- };
689
- let collected = [];
690
1412
  if (meshUrl === "") {
691
1413
  if (this.collider) {
692
1414
  this.scene.remove(this.collider);
@@ -699,15 +1421,15 @@ var PlayerController = class {
699
1421
  let geom = mesh.geometry.clone();
700
1422
  geom.applyMatrix4(mesh.matrixWorld);
701
1423
  if (geom.index) geom = geom.toNonIndexed();
702
- const safe = ensureAttributesMinimal(geom);
703
- if (safe) collected.push(safe);
1424
+ const safe = this.ensureAttributesMinimal(geom);
1425
+ if (safe) this.collected.push(safe);
704
1426
  } catch (e) {
705
1427
  console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
706
1428
  }
707
1429
  }
708
1430
  });
709
- if (!collected.length) return;
710
- collected = this.unifiedAttribute(collected);
1431
+ if (!this.collected.length) return;
1432
+ this.collected = this.unifiedAttribute(this.collected);
711
1433
  } else {
712
1434
  const gltf = await this.loader.loadAsync(meshUrl);
713
1435
  const mesh = gltf.scene.children[0];
@@ -715,34 +1437,73 @@ var PlayerController = class {
715
1437
  let geom = mesh.geometry.clone();
716
1438
  geom.applyMatrix4(mesh.matrixWorld);
717
1439
  if (geom.index) geom = geom.toNonIndexed();
718
- const safe = ensureAttributesMinimal(geom);
719
- if (safe) collected.push(safe);
1440
+ const safe = this.ensureAttributesMinimal(geom);
1441
+ if (safe) this.collected.push(safe);
720
1442
  }
721
- const merged = BufferGeometryUtils.mergeGeometries(collected, false);
1443
+ const merged = BufferGeometryUtils.mergeGeometries(this.collected, false);
722
1444
  if (!merged) {
723
1445
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
724
1446
  return;
725
1447
  }
726
1448
  merged.boundsTree = new MeshBVH(merged, { maxDepth: 100 });
727
- this.collider = new THREE.Mesh(
1449
+ this.collider = new THREE3.Mesh(
728
1450
  merged,
729
- new THREE.MeshBasicMaterial({
1451
+ new THREE3.MeshBasicMaterial({
730
1452
  opacity: 0.5,
731
1453
  transparent: true,
732
- wireframe: true
1454
+ wireframe: true,
1455
+ depthTest: true
733
1456
  })
734
1457
  );
735
1458
  if (this.displayCollider) this.scene.add(this.collider);
736
1459
  if (this.displayVisualizer) {
737
1460
  if (this.visualizer) this.scene.remove(this.visualizer);
738
- this.visualizer = new MeshBVHHelper(this.collider, this.visualizeDepth);
1461
+ this.visualizer = new MeshBVHHelper(this.collider, 0);
739
1462
  this.scene.add(this.visualizer);
740
1463
  }
741
1464
  this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
742
1465
  }
743
- /**
744
- * 获取法线与Y轴的夹角
745
- */
1466
+ createDynamicBVH(objects = []) {
1467
+ if (this.dynamicCollider) {
1468
+ this.scene.remove(this.dynamicCollider);
1469
+ this.dynamicCollider = null;
1470
+ }
1471
+ this.dynamicCollected = [];
1472
+ objects.forEach((object) => {
1473
+ object.traverse((c) => {
1474
+ const mesh = c;
1475
+ if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
1476
+ try {
1477
+ let geom = mesh.geometry.clone();
1478
+ geom.applyMatrix4(mesh.matrixWorld);
1479
+ if (geom.index) geom = geom.toNonIndexed();
1480
+ const safe = this.ensureAttributesMinimal(geom);
1481
+ if (safe) this.dynamicCollected.push(safe);
1482
+ } catch (e) {
1483
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1484
+ }
1485
+ }
1486
+ });
1487
+ });
1488
+ if (!this.dynamicCollected.length) return;
1489
+ this.dynamicCollected = this.unifiedAttribute(this.dynamicCollected);
1490
+ const merged = BufferGeometryUtils.mergeGeometries(this.dynamicCollected, false);
1491
+ if (!merged) {
1492
+ console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
1493
+ return;
1494
+ }
1495
+ merged.boundsTree = new MeshBVH(merged);
1496
+ this.dynamicCollider = new THREE3.Mesh(
1497
+ merged,
1498
+ new THREE3.MeshBasicMaterial({
1499
+ opacity: 0.5,
1500
+ transparent: true,
1501
+ wireframe: true,
1502
+ depthTest: true
1503
+ })
1504
+ );
1505
+ if (this.displayCollider) this.scene.add(this.dynamicCollider);
1506
+ }
746
1507
  getAngleWithYAxis(normal) {
747
1508
  const yAxis = { x: 0, y: 1, z: 0 };
748
1509
  const dotProduct = normal.x * yAxis.x + normal.y * yAxis.y + normal.z * yAxis.z;
@@ -750,13 +1511,193 @@ var PlayerController = class {
750
1511
  const cosTheta = dotProduct / normalMagnitude;
751
1512
  return Math.acos(cosTheta);
752
1513
  }
1514
+ // ==================== 设置控制器过渡 ====================
1515
+ setControllerTransition() {
1516
+ if (this.isChangeControllerTransitionTimer) {
1517
+ clearTimeout(this.isChangeControllerTransitionTimer);
1518
+ this.isChangeControllerTransitionTimer = null;
1519
+ }
1520
+ this.isChangeControllerTransitionTimer = setTimeout(() => {
1521
+ this.isChangeControllerTransitionTimer = null;
1522
+ let vGroups = [];
1523
+ for (const v of this.vehicles) {
1524
+ this.clearVehicleVelocity(v);
1525
+ vGroups.push(v.vehicleGroup);
1526
+ }
1527
+ this.createDynamicBVH(vGroups);
1528
+ }, 3e3);
1529
+ }
1530
+ // 清除车辆速度
1531
+ clearVehicleVelocity(v) {
1532
+ if (!v || !this.world || !this.RAPIER) return;
1533
+ const { chassisBody, vehicleController } = v;
1534
+ const ZERO = new this.RAPIER.Vector3(0, 0, 0);
1535
+ chassisBody.setLinvel(ZERO, true);
1536
+ chassisBody.setAngvel(ZERO, true);
1537
+ const BIG_BRAKE = 1e6;
1538
+ for (let i = 0; i < 4; i++) {
1539
+ vehicleController.setWheelEngineForce(i, 0);
1540
+ vehicleController.setWheelBrake(i, BIG_BRAKE);
1541
+ }
1542
+ vehicleController.updateVehicle(1 / 60);
1543
+ this.world.timestep = 1 / 60;
1544
+ this.world.step();
1545
+ chassisBody.setLinvel(ZERO, true);
1546
+ chassisBody.setAngvel(ZERO, true);
1547
+ for (let i = 0; i < 4; i++) {
1548
+ vehicleController.setWheelBrake(i, 0);
1549
+ }
1550
+ }
753
1551
  // ==================== 循环更新 ====================
754
- /**
755
- * 每帧更新
756
- */
757
1552
  async update(delta = clock.getDelta()) {
758
1553
  if (!this.isupdate || !this.player || !this.collider) return;
759
1554
  delta = Math.min(delta, 1 / 30);
1555
+ if (this.controllerMode == 1) {
1556
+ this.updateVehicle(delta);
1557
+ } else {
1558
+ this.updatePlayer(delta);
1559
+ if (this.isChangeControllerTransitionTimer) this.updateVehicleInertia(delta);
1560
+ }
1561
+ }
1562
+ /**
1563
+ * 更新当前驾驶的车辆
1564
+ */
1565
+ updateVehicle(delta) {
1566
+ const v = this.activeVehicle;
1567
+ if (!v || !this.world) return;
1568
+ const { vehicleController, chassisBody, vehicleGroup } = v;
1569
+ const rotation = chassisBody.rotation();
1570
+ const quat = new THREE3.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
1571
+ const forward = new THREE3.Vector3(1, 0, 0).applyQuaternion(quat);
1572
+ const slopeAngle = Math.asin(forward.y);
1573
+ let factor = 1;
1574
+ if (slopeAngle < -0.05 && this.fwdPressed) {
1575
+ factor = -Math.sin(slopeAngle) * 10;
1576
+ }
1577
+ const engineForce = (Number(this.fwdPressed) * this.vehicleParams.power.accelerateForce - Number(this.bkdPressed) * this.vehicleParams.power.accelerateForce) * factor;
1578
+ vehicleController.setWheelEngineForce(0, engineForce);
1579
+ vehicleController.setWheelEngineForce(1, engineForce);
1580
+ vehicleController.setWheelEngineForce(2, engineForce);
1581
+ vehicleController.setWheelEngineForce(3, engineForce);
1582
+ const wheelBrake = Number(this.spacePressed) * this.vehicleParams.power.brakeForce * delta;
1583
+ vehicleController.setWheelBrake(0, wheelBrake);
1584
+ vehicleController.setWheelBrake(1, wheelBrake);
1585
+ vehicleController.setWheelBrake(2, wheelBrake);
1586
+ vehicleController.setWheelBrake(3, wheelBrake);
1587
+ const currentSteering = vehicleController.wheelSteering(0) || 0;
1588
+ const steerDirection = Number(this.lftPressed) - Number(this.rgtPressed);
1589
+ let steerSpeed;
1590
+ if (steerDirection === 0) {
1591
+ steerSpeed = this.vehicleParams.steering.steerReturnSpeed || 0.15;
1592
+ } else {
1593
+ steerSpeed = this.vehicleParams.steering.steerSpeed || 0.08;
1594
+ }
1595
+ const steerLerpFactor = 1 - Math.pow(1 - steerSpeed, delta);
1596
+ const targetSteering = this.vehicleParams.steering.maxSteerAngle * steerDirection;
1597
+ const steering = THREE3.MathUtils.lerp(currentSteering, targetSteering, steerLerpFactor);
1598
+ vehicleController.setWheelSteering(0, steering);
1599
+ vehicleController.setWheelSteering(1, steering);
1600
+ if ((this.rgtPressed || this.lftPressed) && this.shiftPressed) {
1601
+ vehicleController.setWheelSideFrictionStiffness(2, 0.5);
1602
+ vehicleController.setWheelSideFrictionStiffness(3, 0.5);
1603
+ } else {
1604
+ vehicleController.setWheelSideFrictionStiffness(2, 2);
1605
+ vehicleController.setWheelSideFrictionStiffness(3, 2);
1606
+ }
1607
+ this.updateVehicleInertia(delta);
1608
+ if (!this.isFirstPerson) {
1609
+ const lookTarget = vehicleGroup.position.clone();
1610
+ this.camera.position.sub(this.controls.target);
1611
+ this.controls.target.copy(lookTarget);
1612
+ this.camera.position.add(lookTarget);
1613
+ this.controls.update();
1614
+ const velocity = chassisBody.linvel();
1615
+ const currentSpeed = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z);
1616
+ const maxSpeed = this.vehicleParams.power.maxSpeed * v.scale;
1617
+ const speedRatio = Math.min(currentSpeed / maxSpeed, 1);
1618
+ const baseCamDistance = v.size.l * 0.8;
1619
+ const maxCamDistanceLimit = v.size.l * 5;
1620
+ const targetDistance = THREE3.MathUtils.lerp(baseCamDistance, maxCamDistanceLimit, speedRatio);
1621
+ this._personToCam.subVectors(this.camera.position, vehicleGroup.position);
1622
+ const origin = vehicleGroup.position.clone().add(new THREE3.Vector3(0, 0, 0));
1623
+ const direction = this._personToCam.clone().normalize();
1624
+ const desiredDist = targetDistance;
1625
+ this._raycasterPersonToCam.set(origin, direction);
1626
+ this._raycasterPersonToCam.far = desiredDist;
1627
+ const intersects = this._raycasterPersonToCam.intersectObject(this.collider, false);
1628
+ if (intersects.length > 0) {
1629
+ const hit = intersects[0];
1630
+ const safeDist = Math.max(hit.distance - this._camEpsilon, this._minCamDistance);
1631
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1632
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1633
+ } else {
1634
+ this._raycasterPersonToCam.far = maxCamDistanceLimit;
1635
+ const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(this.collider, false);
1636
+ let safeDist = desiredDist;
1637
+ if (intersectsMaxDis.length) {
1638
+ const hitMax = intersectsMaxDis[0];
1639
+ safeDist = Math.min(desiredDist, hitMax.distance - this._camEpsilon);
1640
+ }
1641
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1642
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1643
+ }
1644
+ if (this.fwdPressed || this.bkdPressed) {
1645
+ const vel = chassisBody.linvel();
1646
+ const velHorizontal = new THREE3.Vector3(vel.x, vel.y, vel.z);
1647
+ const velSpeed = velHorizontal.length();
1648
+ if (velSpeed > 0.3) {
1649
+ const targetBehindDir = velHorizontal.clone().normalize().negate();
1650
+ this.camBehindDir.lerp(targetBehindDir, this._camCollisionLerp).normalize();
1651
+ const camHeightOffset = v.size.h;
1652
+ const targetCamPos = lookTarget.clone().add(this.camBehindDir.clone().multiplyScalar(desiredDist)).add(new THREE3.Vector3(0, camHeightOffset, 0));
1653
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1654
+ this.controls.update();
1655
+ }
1656
+ }
1657
+ }
1658
+ const vehicleUp = this.upVector.clone().applyQuaternion(vehicleGroup.quaternion);
1659
+ const angleWithUp = vehicleUp.angleTo(this.upVector);
1660
+ if (angleWithUp > Math.PI / 2) {
1661
+ const size = new THREE3.Vector3();
1662
+ v.vehicleBBox?.getSize(size);
1663
+ const translation2 = chassisBody.translation();
1664
+ chassisBody.setTranslation(new this.RAPIER.Vector3(translation2.x, translation2.y + size.y, translation2.z), true);
1665
+ chassisBody.setRotation(new this.RAPIER.Quaternion(0, 0, 0, 1), true);
1666
+ chassisBody.setLinvel(new this.RAPIER.Vector3(0, 0, 0), true);
1667
+ chassisBody.setAngvel(new this.RAPIER.Vector3(0, 0, 0), true);
1668
+ }
1669
+ }
1670
+ /**
1671
+ * 更新所有车辆物理和位置
1672
+ */
1673
+ updateVehicleInertia(delta) {
1674
+ if (!this.world) return;
1675
+ this.world.timestep = delta;
1676
+ this.world.step();
1677
+ for (const v of this.vehicles) {
1678
+ const { vehicleController, chassisBody, vehicleGroup, updateWheelVisuals } = v;
1679
+ vehicleController.updateVehicle(delta);
1680
+ const velocity = chassisBody.linvel();
1681
+ const currentSpeed = Math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z);
1682
+ const maxSpeed = this.vehicleParams.power.maxSpeed * v.scale;
1683
+ if (currentSpeed > maxSpeed) {
1684
+ const s = maxSpeed / currentSpeed;
1685
+ chassisBody.setLinvel(new this.RAPIER.Vector3(velocity.x * s, velocity.y * s, velocity.z * s), true);
1686
+ }
1687
+ const translation = chassisBody.translation();
1688
+ const rotationSync = chassisBody.rotation();
1689
+ vehicleGroup.position.set(translation.x, translation.y, translation.z);
1690
+ vehicleGroup.quaternion.set(rotationSync.x, rotationSync.y, rotationSync.z, rotationSync.w);
1691
+ if (updateWheelVisuals) updateWheelVisuals();
1692
+ }
1693
+ }
1694
+ /**
1695
+ * 更新人物
1696
+ */
1697
+ updatePlayer(delta) {
1698
+ if (this.isMovingToBoardingPoint) {
1699
+ this.updateMoveToBoardingPoint(delta);
1700
+ }
760
1701
  if (!this.isFlying) {
761
1702
  this.player.position.addScaledVector(this.playerVelocity, delta);
762
1703
  }
@@ -787,32 +1728,24 @@ var PlayerController = class {
787
1728
  const intersects = this._raycaster.intersectObject(this.collider, false);
788
1729
  if (intersects.length > 0) {
789
1730
  playerDistanceFromGround = this.player.position.y - intersects[0].point.y;
790
- const normal = intersects[0].normal;
791
- const angle2 = this.getAngleWithYAxis(normal) * 180 / Math.PI;
792
1731
  const maxH = this.playerHeight * this.playerModel.scale * 0.9;
793
1732
  const h = this.playerHeight * this.playerModel.scale * 0.75;
794
1733
  const minH = this.playerHeight * this.playerModel.scale * 0.7;
795
1734
  if (!this.isFlying) {
796
- if (playerDistanceFromGround > maxH) {
1735
+ if (playerDistanceFromGround >= maxH) {
797
1736
  this.playerVelocity.y += delta * this.gravity;
798
1737
  this.player.position.addScaledVector(this.playerVelocity, delta);
799
1738
  this.playerIsOnGround = false;
800
- } else if (playerDistanceFromGround > h && playerDistanceFromGround < maxH) {
801
- if (angle2 >= 0 && angle2 < 5) {
802
- this.playerVelocity.y += delta * this.gravity;
803
- this.player.position.addScaledVector(this.playerVelocity, delta);
1739
+ } else if (playerDistanceFromGround >= h && playerDistanceFromGround < maxH) {
1740
+ if (!this.spacePressed) {
1741
+ this.playerVelocity.set(0, 0, 0);
804
1742
  this.playerIsOnGround = true;
805
- } else {
806
- if (this.spacePressed) {
807
- this.playerVelocity.y += delta * this.gravity;
808
- } else {
809
- this.playerVelocity.set(0, 0, 0);
810
- this.playerIsOnGround = true;
811
- }
1743
+ this.player.position.y = intersects[0].point.y + h;
812
1744
  }
813
- } else if (playerDistanceFromGround > minH && playerDistanceFromGround < h) {
1745
+ } else if (playerDistanceFromGround >= minH && playerDistanceFromGround < h) {
814
1746
  this.playerVelocity.set(0, 0, 0);
815
1747
  this.playerIsOnGround = true;
1748
+ this.player.position.y = intersects[0].point.y + h;
816
1749
  } else if (playerDistanceFromGround < minH) {
817
1750
  this.playerVelocity.set(0, 0, 0);
818
1751
  this.player.position.set(this.player.position.x, intersects[0].point.y + h, this.player.position.z);
@@ -830,23 +1763,38 @@ var PlayerController = class {
830
1763
  this.tempBox.expandByPoint(this.tempSegment.start);
831
1764
  this.tempBox.expandByPoint(this.tempSegment.end);
832
1765
  this.tempBox.expandByScalar(capsuleInfo.radius);
833
- const bvh = this.collider?.geometry;
834
- bvh?.boundsTree?.shapecast({
835
- // 检测包围盒碰撞
836
- intersectsBounds: (box) => box.intersectsBox(this.tempBox),
837
- // 检测三角形碰撞
838
- intersectsTriangle: (tri) => {
839
- const triPoint = this.tempVector;
840
- const capsulePoint = this.tempVector2;
841
- const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
842
- if (distance < capsuleInfo.radius) {
843
- const depth = capsuleInfo.radius - distance;
844
- const direction = capsulePoint.sub(triPoint).normalize();
845
- this.tempSegment.start.addScaledVector(direction, depth);
846
- this.tempSegment.end.addScaledVector(direction, depth);
1766
+ if (!this.isMovingToBoardingPoint) {
1767
+ this.collider?.geometry?.boundsTree?.shapecast({
1768
+ intersectsBounds: (box) => box.intersectsBox(this.tempBox),
1769
+ intersectsTriangle: (tri) => {
1770
+ const triPoint = this.tempVector;
1771
+ const capsulePoint = this.tempVector2;
1772
+ const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
1773
+ if (distance < capsuleInfo.radius) {
1774
+ const normal = tri.getNormal(new THREE3.Vector3());
1775
+ if (normal.y > 0.5 && !this.isFlying) return;
1776
+ const depth = capsuleInfo.radius - distance;
1777
+ const direction = capsulePoint.sub(triPoint).normalize();
1778
+ this.tempSegment.start.addScaledVector(direction, depth);
1779
+ this.tempSegment.end.addScaledVector(direction, depth);
1780
+ }
847
1781
  }
848
- }
849
- });
1782
+ });
1783
+ this.dynamicCollider?.geometry?.boundsTree?.shapecast({
1784
+ intersectsBounds: (box) => box.intersectsBox(this.tempBox),
1785
+ intersectsTriangle: (tri) => {
1786
+ const triPoint = this.tempVector;
1787
+ const capsulePoint = this.tempVector2;
1788
+ const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
1789
+ if (distance < capsuleInfo.radius) {
1790
+ const depth = capsuleInfo.radius - distance;
1791
+ const direction = capsulePoint.sub(triPoint).normalize();
1792
+ this.tempSegment.start.addScaledVector(direction, depth);
1793
+ this.tempSegment.end.addScaledVector(direction, depth);
1794
+ }
1795
+ }
1796
+ });
1797
+ }
850
1798
  const newPosition = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
851
1799
  const deltaVector = this.tempVector2.subVectors(newPosition, this.player.position);
852
1800
  const offset = Math.max(0, deltaVector.length() - 1e-5);
@@ -879,34 +1827,36 @@ var PlayerController = class {
879
1827
  }
880
1828
  }
881
1829
  if (this.isFlying) {
882
- this.camDir.y = 0;
883
- this.camDir.normalize();
884
- this.camDir.negate();
885
- this.moveDir.normalize();
886
- this.moveDir.negate();
887
- const lookTarget = this.player.position.clone().add(this.fwdPressed ? this.moveDir : this.camDir);
888
- this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
889
- this.targetQuat.setFromRotationMatrix(this.targetMat);
890
- const alpha = Math.min(1, this.rotationSpeed * delta);
891
- this.player.quaternion.slerp(this.targetQuat, alpha);
1830
+ if (!this.isFirstPerson) {
1831
+ this.camDir.y = 0;
1832
+ this.camDir.normalize();
1833
+ this.camDir.negate();
1834
+ this.moveDir.normalize();
1835
+ this.moveDir.negate();
1836
+ const lookTarget = this.player.position.clone().add(this.fwdPressed ? this.moveDir : this.camDir);
1837
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1838
+ this.targetQuat.setFromRotationMatrix(this.targetMat);
1839
+ const alpha = Math.min(1, this.rotationSpeed * delta);
1840
+ this.player.quaternion.slerp(this.targetQuat, alpha);
1841
+ }
892
1842
  }
893
1843
  if (!this.isFirstPerson) {
894
1844
  const lookTarget = this.player.position.clone();
895
- lookTarget.y += 30 * this.playerModel.scale;
1845
+ lookTarget.y += this.playerHeight / 6 * this.playerModel.scale;
896
1846
  this.camera.position.sub(this.controls.target);
897
1847
  this.controls.target.copy(lookTarget);
898
1848
  this.camera.position.add(lookTarget);
899
1849
  this.controls.update();
900
1850
  if (!this.enableZoom) {
901
1851
  this._personToCam.subVectors(this.camera.position, this.player.position);
902
- const origin = this.player.position.clone().add(new THREE.Vector3(0, 0, 0));
1852
+ const origin = this.player.position.clone();
903
1853
  const direction = this._personToCam.clone().normalize();
904
1854
  const desiredDist = this._personToCam.length();
905
1855
  this._raycasterPersonToCam.set(origin, direction);
906
1856
  this._raycasterPersonToCam.far = desiredDist;
907
- const intersects2 = this._raycasterPersonToCam.intersectObject(this.collider, false);
908
- if (intersects2.length > 0) {
909
- const hit = intersects2[0];
1857
+ const intersectsCamera = this._raycasterPersonToCam.intersectObject(this.collider, false);
1858
+ if (intersectsCamera.length > 0) {
1859
+ const hit = intersectsCamera[0];
910
1860
  const safeDist = Math.max(hit.distance - this._camEpsilon, this._minCamDistance);
911
1861
  const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
912
1862
  this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
@@ -926,13 +1876,13 @@ var PlayerController = class {
926
1876
  if (this.player.position.y < this.boundingBoxMinY - 1) {
927
1877
  this._originTmp.set(this.player.position.x, 1e4, this.player.position.z);
928
1878
  this._raycaster.ray.origin.copy(this._originTmp);
929
- const intersects2 = this._raycaster.intersectObject(this.collider, false);
930
- if (intersects2.length > 0) {
1879
+ const intersectsFall = this._raycaster.intersectObject(this.collider, false);
1880
+ if (intersectsFall.length > 0) {
931
1881
  console.log("\u73A9\u5BB6\u4E3Abug\u610F\u5916\u6389\u843D");
932
- this.reset(new THREE.Vector3(this.player.position.x, intersects2[0].point.y + 5, this.player.position.z));
1882
+ this.reset(new THREE3.Vector3(this.player.position.x, intersectsFall[0].point.y + 5, this.player.position.z));
933
1883
  } else {
934
1884
  console.log("\u73A9\u5BB6\u6B63\u5E38\u6389\u843D");
935
- this.reset(new THREE.Vector3(this.player.position.x, this.player.position.y + 15, this.player.position.z));
1885
+ this.reset(new THREE3.Vector3(this.player.position.x, this.player.position.y + 15, this.player.position.z));
936
1886
  }
937
1887
  }
938
1888
  }
@@ -941,26 +1891,19 @@ var PlayerController = class {
941
1891
  */
942
1892
  updateMixers(delta) {
943
1893
  if (this.personMixer) this.personMixer.update(delta);
944
- if (this.vehicleMixer) this.vehicleMixer.update(delta);
1894
+ for (const v of this.vehicles) {
1895
+ v.vehicleMixer?.update(delta);
1896
+ }
945
1897
  }
946
- /**
947
- * 重置玩家位置
948
- */
949
1898
  reset(position) {
950
1899
  if (!this.player) return;
951
1900
  this.playerVelocity.set(0, 0, 0);
952
1901
  this.player.position.copy(position ?? this.initPos);
953
1902
  }
954
- /**
955
- * 获取玩家位置
956
- */
957
1903
  getPosition() {
958
1904
  return this.player.position;
959
1905
  }
960
1906
  // ==================== 输入处理 ====================
961
- /**
962
- * 设置输入
963
- */
964
1907
  setInput(input) {
965
1908
  if (typeof input.moveX === "number") {
966
1909
  this.lftPressed = input.moveX === -1;
@@ -1000,9 +1943,6 @@ var PlayerController = class {
1000
1943
  }
1001
1944
  }
1002
1945
  }
1003
- /**
1004
- * 事件绑定
1005
- */
1006
1946
  onAllEvent() {
1007
1947
  this.isupdate = true;
1008
1948
  this.setPointerLock();
@@ -1011,9 +1951,6 @@ var PlayerController = class {
1011
1951
  window.addEventListener("mousemove", this._mouseMove);
1012
1952
  window.addEventListener("click", this._mouseClick);
1013
1953
  }
1014
- /**
1015
- * 事件解绑
1016
- */
1017
1954
  offAllEvent() {
1018
1955
  this.isupdate = false;
1019
1956
  document.exitPointerLock();
@@ -1022,9 +1959,6 @@ var PlayerController = class {
1022
1959
  window.removeEventListener("mousemove", this._mouseMove);
1023
1960
  window.removeEventListener("click", this._mouseClick);
1024
1961
  }
1025
- /**
1026
- * 初始化移动端摇杆控制
1027
- */
1028
1962
  async initMobileControls() {
1029
1963
  this.controls.maxPolarAngle = Math.PI * (300 / 360);
1030
1964
  this.controls.touches = { ONE: null, TWO: null };
@@ -1075,9 +2009,7 @@ var PlayerController = class {
1075
2009
  const sprintThreshold = JOY_SIZE / 2;
1076
2010
  const isSprinting = distance >= sprintThreshold;
1077
2011
  const prev = this.prevJoyState || { dirX: 0, dirY: 0, shift: false };
1078
- if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift) {
1079
- return;
1080
- }
2012
+ if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift) return;
1081
2013
  this.prevJoyState = { dirX, dirY, shift: isSprinting };
1082
2014
  this.setInput({ moveX: dirX, moveY: dirY, shift: isSprinting });
1083
2015
  });
@@ -1189,9 +2121,6 @@ var PlayerController = class {
1189
2121
  { passive: false }
1190
2122
  );
1191
2123
  }
1192
- /**
1193
- * 销毁移动端摇杆控制
1194
- */
1195
2124
  destroyMobileControls() {
1196
2125
  try {
1197
2126
  if (this.joystickManager && this.joystickManager.destroy) {
@@ -1227,9 +2156,6 @@ var PlayerController = class {
1227
2156
  }
1228
2157
  }
1229
2158
  // ==================== 销毁 ====================
1230
- /**
1231
- * 销毁人物控制器
1232
- */
1233
2159
  destroy() {
1234
2160
  this.offAllEvent();
1235
2161
  if (this.player) {
@@ -1251,6 +2177,12 @@ var PlayerController = class {
1251
2177
  this.collider = null;
1252
2178
  }
1253
2179
  this.destroyMobileControls();
2180
+ for (const v of this.vehicles) {
2181
+ this.scene.remove(v.vehicleGroup);
2182
+ v.pathPlanner?.dispose();
2183
+ }
2184
+ this.vehicles = [];
2185
+ this.activeVehicle = null;
1254
2186
  controllerInstance = null;
1255
2187
  }
1256
2188
  };
@@ -1259,13 +2191,16 @@ function playerController() {
1259
2191
  const c = controllerInstance;
1260
2192
  return {
1261
2193
  init: (opts, callback) => c.init(opts, callback),
2194
+ loadVehicleModel: (params) => c.loadVehicleModel(params),
1262
2195
  changeView: () => c.changeView(),
1263
2196
  reset: (pos) => c.reset(pos),
1264
2197
  update: (dt) => c.update(dt),
1265
2198
  destroy: () => c.destroy(),
1266
2199
  setInput: (i) => c.setInput(i),
1267
2200
  getposition: () => c.getPosition(),
1268
- loadVehicleModel: (params) => c.loadVehicleModel(params)
2201
+ getPerson: () => c.person,
2202
+ getActiveVehicle: () => c.activeVehicle,
2203
+ getAllVehicles: () => c.vehicles
1269
2204
  };
1270
2205
  }
1271
2206
  function onAllEvent() {