three-player-controller 0.3.2 → 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,18 +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();
26
- // 0: 普通 1: 飞行 2: 车辆
292
+ this.controllerMode = 0;
293
+ // 0: 人物 1: 车辆
294
+ this.isChangeControllerTransitionTimer = null;
27
295
  // ==================== 玩家基本属性 ====================
28
296
  this.playerRadius = 45;
29
297
  this.playerHeight = 180;
298
+ // 玩家参考身高
30
299
  this.isFirstPerson = false;
31
300
  this.boundingBoxMinY = 0;
32
301
  // ==================== 测试参数 ====================
@@ -37,7 +306,59 @@ var PlayerController = class {
37
306
  this.collider = null;
38
307
  this.visualizer = null;
39
308
  this.person = null;
40
- 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;
41
362
  // ==================== 状态开关 ====================
42
363
  this.playerIsOnGround = false;
43
364
  this.isupdate = true;
@@ -65,46 +386,63 @@ var PlayerController = class {
65
386
  this.lastTouchY = 0;
66
387
  // ==================== 第三人称相机参数 ====================
67
388
  this._camCollisionLerp = 0.18;
68
- // 平滑系数
69
389
  this._camEpsilon = 0.35;
70
- // 摄像机与障碍物之间的安全距离
71
390
  this._minCamDistance = 1;
72
- // 摄像机最小距离
73
391
  this._maxCamDistance = 4.4;
74
- // 摄像机最大距离
75
392
  this.orginMaxCamDistance = 4.4;
76
393
  // ==================== 物理/运动 ====================
77
- this.playerVelocity = new THREE.Vector3();
78
- // 玩家速度向量
79
- this.upVector = new THREE.Vector3(0, 1, 0);
394
+ this.playerVelocity = new THREE3.Vector3();
395
+ this.upVector = new THREE3.Vector3(0, 1, 0);
80
396
  // ==================== 临时复用向量/矩阵 ====================
81
- this.tempVector = new THREE.Vector3();
82
- this.tempVector2 = new THREE.Vector3();
83
- this.tempBox = new THREE.Box3();
84
- this.tempMat = new THREE.Matrix4();
85
- 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();
86
402
  this.recheckAnimTimer = null;
87
403
  // ==================== 相机朝向/移动复用向量 ====================
88
- this.camDir = new THREE.Vector3();
89
- this.moveDir = new THREE.Vector3();
90
- this.targetQuat = new THREE.Quaternion();
91
- 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();
92
408
  this.rotationSpeed = 10;
93
- this.DIR_FWD = new THREE.Vector3(0, 0, -1);
94
- this.DIR_BKD = new THREE.Vector3(0, 0, 1);
95
- this.DIR_LFT = new THREE.Vector3(-1, 0, 0);
96
- this.DIR_RGT = new THREE.Vector3(1, 0, 0);
97
- 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);
98
414
  // ==================== 射线检测 ====================
99
- this._personToCam = new THREE.Vector3();
100
- this._originTmp = new THREE.Vector3();
101
- this._raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0));
102
- this._raycasterPersonToCam = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3());
103
- /**
104
- * 根据按键设置人物动画
105
- */
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
+ };
106
430
  this.setAnimationByPressed = () => {
107
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
+ }
108
446
  if (this.isFlying) {
109
447
  if (!this.fwdPressed) {
110
448
  this.playPersonAnimationByName("flyidle");
@@ -149,31 +487,33 @@ var PlayerController = class {
149
487
  }, 200);
150
488
  };
151
489
  // ==================== 事件处理 ====================
152
- /**
153
- * 键盘按下事件
154
- */
155
490
  this._boundOnKeydown = async (e) => {
156
491
  if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) {
157
492
  e.preventDefault();
158
493
  }
159
494
  switch (e.code) {
160
495
  case "KeyW":
496
+ case "ArrowUp":
161
497
  this.fwdPressed = true;
162
498
  this.setAnimationByPressed();
163
499
  break;
164
500
  case "KeyS":
501
+ case "ArrowDown":
165
502
  this.bkdPressed = true;
166
503
  this.setAnimationByPressed();
167
504
  break;
168
505
  case "KeyD":
506
+ case "ArrowRight":
169
507
  this.rgtPressed = true;
170
508
  this.setAnimationByPressed();
171
509
  break;
172
510
  case "KeyA":
511
+ case "ArrowLeft":
173
512
  this.lftPressed = true;
174
513
  this.setAnimationByPressed();
175
514
  break;
176
515
  case "ShiftLeft":
516
+ case "ShiftRight":
177
517
  this.shiftPressed = true;
178
518
  this.setAnimationByPressed();
179
519
  this.controls.mouseButtons = { LEFT: 2, MIDDLE: 1, RIGHT: 0 };
@@ -194,6 +534,7 @@ var PlayerController = class {
194
534
  this.changeView();
195
535
  break;
196
536
  case "KeyF":
537
+ if (this.controllerMode == 1) return;
197
538
  this.isFlying = !this.isFlying;
198
539
  this.setAnimationByPressed();
199
540
  if (!this.isFlying && !this.playerIsOnGround) {
@@ -201,32 +542,39 @@ var PlayerController = class {
201
542
  }
202
543
  break;
203
544
  case "KeyE":
204
- this.setDrive();
545
+ if (this.isFlying) return;
546
+ if (this.controllerMode == 0) {
547
+ this.enterVehicle();
548
+ } else {
549
+ this.exitVehicle();
550
+ }
205
551
  break;
206
552
  }
207
553
  };
208
- /**
209
- * 键盘抬起事件
210
- */
211
554
  this._boundOnKeyup = (e) => {
212
555
  switch (e.code) {
213
556
  case "KeyW":
557
+ case "ArrowUp":
214
558
  this.fwdPressed = false;
215
559
  this.setAnimationByPressed();
216
560
  break;
217
561
  case "KeyS":
562
+ case "ArrowDown":
218
563
  this.bkdPressed = false;
219
564
  this.setAnimationByPressed();
220
565
  break;
221
566
  case "KeyD":
567
+ case "ArrowRight":
222
568
  this.rgtPressed = false;
223
569
  this.setAnimationByPressed();
224
570
  break;
225
571
  case "KeyA":
572
+ case "ArrowLeft":
226
573
  this.lftPressed = false;
227
574
  this.setAnimationByPressed();
228
575
  break;
229
576
  case "ShiftLeft":
577
+ case "ShiftRight":
230
578
  this.shiftPressed = false;
231
579
  this.setAnimationByPressed();
232
580
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
@@ -239,23 +587,14 @@ var PlayerController = class {
239
587
  break;
240
588
  }
241
589
  };
242
- /**
243
- * 鼠标移动事件
244
- */
245
590
  this._mouseMove = (e) => {
246
591
  if (document.pointerLockElement !== document.body) return;
247
592
  this.setToward(e.movementX, e.movementY, 1e-4);
248
593
  };
249
- /**
250
- * 鼠标点击事件
251
- */
252
- this._mouseClick = (e) => {
594
+ this._mouseClick = (_e) => {
253
595
  this.setPointerLock();
254
596
  };
255
597
  // ==================== 移动端控制 ====================
256
- /**
257
- * 指针按下事件
258
- */
259
598
  this.onPointerDown = (e) => {
260
599
  if (e.pointerType !== "touch") return;
261
600
  this.isLookDown = true;
@@ -265,9 +604,6 @@ var PlayerController = class {
265
604
  this.lookAreaEl?.setPointerCapture?.(e.pointerId);
266
605
  e.preventDefault();
267
606
  };
268
- /**
269
- * 指针移动事件
270
- */
271
607
  this.onPointerMove = (e) => {
272
608
  if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
273
609
  const dx = e.clientX - this.lastTouchX;
@@ -277,9 +613,6 @@ var PlayerController = class {
277
613
  this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
278
614
  e.preventDefault();
279
615
  };
280
- /**
281
- * 指针抬起事件
282
- */
283
616
  this.onPointerUp = (e) => {
284
617
  if (e.pointerId !== this.lookPointerId) return;
285
618
  this.isLookDown = false;
@@ -290,19 +623,15 @@ var PlayerController = class {
290
623
  this._raycasterPersonToCam.firstHitOnly = true;
291
624
  }
292
625
  // ==================== 初始化相关方法 ====================
293
- /**
294
- * 初始化控制器
295
- */
296
626
  async init(opts, callback) {
297
627
  this.scene = opts.scene;
298
628
  this.camera = opts.camera;
299
629
  this.camera.rotation.order = "YXZ";
300
630
  this.controls = opts.controls;
301
631
  this.playerModel = opts.playerModel;
302
- this.initPos = opts.initPos ?? new THREE.Vector3(0, 0, 0);
632
+ this.initPos = opts.initPos ?? new THREE3.Vector3(0, 0, 0);
303
633
  this.mouseSensity = opts.mouseSensity ?? 5;
304
634
  const s = this.playerModel.scale;
305
- this.visualizeDepth = 0 * s;
306
635
  this.gravity = (opts.playerModel.gravity ?? -2400) * s;
307
636
  this.jumpHeight = (opts.playerModel.jumpHeight ?? 800) * s;
308
637
  this.originPlayerSpeed = (opts.playerModel.speed ?? 400) * s;
@@ -314,52 +643,104 @@ var PlayerController = class {
314
643
  this._maxCamDistance = (opts.maxCamDistance ?? 440) * s;
315
644
  this.orginMaxCamDistance = this._maxCamDistance;
316
645
  this.thirdMouseMode = opts.thirdMouseMode ?? 1;
646
+ this.enableZoom = opts.enableZoom ?? false;
317
647
  const isMobileDevice = () => navigator.maxTouchPoints && navigator.maxTouchPoints > 0 || "ontouchstart" in window || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
318
648
  this.isShowMobileControls = (opts.isShowMobileControls ?? true) && isMobileDevice();
319
649
  if (this.isShowMobileControls) {
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,135 +831,467 @@ 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);
869
+ }
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}`);
511
880
  }
512
- console.log("\u5F00\u95E8\u52A8\u753B", this.vehicleActions.get("open_door"));
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
+ }
518
1009
  /**
519
- * 第一/三人称视角切换
1010
+ * 为指定车辆创建障碍物检测器
520
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
+ }
1160
+ /**
1161
+ * 完成上车
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);
1242
+ this.setFirstPersonCamera();
527
1243
  } else {
528
1244
  this.scene.attach(this.camera);
529
1245
  const worldPos = this.player.position.clone();
530
- 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);
531
1247
  const angle = Math.atan2(dir.z, dir.x);
532
- 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);
533
1249
  this.camera.position.copy(worldPos).add(offset);
534
1250
  this.controls.target.copy(worldPos);
1251
+ this.controls.enableZoom = this.enableZoom;
535
1252
  }
536
1253
  this.setPointerLock();
537
1254
  }
538
- setDrive() {
539
- this.controllerMode = 2;
540
- 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;
541
1265
  }
542
- /**
543
- * 设置指针锁定
544
- */
545
1266
  setPointerLock() {
546
1267
  if ((this.thirdMouseMode === 0 || this.thirdMouseMode === 1) && !this.isFirstPerson || this.isFirstPerson) {
547
1268
  document.body.requestPointerLock();
1269
+ } else {
1270
+ document.exitPointerLock();
548
1271
  }
549
1272
  }
550
- /**
551
- * 设置摄像机初始位置
552
- */
553
1273
  setCameraPos() {
554
- if (this.isFirstPerson) {
555
- this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
556
- } else {
557
- const worldPos = this.player.position.clone();
558
- const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
559
- const angle = Math.atan2(dir.z, dir.x);
560
- const offset = new THREE.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, -100 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
561
- this.camera.position.copy(worldPos).add(offset);
562
- }
563
- 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);
564
1288
  }
565
- /**
566
- * 设置控制器
567
- */
568
1289
  setControls() {
569
- this.controls.enabled = !(this.thirdMouseMode === 0 || this.thirdMouseMode === 1);
1290
+ this.controls.enableZoom = this.enableZoom;
570
1291
  this.controls.rotateSpeed = this.mouseSensity * 0.05;
571
1292
  this.controls.maxPolarAngle = Math.PI * (300 / 360);
1293
+ this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
572
1294
  }
573
- /**
574
- * 重置控制器
575
- */
576
1295
  resetControls() {
577
1296
  if (!this.controls) return;
578
1297
  this.controls.enabled = true;
@@ -582,38 +1301,60 @@ var PlayerController = class {
582
1301
  this.controls.enableZoom = true;
583
1302
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
584
1303
  }
585
- /**
586
- * 设置朝向
587
- */
588
1304
  setToward(dx, dy, speed) {
589
- if (this.isFirstPerson) {
590
- const yaw = -dx * speed * this.mouseSensity;
591
- const pitch = -dy * speed * this.mouseSensity;
592
- this.player.rotateY(yaw);
593
- 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
+ }
594
1330
  } else {
595
- const sensitivity = this.mouseSensity;
596
- const deltaX = -dx * speed * sensitivity;
597
- const deltaY = -dy * speed * sensitivity;
598
- const target = this.player.position.clone();
599
- const distance = this.camera.position.distanceTo(target);
600
- const currentPosition = this.camera.position.clone().sub(target);
601
- let theta = Math.atan2(currentPosition.x, currentPosition.z);
602
- let phi = Math.acos(currentPosition.y / distance);
603
- theta += deltaX;
604
- phi += deltaY;
605
- phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
606
- const newX = distance * Math.sin(phi) * Math.sin(theta);
607
- const newY = distance * Math.cos(phi);
608
- const newZ = distance * Math.sin(phi) * Math.cos(theta);
609
- this.camera.position.set(target.x + newX, target.y + newY, target.z + newZ);
610
- 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
+ }
611
1356
  }
612
1357
  }
613
- // ==================== 物理与碰撞检测 ====================
614
- /**
615
- * 统一属性集合
616
- */
617
1358
  unifiedAttribute(collected) {
618
1359
  const attrMap = /* @__PURE__ */ new Map();
619
1360
  const attrConflict = /* @__PURE__ */ new Set();
@@ -660,28 +1401,14 @@ var PlayerController = class {
660
1401
  const meta = attrMap.get(name);
661
1402
  const len = count * meta.itemSize;
662
1403
  const array = new meta.arrayCtor(len);
663
- g.setAttribute(name, new THREE.BufferAttribute(array, meta.itemSize, meta.normalized));
1404
+ g.setAttribute(name, new THREE3.BufferAttribute(array, meta.itemSize, meta.normalized));
664
1405
  }
665
1406
  }
666
1407
  }
667
1408
  return collected;
668
1409
  }
669
- /**
670
- * BVH碰撞体构建
671
- */
672
1410
  async createBVH(meshUrl = "") {
673
1411
  await this.initLoader();
674
- const ensureAttributesMinimal = (geom) => {
675
- if (!geom.attributes.position) return null;
676
- if (!geom.attributes.normal) geom.computeVertexNormals();
677
- if (!geom.attributes.uv) {
678
- const count = geom.attributes.position.count;
679
- const dummyUV = new Float32Array(count * 2);
680
- geom.setAttribute("uv", new THREE.BufferAttribute(dummyUV, 2));
681
- }
682
- return geom;
683
- };
684
- let collected = [];
685
1412
  if (meshUrl === "") {
686
1413
  if (this.collider) {
687
1414
  this.scene.remove(this.collider);
@@ -694,15 +1421,15 @@ var PlayerController = class {
694
1421
  let geom = mesh.geometry.clone();
695
1422
  geom.applyMatrix4(mesh.matrixWorld);
696
1423
  if (geom.index) geom = geom.toNonIndexed();
697
- const safe = ensureAttributesMinimal(geom);
698
- if (safe) collected.push(safe);
1424
+ const safe = this.ensureAttributesMinimal(geom);
1425
+ if (safe) this.collected.push(safe);
699
1426
  } catch (e) {
700
1427
  console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
701
1428
  }
702
1429
  }
703
1430
  });
704
- if (!collected.length) return;
705
- collected = this.unifiedAttribute(collected);
1431
+ if (!this.collected.length) return;
1432
+ this.collected = this.unifiedAttribute(this.collected);
706
1433
  } else {
707
1434
  const gltf = await this.loader.loadAsync(meshUrl);
708
1435
  const mesh = gltf.scene.children[0];
@@ -710,34 +1437,73 @@ var PlayerController = class {
710
1437
  let geom = mesh.geometry.clone();
711
1438
  geom.applyMatrix4(mesh.matrixWorld);
712
1439
  if (geom.index) geom = geom.toNonIndexed();
713
- const safe = ensureAttributesMinimal(geom);
714
- if (safe) collected.push(safe);
1440
+ const safe = this.ensureAttributesMinimal(geom);
1441
+ if (safe) this.collected.push(safe);
715
1442
  }
716
- const merged = BufferGeometryUtils.mergeGeometries(collected, false);
1443
+ const merged = BufferGeometryUtils.mergeGeometries(this.collected, false);
717
1444
  if (!merged) {
718
1445
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
719
1446
  return;
720
1447
  }
721
1448
  merged.boundsTree = new MeshBVH(merged, { maxDepth: 100 });
722
- this.collider = new THREE.Mesh(
1449
+ this.collider = new THREE3.Mesh(
723
1450
  merged,
724
- new THREE.MeshBasicMaterial({
1451
+ new THREE3.MeshBasicMaterial({
725
1452
  opacity: 0.5,
726
1453
  transparent: true,
727
- wireframe: true
1454
+ wireframe: true,
1455
+ depthTest: true
728
1456
  })
729
1457
  );
730
1458
  if (this.displayCollider) this.scene.add(this.collider);
731
1459
  if (this.displayVisualizer) {
732
1460
  if (this.visualizer) this.scene.remove(this.visualizer);
733
- this.visualizer = new MeshBVHHelper(this.collider, this.visualizeDepth);
1461
+ this.visualizer = new MeshBVHHelper(this.collider, 0);
734
1462
  this.scene.add(this.visualizer);
735
1463
  }
736
1464
  this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
737
1465
  }
738
- /**
739
- * 获取法线与Y轴的夹角
740
- */
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
+ }
741
1507
  getAngleWithYAxis(normal) {
742
1508
  const yAxis = { x: 0, y: 1, z: 0 };
743
1509
  const dotProduct = normal.x * yAxis.x + normal.y * yAxis.y + normal.z * yAxis.z;
@@ -745,13 +1511,193 @@ var PlayerController = class {
745
1511
  const cosTheta = dotProduct / normalMagnitude;
746
1512
  return Math.acos(cosTheta);
747
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
+ }
748
1551
  // ==================== 循环更新 ====================
749
- /**
750
- * 每帧更新
751
- */
752
1552
  async update(delta = clock.getDelta()) {
753
1553
  if (!this.isupdate || !this.player || !this.collider) return;
754
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
+ }
755
1701
  if (!this.isFlying) {
756
1702
  this.player.position.addScaledVector(this.playerVelocity, delta);
757
1703
  }
@@ -782,32 +1728,24 @@ var PlayerController = class {
782
1728
  const intersects = this._raycaster.intersectObject(this.collider, false);
783
1729
  if (intersects.length > 0) {
784
1730
  playerDistanceFromGround = this.player.position.y - intersects[0].point.y;
785
- const normal = intersects[0].normal;
786
- const angle2 = this.getAngleWithYAxis(normal) * 180 / Math.PI;
787
1731
  const maxH = this.playerHeight * this.playerModel.scale * 0.9;
788
1732
  const h = this.playerHeight * this.playerModel.scale * 0.75;
789
1733
  const minH = this.playerHeight * this.playerModel.scale * 0.7;
790
1734
  if (!this.isFlying) {
791
- if (playerDistanceFromGround > maxH) {
1735
+ if (playerDistanceFromGround >= maxH) {
792
1736
  this.playerVelocity.y += delta * this.gravity;
793
1737
  this.player.position.addScaledVector(this.playerVelocity, delta);
794
1738
  this.playerIsOnGround = false;
795
- } else if (playerDistanceFromGround > h && playerDistanceFromGround < maxH) {
796
- if (angle2 >= 0 && angle2 < 5) {
797
- this.playerVelocity.y += delta * this.gravity;
798
- 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);
799
1742
  this.playerIsOnGround = true;
800
- } else {
801
- if (this.spacePressed) {
802
- this.playerVelocity.y += delta * this.gravity;
803
- } else {
804
- this.playerVelocity.set(0, 0, 0);
805
- this.playerIsOnGround = true;
806
- }
1743
+ this.player.position.y = intersects[0].point.y + h;
807
1744
  }
808
- } else if (playerDistanceFromGround > minH && playerDistanceFromGround < h) {
1745
+ } else if (playerDistanceFromGround >= minH && playerDistanceFromGround < h) {
809
1746
  this.playerVelocity.set(0, 0, 0);
810
1747
  this.playerIsOnGround = true;
1748
+ this.player.position.y = intersects[0].point.y + h;
811
1749
  } else if (playerDistanceFromGround < minH) {
812
1750
  this.playerVelocity.set(0, 0, 0);
813
1751
  this.player.position.set(this.player.position.x, intersects[0].point.y + h, this.player.position.z);
@@ -825,23 +1763,38 @@ var PlayerController = class {
825
1763
  this.tempBox.expandByPoint(this.tempSegment.start);
826
1764
  this.tempBox.expandByPoint(this.tempSegment.end);
827
1765
  this.tempBox.expandByScalar(capsuleInfo.radius);
828
- const bvh = this.collider?.geometry;
829
- bvh?.boundsTree?.shapecast({
830
- // 检测包围盒碰撞
831
- intersectsBounds: (box) => box.intersectsBox(this.tempBox),
832
- // 检测三角形碰撞
833
- intersectsTriangle: (tri) => {
834
- const triPoint = this.tempVector;
835
- const capsulePoint = this.tempVector2;
836
- const distance = tri.closestPointToSegment(this.tempSegment, triPoint, capsulePoint);
837
- if (distance < capsuleInfo.radius) {
838
- const depth = capsuleInfo.radius - distance;
839
- const direction = capsulePoint.sub(triPoint).normalize();
840
- this.tempSegment.start.addScaledVector(direction, depth);
841
- 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
+ }
842
1781
  }
843
- }
844
- });
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
+ }
845
1798
  const newPosition = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
846
1799
  const deltaVector = this.tempVector2.subVectors(newPosition, this.player.position);
847
1800
  const offset = Math.max(0, deltaVector.length() - 1e-5);
@@ -874,58 +1827,62 @@ var PlayerController = class {
874
1827
  }
875
1828
  }
876
1829
  if (this.isFlying) {
877
- this.camDir.y = 0;
878
- this.camDir.normalize();
879
- this.camDir.negate();
880
- this.moveDir.normalize();
881
- this.moveDir.negate();
882
- const lookTarget = this.player.position.clone().add(this.fwdPressed ? this.moveDir : this.camDir);
883
- this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
884
- this.targetQuat.setFromRotationMatrix(this.targetMat);
885
- const alpha = Math.min(1, this.rotationSpeed * delta);
886
- 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
+ }
887
1842
  }
888
1843
  if (!this.isFirstPerson) {
889
1844
  const lookTarget = this.player.position.clone();
890
- lookTarget.y += 30 * this.playerModel.scale;
1845
+ lookTarget.y += this.playerHeight / 6 * this.playerModel.scale;
891
1846
  this.camera.position.sub(this.controls.target);
892
1847
  this.controls.target.copy(lookTarget);
893
1848
  this.camera.position.add(lookTarget);
894
1849
  this.controls.update();
895
- this._personToCam.subVectors(this.camera.position, this.player.position);
896
- const origin = this.player.position.clone().add(new THREE.Vector3(0, 0, 0));
897
- const direction = this._personToCam.clone().normalize();
898
- const desiredDist = this._personToCam.length();
899
- this._raycasterPersonToCam.set(origin, direction);
900
- this._raycasterPersonToCam.far = desiredDist;
901
- const intersects2 = this._raycasterPersonToCam.intersectObject(this.collider, false);
902
- if (intersects2.length > 0) {
903
- const hit = intersects2[0];
904
- const safeDist = Math.max(hit.distance - this._camEpsilon, this._minCamDistance);
905
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
906
- this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
907
- } else {
908
- this._raycasterPersonToCam.far = this._maxCamDistance;
909
- const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(this.collider, false);
910
- let safeDist = this._maxCamDistance;
911
- if (intersectsMaxDis.length) {
912
- const hitMax = intersectsMaxDis[0];
913
- safeDist = hitMax.distance - this._camEpsilon;
1850
+ if (!this.enableZoom) {
1851
+ this._personToCam.subVectors(this.camera.position, this.player.position);
1852
+ const origin = this.player.position.clone();
1853
+ const direction = this._personToCam.clone().normalize();
1854
+ const desiredDist = this._personToCam.length();
1855
+ this._raycasterPersonToCam.set(origin, direction);
1856
+ this._raycasterPersonToCam.far = desiredDist;
1857
+ const intersectsCamera = this._raycasterPersonToCam.intersectObject(this.collider, false);
1858
+ if (intersectsCamera.length > 0) {
1859
+ const hit = intersectsCamera[0];
1860
+ const safeDist = Math.max(hit.distance - this._camEpsilon, this._minCamDistance);
1861
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1862
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1863
+ } else {
1864
+ this._raycasterPersonToCam.far = this._maxCamDistance;
1865
+ const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(this.collider, false);
1866
+ let safeDist = this._maxCamDistance;
1867
+ if (intersectsMaxDis.length) {
1868
+ const hitMax = intersectsMaxDis[0];
1869
+ safeDist = hitMax.distance - this._camEpsilon;
1870
+ }
1871
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1872
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
914
1873
  }
915
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
916
- this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
917
1874
  }
918
1875
  }
919
1876
  if (this.player.position.y < this.boundingBoxMinY - 1) {
920
1877
  this._originTmp.set(this.player.position.x, 1e4, this.player.position.z);
921
1878
  this._raycaster.ray.origin.copy(this._originTmp);
922
- const intersects2 = this._raycaster.intersectObject(this.collider, false);
923
- if (intersects2.length > 0) {
1879
+ const intersectsFall = this._raycaster.intersectObject(this.collider, false);
1880
+ if (intersectsFall.length > 0) {
924
1881
  console.log("\u73A9\u5BB6\u4E3Abug\u610F\u5916\u6389\u843D");
925
- 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));
926
1883
  } else {
927
1884
  console.log("\u73A9\u5BB6\u6B63\u5E38\u6389\u843D");
928
- 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));
929
1886
  }
930
1887
  }
931
1888
  }
@@ -934,26 +1891,19 @@ var PlayerController = class {
934
1891
  */
935
1892
  updateMixers(delta) {
936
1893
  if (this.personMixer) this.personMixer.update(delta);
937
- if (this.vehicleMixer) this.vehicleMixer.update(delta);
1894
+ for (const v of this.vehicles) {
1895
+ v.vehicleMixer?.update(delta);
1896
+ }
938
1897
  }
939
- /**
940
- * 重置玩家位置
941
- */
942
1898
  reset(position) {
943
1899
  if (!this.player) return;
944
1900
  this.playerVelocity.set(0, 0, 0);
945
1901
  this.player.position.copy(position ?? this.initPos);
946
1902
  }
947
- /**
948
- * 获取玩家位置
949
- */
950
1903
  getPosition() {
951
1904
  return this.player.position;
952
1905
  }
953
1906
  // ==================== 输入处理 ====================
954
- /**
955
- * 设置输入
956
- */
957
1907
  setInput(input) {
958
1908
  if (typeof input.moveX === "number") {
959
1909
  this.lftPressed = input.moveX === -1;
@@ -993,9 +1943,6 @@ var PlayerController = class {
993
1943
  }
994
1944
  }
995
1945
  }
996
- /**
997
- * 事件绑定
998
- */
999
1946
  onAllEvent() {
1000
1947
  this.isupdate = true;
1001
1948
  this.setPointerLock();
@@ -1004,9 +1951,6 @@ var PlayerController = class {
1004
1951
  window.addEventListener("mousemove", this._mouseMove);
1005
1952
  window.addEventListener("click", this._mouseClick);
1006
1953
  }
1007
- /**
1008
- * 事件解绑
1009
- */
1010
1954
  offAllEvent() {
1011
1955
  this.isupdate = false;
1012
1956
  document.exitPointerLock();
@@ -1015,9 +1959,6 @@ var PlayerController = class {
1015
1959
  window.removeEventListener("mousemove", this._mouseMove);
1016
1960
  window.removeEventListener("click", this._mouseClick);
1017
1961
  }
1018
- /**
1019
- * 初始化移动端摇杆控制
1020
- */
1021
1962
  async initMobileControls() {
1022
1963
  this.controls.maxPolarAngle = Math.PI * (300 / 360);
1023
1964
  this.controls.touches = { ONE: null, TWO: null };
@@ -1068,9 +2009,7 @@ var PlayerController = class {
1068
2009
  const sprintThreshold = JOY_SIZE / 2;
1069
2010
  const isSprinting = distance >= sprintThreshold;
1070
2011
  const prev = this.prevJoyState || { dirX: 0, dirY: 0, shift: false };
1071
- if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift) {
1072
- return;
1073
- }
2012
+ if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift) return;
1074
2013
  this.prevJoyState = { dirX, dirY, shift: isSprinting };
1075
2014
  this.setInput({ moveX: dirX, moveY: dirY, shift: isSprinting });
1076
2015
  });
@@ -1182,9 +2121,6 @@ var PlayerController = class {
1182
2121
  { passive: false }
1183
2122
  );
1184
2123
  }
1185
- /**
1186
- * 销毁移动端摇杆控制
1187
- */
1188
2124
  destroyMobileControls() {
1189
2125
  try {
1190
2126
  if (this.joystickManager && this.joystickManager.destroy) {
@@ -1220,9 +2156,6 @@ var PlayerController = class {
1220
2156
  }
1221
2157
  }
1222
2158
  // ==================== 销毁 ====================
1223
- /**
1224
- * 销毁人物控制器
1225
- */
1226
2159
  destroy() {
1227
2160
  this.offAllEvent();
1228
2161
  if (this.player) {
@@ -1244,6 +2177,12 @@ var PlayerController = class {
1244
2177
  this.collider = null;
1245
2178
  }
1246
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;
1247
2186
  controllerInstance = null;
1248
2187
  }
1249
2188
  };
@@ -1252,13 +2191,16 @@ function playerController() {
1252
2191
  const c = controllerInstance;
1253
2192
  return {
1254
2193
  init: (opts, callback) => c.init(opts, callback),
2194
+ loadVehicleModel: (params) => c.loadVehicleModel(params),
1255
2195
  changeView: () => c.changeView(),
1256
2196
  reset: (pos) => c.reset(pos),
1257
2197
  update: (dt) => c.update(dt),
1258
2198
  destroy: () => c.destroy(),
1259
2199
  setInput: (i) => c.setInput(i),
1260
2200
  getposition: () => c.getPosition(),
1261
- loadVehicleModel: (params) => c.loadVehicleModel(params)
2201
+ getPerson: () => c.person,
2202
+ getActiveVehicle: () => c.activeVehicle,
2203
+ getAllVehicles: () => c.vehicles
1262
2204
  };
1263
2205
  }
1264
2206
  function onAllEvent() {