three-player-controller 0.3.7 → 0.3.9

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 THREE3 from "three";
2
+ import * as THREE4 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";
@@ -21,6 +21,231 @@ var vehicle_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAY
21
21
  // assets/imgs/view.png
22
22
  var view_default = "data:image/png;base64,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";
23
23
 
24
+ // src/utils/mobileControls.ts
25
+ var MobileControls = class {
26
+ constructor(setInput, controls) {
27
+ // 摇杆状态
28
+ this.nippleModule = null;
29
+ this.joystickManager = null;
30
+ this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
31
+ // DOM 元素
32
+ this.joystickZoneEl = null;
33
+ this.lookAreaEl = null;
34
+ this.jumpBtnEl = null;
35
+ this.flyBtnEl = null;
36
+ this.viewBtnEl = null;
37
+ this.vehicleBtnEl = null;
38
+ // 触摸状态
39
+ this.lookPointerId = null;
40
+ this.isLookDown = false;
41
+ this.lastTouchX = 0;
42
+ this.lastTouchY = 0;
43
+ // 触摸按下
44
+ this.onPointerDown = (e) => {
45
+ if (e.pointerType !== "touch") return;
46
+ this.isLookDown = true;
47
+ this.lookPointerId = e.pointerId;
48
+ this.lastTouchX = e.clientX;
49
+ this.lastTouchY = e.clientY;
50
+ this.lookAreaEl?.setPointerCapture?.(e.pointerId);
51
+ e.preventDefault();
52
+ };
53
+ // 触摸移动
54
+ this.onPointerMove = (e) => {
55
+ if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
56
+ const dx = e.clientX - this.lastTouchX;
57
+ const dy = e.clientY - this.lastTouchY;
58
+ this.lastTouchX = e.clientX;
59
+ this.lastTouchY = e.clientY;
60
+ this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
61
+ e.preventDefault();
62
+ };
63
+ // 触摸抬起
64
+ this.onPointerUp = (e) => {
65
+ if (e.pointerId !== this.lookPointerId) return;
66
+ this.isLookDown = false;
67
+ this.lookPointerId = null;
68
+ this.lookAreaEl?.releasePointerCapture?.(e.pointerId);
69
+ };
70
+ this.setInput = setInput;
71
+ this.controls = controls;
72
+ }
73
+ // 初始化移动端控制
74
+ async init() {
75
+ this.controls.maxPolarAngle = Math.PI * (300 / 360);
76
+ this.controls.touches = { ONE: null, TWO: null };
77
+ this.nippleModule = await import("nipplejs");
78
+ const nipple = this.nippleModule?.default;
79
+ const JOY_SIZE = 120;
80
+ const container = document.body;
81
+ this.joystickZoneEl = document.createElement("div");
82
+ this.joystickZoneEl.id = "joy-zone";
83
+ Object.assign(this.joystickZoneEl.style, {
84
+ position: "absolute",
85
+ left: "16px",
86
+ bottom: "16px",
87
+ width: `${JOY_SIZE + 40}px`,
88
+ height: `${JOY_SIZE + 40}px`,
89
+ touchAction: "none",
90
+ zIndex: "999",
91
+ pointerEvents: "auto",
92
+ WebkitUserSelect: "none",
93
+ userSelect: "none"
94
+ });
95
+ container.appendChild(this.joystickZoneEl);
96
+ this.blockTouch(this.joystickZoneEl);
97
+ this.joystickManager = nipple.create({
98
+ zone: this.joystickZoneEl,
99
+ mode: "static",
100
+ position: { left: `${(JOY_SIZE + 40) / 2}px`, bottom: `${(JOY_SIZE + 40) / 2}px` },
101
+ color: "#ffffff",
102
+ size: JOY_SIZE,
103
+ multitouch: true,
104
+ maxNumberOfNipples: 1
105
+ });
106
+ this.joystickManager.on("move", (_evt, data) => {
107
+ if (!data) return;
108
+ const rawX = data.vector?.x ?? 0;
109
+ const rawY = data.vector?.y ?? 0;
110
+ const distance = data.distance ?? 0;
111
+ const deadzone = 0.5;
112
+ const dirX = rawX > deadzone ? 1 : rawX < -deadzone ? -1 : 0;
113
+ const dirY = rawY > deadzone ? 1 : rawY < -deadzone ? -1 : 0;
114
+ const isSprinting = distance >= JOY_SIZE / 2;
115
+ const prev = this.prevJoyState;
116
+ if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift) return;
117
+ this.prevJoyState = { dirX, dirY, shift: isSprinting };
118
+ this.setInput({ moveX: dirX, moveY: dirY, shift: isSprinting });
119
+ });
120
+ this.joystickManager.on("end", () => {
121
+ const prev = this.prevJoyState;
122
+ if (prev.dirX !== 0 || prev.dirY !== 0 || prev.shift) {
123
+ this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
124
+ this.setInput({ moveX: 0, moveY: 0, shift: false });
125
+ }
126
+ });
127
+ this.lookAreaEl = document.createElement("div");
128
+ Object.assign(this.lookAreaEl.style, {
129
+ position: "absolute",
130
+ right: "0",
131
+ bottom: "0",
132
+ width: "50%",
133
+ height: "100%",
134
+ zIndex: "998",
135
+ touchAction: "none",
136
+ WebkitUserSelect: "none",
137
+ userSelect: "none"
138
+ });
139
+ container.appendChild(this.lookAreaEl);
140
+ this.blockTouch(this.lookAreaEl);
141
+ this.lookAreaEl.addEventListener("pointerdown", this.onPointerDown, { passive: false });
142
+ this.lookAreaEl.addEventListener("pointermove", this.onPointerMove, { passive: false });
143
+ this.lookAreaEl.addEventListener("pointerup", this.onPointerUp, { passive: false });
144
+ this.lookAreaEl.addEventListener("pointercancel", this.onPointerUp, { passive: false });
145
+ this.jumpBtnEl = this.createBtn(container, 14, 14, jump_default);
146
+ this.flyBtnEl = this.createBtn(container, 14, 14 + 80, fly_default);
147
+ this.viewBtnEl = this.createBtn(container, 14, 14 + 200, view_default);
148
+ this.vehicleBtnEl = this.createBtn(container, 14 + 100, 14 + 120, vehicle_default);
149
+ this.vehicleBtnEl.style.display = "none";
150
+ this.jumpBtnEl.addEventListener("touchstart", (e) => {
151
+ e.preventDefault();
152
+ this.setInput({ jump: true });
153
+ }, { passive: false });
154
+ this.jumpBtnEl.addEventListener("touchend", (e) => {
155
+ e.preventDefault();
156
+ this.setInput({ jump: false });
157
+ }, { passive: false });
158
+ this.jumpBtnEl.addEventListener("touchcancel", (e) => {
159
+ e.preventDefault();
160
+ this.setInput({ jump: false });
161
+ }, { passive: false });
162
+ this.flyBtnEl.addEventListener("touchstart", (e) => {
163
+ e.preventDefault();
164
+ this.setInput({ toggleFly: true });
165
+ }, { passive: false });
166
+ this.viewBtnEl.addEventListener("touchstart", (e) => {
167
+ e.preventDefault();
168
+ this.setInput({ toggleView: true });
169
+ }, { passive: false });
170
+ this.vehicleBtnEl.addEventListener("touchstart", (e) => {
171
+ e.preventDefault();
172
+ this.setInput({ toggleVehicle: true });
173
+ }, { passive: false });
174
+ }
175
+ // 销毁移动端控制
176
+ destroy() {
177
+ try {
178
+ this.joystickManager?.destroy?.();
179
+ this.joystickManager = null;
180
+ if (this.lookAreaEl) {
181
+ this.lookAreaEl.removeEventListener("pointerdown", this.onPointerDown);
182
+ this.lookAreaEl.removeEventListener("pointermove", this.onPointerMove);
183
+ this.lookAreaEl.removeEventListener("pointerup", this.onPointerUp);
184
+ this.lookAreaEl.removeEventListener("pointercancel", this.onPointerUp);
185
+ }
186
+ [this.joystickZoneEl, this.lookAreaEl, this.jumpBtnEl, this.flyBtnEl, this.viewBtnEl, this.vehicleBtnEl].forEach((el) => el?.parentElement?.removeChild(el));
187
+ this.joystickZoneEl = this.lookAreaEl = this.jumpBtnEl = this.flyBtnEl = this.viewBtnEl = this.vehicleBtnEl = null;
188
+ } catch (e) {
189
+ console.warn("\u9500\u6BC1\u79FB\u52A8\u7AEF\u63A7\u5236\u65F6\u51FA\u9519\uFF1A", e);
190
+ }
191
+ }
192
+ // 同步车辆按钮显隐
193
+ syncVehicleBtn(show) {
194
+ if (this.vehicleBtnEl) this.vehicleBtnEl.style.display = show ? "block" : "none";
195
+ }
196
+ // 同步控制模式按钮
197
+ syncControllerModeBtn(mode) {
198
+ if (!this.flyBtnEl || !this.jumpBtnEl) return;
199
+ if (mode === 0) {
200
+ this.flyBtnEl.style.display = "block";
201
+ this.jumpBtnEl.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${jump_default}")`;
202
+ } else {
203
+ this.flyBtnEl.style.display = "none";
204
+ this.jumpBtnEl.style.backgroundImage = `url(${break_default})`;
205
+ }
206
+ }
207
+ // 阻止默认触摸
208
+ blockTouch(el) {
209
+ ["touchstart", "touchmove", "touchend", "touchcancel"].forEach((name) => {
210
+ el.addEventListener(name, (e) => e.preventDefault(), { passive: false });
211
+ });
212
+ }
213
+ // 创建圆形按钮
214
+ createBtn(container, rightPx, bottomPx, bgUrl) {
215
+ const btn = document.createElement("button");
216
+ Object.assign(btn.style, {
217
+ position: "absolute",
218
+ right: `${rightPx}px`,
219
+ bottom: `${bottomPx}px`,
220
+ width: "56px",
221
+ height: "56px",
222
+ zIndex: "1000",
223
+ borderRadius: "50%",
224
+ border: "2px solid black",
225
+ padding: "20px",
226
+ opacity: "0.95",
227
+ touchAction: "none",
228
+ fontSize: "14px",
229
+ userSelect: "none",
230
+ overflow: "hidden",
231
+ boxSizing: "border-box",
232
+ backgroundColor: "transparent",
233
+ backgroundRepeat: "no-repeat, no-repeat",
234
+ backgroundPosition: "center center, center center",
235
+ backgroundSize: "100% 100%, 80% 80%",
236
+ backgroundImage: `linear-gradient(rgba(0,0,0,0.5),rgba(0,0,0,0.5)),url("${bgUrl}")`
237
+ });
238
+ container.appendChild(btn);
239
+ ["touchstart", "touchend", "touchcancel"].forEach((name) => {
240
+ btn.addEventListener(name, (e) => e.preventDefault(), { passive: false });
241
+ });
242
+ return btn;
243
+ }
244
+ };
245
+
246
+ // src/utils/vehicleLoader.ts
247
+ import * as THREE3 from "three";
248
+
24
249
  // src/utils/pathPlanner.ts
25
250
  import * as THREE from "three";
26
251
  var PathNode = class {
@@ -34,6 +259,7 @@ var PathNode = class {
34
259
  this.parent = null;
35
260
  this.position = position.clone();
36
261
  }
262
+ // 判断节点相等
37
263
  equals(other) {
38
264
  return this.position.distanceTo(other.position) < 0.01;
39
265
  }
@@ -42,10 +268,12 @@ var PriorityQueue = class {
42
268
  constructor() {
43
269
  this.elements = [];
44
270
  }
271
+ // 入队并排序
45
272
  enqueue(item, priority) {
46
273
  this.elements.push({ priority, item });
47
274
  this.elements.sort((a, b) => a.priority - b.priority);
48
275
  }
276
+ // 出队最小优先级
49
277
  dequeue() {
50
278
  return this.elements.shift()?.item;
51
279
  }
@@ -55,6 +283,7 @@ var PriorityQueue = class {
55
283
  contains(item, compareFn) {
56
284
  return this.elements.some((e) => compareFn(e.item, item));
57
285
  }
286
+ // 更新节点优先级
58
287
  update(item, newPriority, compareFn) {
59
288
  const index = this.elements.findIndex((e) => compareFn(e.item, item));
60
289
  if (index !== -1) {
@@ -68,22 +297,15 @@ var PathPlanner = class {
68
297
  this.debugLines = [];
69
298
  this.debugPoints = [];
70
299
  this.obstacleChecker = obstacleChecker;
71
- this.config = {
72
- debugEnabled: false,
73
- scale: 1,
74
- ...config
75
- };
300
+ this.config = { debugEnabled: false, scale: 1, ...config };
76
301
  }
77
- // 计算启发式距离
302
+ // 启发式距离
78
303
  heuristic(a, b) {
79
304
  return a.distanceTo(b);
80
305
  }
81
- // A*路径规划算法
306
+ // A* 寻路主入口
82
307
  findPath(start, goal) {
83
- const startTime = performance.now();
84
- if (!this.obstacleChecker.isBlocked(start, goal)) {
85
- return [goal];
86
- }
308
+ if (!this.obstacleChecker.isBlocked(start, goal)) return [goal];
87
309
  const navigationPoints = this.obstacleChecker.getNavigationNodes(start, goal);
88
310
  const allNodes = [new PathNode(start), new PathNode(goal), ...navigationPoints.map((p) => new PathNode(p))];
89
311
  if (allNodes.length < 2) {
@@ -97,45 +319,35 @@ var PathPlanner = class {
97
319
  startNode.f = startNode.h;
98
320
  const openList = new PriorityQueue();
99
321
  const closedSet = /* @__PURE__ */ new Set();
100
- openList.enqueue(startNode, startNode.f);
101
322
  const nodeEquals = (a, b) => a.equals(b);
323
+ openList.enqueue(startNode, startNode.f);
102
324
  while (!openList.isEmpty()) {
103
325
  const current = openList.dequeue();
104
326
  if (!current) break;
105
327
  if (current.equals(goalNode)) {
106
328
  const path = this.reconstructPath(current);
107
- const endTime = performance.now();
108
- if (this.config.debugEnabled) {
109
- this.visualizePath([start, ...path]);
110
- }
329
+ if (this.config.debugEnabled) this.visualizePath([start, ...path]);
111
330
  return path;
112
331
  }
113
332
  closedSet.add(current);
114
333
  for (const neighbor of allNodes) {
115
334
  if (closedSet.has(neighbor)) continue;
116
- if (this.obstacleChecker.isBlocked(current.position, neighbor.position)) {
117
- continue;
118
- }
335
+ if (this.obstacleChecker.isBlocked(current.position, neighbor.position)) continue;
119
336
  const tentativeG = current.g + current.position.distanceTo(neighbor.position);
120
337
  if (tentativeG < neighbor.g) {
121
338
  neighbor.parent = current;
122
339
  neighbor.g = tentativeG;
123
340
  neighbor.h = this.heuristic(neighbor.position, goalNode.position);
124
341
  neighbor.f = neighbor.g + neighbor.h;
125
- if (openList.contains(neighbor, nodeEquals)) {
126
- openList.update(neighbor, neighbor.f, nodeEquals);
127
- } else {
128
- openList.enqueue(neighbor, neighbor.f);
129
- }
342
+ if (openList.contains(neighbor, nodeEquals)) openList.update(neighbor, neighbor.f, nodeEquals);
343
+ else openList.enqueue(neighbor, neighbor.f);
130
344
  }
131
345
  }
132
346
  }
133
- console.warn("A*\u672A\u627E\u5230\u8DEF\u5F84\uFF0C\u4F7F\u7528\u76F4\u7EBF\u8DEF\u5F84");
347
+ console.warn("A* \u672A\u627E\u5230\u8DEF\u5F84\uFF0C\u4F7F\u7528\u76F4\u7EBF\u8DEF\u5F84");
134
348
  return [goal];
135
349
  }
136
- /**
137
- * 重建路径
138
- */
350
+ // 重建路径
139
351
  reconstructPath(endNode) {
140
352
  const path = [];
141
353
  let current = endNode;
@@ -143,14 +355,10 @@ var PathPlanner = class {
143
355
  path.unshift(current.position.clone());
144
356
  current = current.parent;
145
357
  }
146
- if (path.length > 0) {
147
- path.shift();
148
- }
358
+ if (path.length > 0) path.shift();
149
359
  return this.smoothPath(path);
150
360
  }
151
- /**
152
- * 路径平滑
153
- */
361
+ // 路径平滑优化
154
362
  smoothPath(path) {
155
363
  if (path.length <= 2) return path;
156
364
  const smoothed = [path[0]];
@@ -168,38 +376,28 @@ var PathPlanner = class {
168
376
  }
169
377
  return smoothed;
170
378
  }
171
- /**
172
- * 可视化路径
173
- */
379
+ // 可视化路径
174
380
  visualizePath(path) {
175
381
  if (!this.config.scene || !this.config.debugEnabled) return;
176
382
  this.clearVisualization();
177
383
  const scale = this.config.scale || 1;
178
384
  if (path.length > 1) {
179
- const points = path.map((p) => p.clone());
180
- const geometry = new THREE.BufferGeometry().setFromPoints(points);
181
- const material = new THREE.LineBasicMaterial({
182
- color: 65280,
183
- linewidth: 3
184
- });
185
- const line = new THREE.Line(geometry, material);
385
+ const geometry = new THREE.BufferGeometry().setFromPoints(path.map((p) => p.clone()));
386
+ const line = new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 65280, linewidth: 3 }));
186
387
  this.config.scene.add(line);
187
388
  this.debugLines.push(line);
188
389
  }
189
390
  path.forEach((point, index) => {
190
- const geometry = new THREE.SphereGeometry(20 * scale);
191
- const material = new THREE.MeshBasicMaterial({
192
- color: index === path.length - 1 ? 16711680 : 65280
193
- });
194
- const sphere = new THREE.Mesh(geometry, material);
391
+ const sphere = new THREE.Mesh(
392
+ new THREE.SphereGeometry(20 * scale),
393
+ new THREE.MeshBasicMaterial({ color: index === path.length - 1 ? 16711680 : 65280 })
394
+ );
195
395
  sphere.position.copy(point);
196
396
  this.config.scene.add(sphere);
197
397
  this.debugPoints.push(sphere);
198
398
  });
199
399
  }
200
- /**
201
- * 清除路径可视化
202
- */
400
+ // 清除路径可视化
203
401
  clearVisualization() {
204
402
  if (!this.config.scene) return;
205
403
  this.debugLines.forEach((line) => {
@@ -215,15 +413,11 @@ var PathPlanner = class {
215
413
  });
216
414
  this.debugPoints = [];
217
415
  }
218
- /**
219
- * 更新配置
220
- */
416
+ // 更新配置
221
417
  updateConfig(config) {
222
418
  this.config = { ...this.config, ...config };
223
419
  }
224
- /**
225
- * 销毁
226
- */
420
+ // 销毁规划器
227
421
  dispose() {
228
422
  this.clearVisualization();
229
423
  }
@@ -243,7 +437,7 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
243
437
  vehicle.setWheelAxleCs(index, wheel.axleCs);
244
438
  vehicle.setWheelSuspensionRestLength(index, wheel.suspensionRestLength);
245
439
  vehicle.setWheelRadius(index, wheel.radius);
246
- vehicle.setWheelMaxSuspensionTravel(index, wheel.suspensionRestLength * 1);
440
+ vehicle.setWheelMaxSuspensionTravel(index, wheel.suspensionRestLength);
247
441
  vehicle.setWheelSuspensionStiffness(index, 250);
248
442
  vehicle.setWheelSuspensionCompression(index, 6);
249
443
  vehicle.setWheelSuspensionRelaxation(index, 6);
@@ -255,8 +449,8 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
255
449
  vehicle.setWheelSideFrictionStiffness(index, 2);
256
450
  });
257
451
  const up = new THREE2.Vector3(0, 1, 0);
258
- const _wheelSteeringQuat = new THREE2.Quaternion();
259
- const _wheelRotationQuat = new THREE2.Quaternion();
452
+ const wheelSteeringQuat = new THREE2.Quaternion();
453
+ const wheelRotationQuat = new THREE2.Quaternion();
260
454
  function updateWheelVisuals() {
261
455
  for (const [index, wheelObj] of wheels.entries()) {
262
456
  if (!wheelObj) continue;
@@ -267,9 +461,9 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
267
461
  const steering = vehicle.wheelSteering(index) ?? 0;
268
462
  const rotationRad = vehicle.wheelRotation(index) ?? 0;
269
463
  wheelObj.position.y = connection - suspension;
270
- _wheelSteeringQuat.setFromAxisAngle(up, steering);
271
- _wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad);
272
- wheelObj.quaternion.copy(_wheelSteeringQuat).multiply(_wheelRotationQuat);
464
+ wheelSteeringQuat.setFromAxisAngle(up, steering);
465
+ wheelRotationQuat.setFromAxisAngle(wheelAxleCs, rotationRad);
466
+ wheelObj.quaternion.copy(wheelSteeringQuat).multiply(wheelRotationQuat);
273
467
  } catch (e) {
274
468
  }
275
469
  }
@@ -280,37 +474,247 @@ function createVehicleController(world, chassisBody, wheels, wheelsInfo) {
280
474
  } catch {
281
475
  }
282
476
  }
477
+ return { vehicle, updateWheelVisuals, destroy };
478
+ }
479
+
480
+ // src/utils/vehicleLoader.ts
481
+ function getBbox(object) {
482
+ const bbox = new THREE3.Box3().setFromObject(object);
483
+ const center = new THREE3.Vector3();
484
+ const size = new THREE3.Vector3();
485
+ bbox.getCenter(center);
486
+ bbox.getSize(size);
487
+ return { bbox, center, size };
488
+ }
489
+ function createObstacleChecker(vehicleGroup, bbox, scale, playerScale) {
490
+ return {
491
+ // 射线检测路径是否被车辆遮挡
492
+ isBlocked(start, end) {
493
+ const vehiclePos = vehicleGroup.position;
494
+ const vehicleQuat = vehicleGroup.quaternion;
495
+ const center = new THREE3.Vector3();
496
+ const size = new THREE3.Vector3();
497
+ bbox.getCenter(center);
498
+ bbox.getSize(size);
499
+ center.applyQuaternion(vehicleQuat).add(vehiclePos);
500
+ const halfSize = size.clone().multiplyScalar(0.5 * scale);
501
+ const corners = [];
502
+ for (let x = -1; x <= 1; x += 2)
503
+ for (let y = -1; y <= 1; y += 2)
504
+ for (let z = -1; z <= 1; z += 2)
505
+ corners.push(
506
+ new THREE3.Vector3(halfSize.x * x, halfSize.y * y, halfSize.z * z).applyQuaternion(vehicleQuat).add(center)
507
+ );
508
+ const expandedBBox = new THREE3.Box3();
509
+ corners.forEach((c) => expandedBBox.expandByPoint(c));
510
+ expandedBBox.expandByScalar(100 * playerScale);
511
+ const direction = new THREE3.Vector3().subVectors(end, start);
512
+ const length = direction.length();
513
+ const ray = new THREE3.Ray(start, direction.normalize());
514
+ const intersects = ray.intersectBox(expandedBBox, new THREE3.Vector3());
515
+ return intersects !== null && start.distanceTo(intersects) < length;
516
+ },
517
+ // 生成绕行导航节点
518
+ getNavigationNodes(start, _goal) {
519
+ const nodes = [];
520
+ const vehiclePos = vehicleGroup.position;
521
+ const vehicleQuat = vehicleGroup.quaternion;
522
+ const bboxSize = new THREE3.Vector3();
523
+ bbox.getSize(bboxSize);
524
+ const fwd = new THREE3.Vector3(0, 0, 1).applyQuaternion(vehicleQuat);
525
+ const right = new THREE3.Vector3(1, 0, 0).applyQuaternion(vehicleQuat);
526
+ const halfLen = bboxSize.z / 2 * scale;
527
+ const halfWidth = bboxSize.x / 2 * scale;
528
+ const groundY = start.y;
529
+ for (const margin of [300 * playerScale, 500 * playerScale]) {
530
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(halfLen + margin)).add(right.clone().multiplyScalar(-halfWidth - margin)).setY(groundY));
531
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(halfLen + margin)).add(right.clone().multiplyScalar(halfWidth + margin)).setY(groundY));
532
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(-halfLen - margin)).add(right.clone().multiplyScalar(-halfWidth - margin)).setY(groundY));
533
+ nodes.push(vehiclePos.clone().add(fwd.clone().multiplyScalar(-halfLen - margin)).add(right.clone().multiplyScalar(halfWidth + margin)).setY(groundY));
534
+ }
535
+ return nodes;
536
+ }
537
+ };
538
+ }
539
+ async function loadVehicleModel(opts, ctx) {
540
+ const { loader, scene, world, RAPIER, vehicleParams, vehicleLength, playerScale } = ctx;
541
+ const scale = opts.scale ?? 1;
542
+ const chassisRatio = opts.chassisRatio ?? 0.2;
543
+ const suspensionRestLengthRatio = opts.suspensionRestLengthRatio ?? 0.2;
544
+ const speedMultiplier = opts.speedMultiplier ?? 1;
545
+ vehicleParams.power.accelerateForce = 50 * scale;
546
+ vehicleParams.power.brakeForce = 200 * scale;
547
+ vehicleParams.power.maxSpeed = 1e4 * scale;
548
+ vehicleParams.followVehicleDirection = opts.followVehicleDirection ?? true;
549
+ const vehicleModel = await loader.loadAsync(opts.url);
550
+ const { size: originalSize } = getBbox(vehicleModel.scene);
551
+ const modelScale = vehicleLength / Math.max(originalSize.x, originalSize.y, originalSize.z);
552
+ const vehicleMixer = new THREE3.AnimationMixer(vehicleModel.scene);
553
+ const vehicleActions = /* @__PURE__ */ new Map();
554
+ const animations = vehicleModel.animations ?? [];
555
+ const openDoorClip = animations.find((a) => a.name === (opts.animations?.openDoorAnim ?? ""));
556
+ if (openDoorClip) {
557
+ const action = vehicleMixer.clipAction(openDoorClip);
558
+ action.setLoop(THREE3.LoopOnce, 1);
559
+ action.clampWhenFinished = true;
560
+ action.setEffectiveTimeScale(openDoorClip.duration);
561
+ action.enabled = true;
562
+ action.setEffectiveWeight(0);
563
+ vehicleActions.set("openDoor", action);
564
+ }
565
+ const wheelObjects = [];
566
+ for (const name of opts.wheelsNames) {
567
+ let found = false;
568
+ vehicleModel.scene.traverse((child) => {
569
+ if (child.name === name && !found) {
570
+ wheelObjects.push(child);
571
+ found = true;
572
+ }
573
+ });
574
+ if (!found) console.warn(`\u672A\u627E\u5230\u8F6E\u5B50: ${name}`);
575
+ }
576
+ const tempGroup = new THREE3.Group();
577
+ scene.add(tempGroup);
578
+ vehicleModel.scene.scale.multiplyScalar(modelScale * scale);
579
+ vehicleModel.scene.rotateY(vehicleParams.model.rotation);
580
+ const { size, bbox, center } = getBbox(vehicleModel.scene);
581
+ vehicleModel.scene.position.set(-center.x, -center.y, -center.z);
582
+ tempGroup.add(vehicleModel.scene);
583
+ tempGroup.updateMatrixWorld(true);
584
+ let wheelRadius = 0, wheelWidth = 0, suspensionRestLength = 0, chassisHeight = 0, wheelSizeInit = false;
585
+ const wheelsInfo = [];
586
+ for (const wheel of wheelObjects) {
587
+ const worldPos = new THREE3.Vector3();
588
+ const worldQuat = new THREE3.Quaternion();
589
+ const worldScale = new THREE3.Vector3();
590
+ wheel.getWorldPosition(worldPos);
591
+ wheel.getWorldQuaternion(worldQuat);
592
+ wheel.getWorldScale(worldScale);
593
+ if (!wheelSizeInit) {
594
+ const { size: ws } = getBbox(wheel);
595
+ wheelRadius = Number((Math.max(ws.x, ws.y, ws.z) / 2).toFixed(2));
596
+ wheelWidth = Number(Math.min(ws.x, ws.y, ws.z).toFixed(2));
597
+ suspensionRestLength = Number((wheelRadius * 2 * suspensionRestLengthRatio).toFixed(2));
598
+ chassisHeight = Number((wheelRadius * 2 * chassisRatio).toFixed(2));
599
+ wheelSizeInit = true;
600
+ }
601
+ wheelsInfo.push({ axleCs: new THREE3.Vector3(0, 0, -1), position: worldPos, quaternion: worldQuat, scale: worldScale, radius: wheelRadius, width: wheelWidth, suspensionRestLength, object: wheel });
602
+ }
603
+ tempGroup.remove(vehicleModel.scene);
604
+ scene.remove(tempGroup);
605
+ const vehicleGroup = new THREE3.Group();
606
+ scene.add(vehicleGroup);
607
+ vehicleGroup.add(vehicleModel.scene);
608
+ vehicleGroup.updateMatrixWorld(true);
609
+ const wheelWrappers = [];
610
+ for (let i = 0; i < wheelsInfo.length; i++) {
611
+ const wheel = wheelsInfo[i];
612
+ const wheelWrapper = new THREE3.Group();
613
+ wheelWrapper.position.copy(vehicleGroup.worldToLocal(wheel.position.clone()));
614
+ const wheelObj = wheel.object;
615
+ wheelObj.parent?.remove(wheelObj);
616
+ wheelObj.position.set(0, 0, 0);
617
+ wheelObj.quaternion.copy(wheel.quaternion);
618
+ wheelObj.scale.copy(wheel.scale);
619
+ wheelObj.updateMatrixWorld();
620
+ wheelWrapper.add(wheelObj);
621
+ vehicleGroup.add(wheelWrapper);
622
+ wheelWrappers.push(wheelWrapper);
623
+ }
624
+ const halfExtents = size.clone().multiplyScalar(0.5);
625
+ halfExtents.y -= chassisHeight / 2;
626
+ vehicleModel.scene.position.y -= chassisHeight / 2;
627
+ halfExtents.x *= 0.95;
628
+ halfExtents.z *= 0.95;
629
+ const chassisBody = world.createRigidBody(
630
+ RAPIER.RigidBodyDesc.dynamic().setTranslation(opts.position.x, opts.position.y, opts.position.z).setLinearDamping(vehicleParams.chassis.linearDamping).setAngularDamping(vehicleParams.chassis.angularDamping).setCanSleep(true).setAdditionalMass(10)
631
+ );
632
+ world.createCollider(RAPIER.ColliderDesc.cuboid(halfExtents.x, halfExtents.y, halfExtents.z), chassisBody);
633
+ if (vehicleParams.debug.showPhysicsBox) {
634
+ vehicleGroup.add(new THREE3.Mesh(
635
+ new THREE3.BoxGeometry(halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2),
636
+ new THREE3.MeshBasicMaterial({ color: 16711680, wireframe: true, transparent: true, opacity: 0.3 })
637
+ ));
638
+ }
639
+ vehicleGroup.position.copy(opts.position);
640
+ vehicleGroup.updateMatrixWorld(true);
641
+ const { vehicle, updateWheelVisuals } = createVehicleController(world, chassisBody, wheelWrappers, wheelsInfo);
283
642
  return {
284
- vehicle,
643
+ vehicleGroup,
644
+ chassisBody,
645
+ vehicleController: vehicle,
285
646
  updateWheelVisuals,
286
- destroy
647
+ vehicleMixer,
648
+ vehicleActions,
649
+ vehiclIsOpenDoor: false,
650
+ vehicleBBox: bbox.clone(),
651
+ pathPlanner: new PathPlanner(
652
+ createObstacleChecker(vehicleGroup, bbox, scale, playerScale),
653
+ { debugEnabled: false, scene, scale: playerScale }
654
+ ),
655
+ scale,
656
+ boardingPoint: opts.boardingPoint,
657
+ seatOffset: opts.seatOffset ?? new THREE3.Vector3(),
658
+ enterVehicleTime: 1.5,
659
+ chassisRatio,
660
+ suspensionRestLengthRatio,
661
+ size: { l: Math.max(size.x, size.z), w: Math.min(size.x, size.z), h: size.y },
662
+ speedMultiplier
287
663
  };
288
664
  }
289
665
 
290
666
  // src/playerController.ts
291
- THREE3.Mesh.prototype.raycast = acceleratedRaycast;
667
+ THREE4.Mesh.prototype.raycast = acceleratedRaycast;
292
668
  var controllerInstance = null;
293
- var clock = new THREE3.Clock();
669
+ var clock = new THREE4.Clock();
670
+ function isMobileDevice() {
671
+ return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);
672
+ }
294
673
  var PlayerController = class {
295
674
  constructor() {
296
- // ==================== 基本配置与参数 ====================
675
+ // ==================== 场景引用 ====================
297
676
  this.loader = new GLTFLoader();
298
- this.controllerMode = 0;
299
- // 0: 人物 1: 车辆
677
+ // 物理参数
678
+ this.gravity = -2400;
679
+ this.jumpHeight = 600;
680
+ this.playerSpeed = 300;
681
+ this.playerFlySpeed = 2100;
682
+ this.curPlayerSpeed = 0;
683
+ // 交互参数
684
+ this.mouseSensitivity = 5;
685
+ this.thirdMouseMode = 1;
686
+ this.enableZoom = false;
687
+ this.playerFlyEnabled = true;
688
+ this.isShowMobileControls = true;
300
689
  this.enableOverShoulderView = false;
301
- this.isChangeControllerTransitionTimer = null;
302
- // ==================== 玩家基本属性 ====================
690
+ // ==================== 玩家胶囊体 ====================
303
691
  this.playerCapsuleRadius = 45;
304
- this.playerCapsuleRadiusRatio = 1;
305
692
  this.playerCapsuleHeight = 180;
306
- // 玩家参考身高
693
+ this.playerCapsuleRadiusRatio = 1;
307
694
  this.isFirstPerson = false;
308
695
  this.boundingBoxMinY = 0;
309
- // ==================== 测试参数 ====================
310
- this.displayPlayer = false;
311
- this.displayCollider = false;
312
- this.displayVisualizer = false;
313
- // ==================== 场景对象 ====================
696
+ // ==================== 运行状态 ====================
697
+ this.controllerMode = 0;
698
+ // 0:人物 1:车辆
699
+ this.playerIsOnGround = false;
700
+ this.isupdate = true;
701
+ this.isFlying = false;
702
+ this.isChangeControllerTransitionTimer = null;
703
+ // ==================== 输入按键 ====================
704
+ this.fwdPressed = false;
705
+ this.bkdPressed = false;
706
+ this.lftPressed = false;
707
+ this.rgtPressed = false;
708
+ this.spacePressed = false;
709
+ this.ctPressed = false;
710
+ this.shiftPressed = false;
711
+ // ==================== 相机参数 ====================
712
+ this.camCollisionLerp = 0.18;
713
+ this.camEpsilon = 35;
714
+ this.minCamDistance = 100;
715
+ this.maxCamDistance = 440;
716
+ this.orginMaxCamDistance = 440;
717
+ // ==================== 场景碰撞体 ====================
314
718
  this.collider = null;
315
719
  this.visualizer = null;
316
720
  this.person = null;
@@ -318,150 +722,78 @@ var PlayerController = class {
318
722
  this.collected = [];
319
723
  this.dynamicCollider = null;
320
724
  this.dynamicCollected = [];
321
- // ==================== 多车辆相关 ====================
725
+ // ==================== 车辆系统 ====================
322
726
  this.vehicles = [];
323
- // 所有已加载车辆
324
727
  this.activeVehicle = null;
325
- // 当前驾驶/交互的车辆
326
728
  this.vehicleLength = 6;
327
- // 车辆参考长度
328
- this.wheelSteeringQuat = new THREE3.Quaternion();
329
- this.wheelRotationQuat = new THREE3.Quaternion();
330
729
  this.RAPIER = null;
331
730
  this.world = null;
332
- // 全局车辆共享参数
731
+ this.wheelSteeringQuat = new THREE4.Quaternion();
732
+ this.wheelRotationQuat = new THREE4.Quaternion();
733
+ this.camBehindDir = new THREE4.Vector3(0, 0, 1);
333
734
  this.vehicleParams = {
334
- debug: {
335
- showPhysicsBox: false
336
- },
337
- chassis: {
338
- linearDamping: 0.5,
339
- angularDamping: 0.5
340
- },
341
- model: {
342
- rotation: -Math.PI / 2
343
- },
344
- power: {
345
- accelerateForce: 50,
346
- // 推进
347
- brakeForce: 200,
348
- // 刹车
349
- maxSpeed: 1e4
350
- // 最大速度
351
- },
352
- steering: {
353
- maxSteerAngle: Math.PI / 4,
354
- steerSpeed: 0.5,
355
- steerReturnSpeed: 1
356
- },
735
+ debug: { showPhysicsBox: false },
736
+ chassis: { linearDamping: 0.5, angularDamping: 0.5 },
737
+ model: { rotation: -Math.PI / 2 },
738
+ power: { accelerateForce: 50, brakeForce: 200, maxSpeed: 1e4 },
739
+ steering: { maxSteerAngle: Math.PI / 4, steerSpeed: 0.5, steerReturnSpeed: 1 },
357
740
  followVehicleDirection: true
358
741
  };
359
- this.camBehindDir = new THREE3.Vector3(0, 0, 1);
360
- // ==================== 上车相关 ====================
742
+ // ==================== 上车流程 ====================
361
743
  this.isMovingToBoardingPoint = false;
362
744
  this.boardingWaypoints = [];
363
745
  this.currentWaypointIndex = 0;
364
746
  this.boardingTargetDir = null;
365
747
  this.boardingMoveSpeed = 300;
366
748
  this.boardingRotateSpeed = 10;
367
- this.flip180Quat = new THREE3.Quaternion().setFromAxisAngle(
368
- new THREE3.Vector3(0, 1, 0),
369
- Math.PI
370
- );
371
- this.closeVehicleDoorTimer = null;
372
749
  this.boardingPointWorld = null;
373
750
  this.isBoardingAnimPlaying = false;
374
751
  this.closeDoorTriggered = false;
375
752
  this.isExitAnimPlaying = false;
376
753
  this.closeExitDoorTriggered = false;
377
- // ==================== 状态开关 ====================
378
- this.playerIsOnGround = false;
379
- this.isupdate = true;
380
- this.isFlying = false;
381
- // ==================== 输入状态 ====================
382
- this.fwdPressed = false;
383
- this.bkdPressed = false;
384
- this.lftPressed = false;
385
- this.rgtPressed = false;
386
- this.spacePressed = false;
387
- this.ctPressed = false;
388
- this.shiftPressed = false;
389
- // ==================== 移动端输入 ====================
390
- this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
391
- this.nippleModule = null;
392
- this.joystickManager = null;
393
- this.joystickZoneEl = null;
394
- this.lookAreaEl = null;
395
- this.jumpBtnEl = null;
396
- this.flyBtnEl = null;
397
- this.viewBtnEl = null;
398
- this.vehicleBtnEl = null;
399
- this.lookPointerId = null;
400
- this.isLookDown = false;
401
- this.lastTouchX = 0;
402
- this.lastTouchY = 0;
403
- this.nearCheckLocal = new THREE3.Vector3();
404
- this.nearCheckWorld = new THREE3.Vector3();
405
- this.isNearVehicle = false;
406
- // ==================== 第三人称相机参数 ====================
407
- this._camCollisionLerp = 0.18;
408
- this._camEpsilon = 0.35;
409
- this.minCamDistance = 1;
410
- this.maxCamDistance = 4.4;
411
- this.orginMaxCamDistance = 4.4;
412
- // ==================== 物理/运动 ====================
413
- this.playerVelocity = new THREE3.Vector3();
414
- this.upVector = new THREE3.Vector3(0, 1, 0);
415
- // ==================== 临时复用向量/矩阵 ====================
416
- this.tempVector = new THREE3.Vector3();
417
- this.tempVector2 = new THREE3.Vector3();
418
- this.tempBox = new THREE3.Box3();
419
- this.tempMat = new THREE3.Matrix4();
420
- this.tempSegment = new THREE3.Line3();
754
+ this.closeVehicleDoorTimer = null;
755
+ this.flip180Quat = new THREE4.Quaternion().setFromAxisAngle(new THREE4.Vector3(0, 1, 0), Math.PI);
421
756
  this.recheckAnimTimer = null;
422
- // ==================== 相机朝向/移动复用向量 ====================
423
- this.camDir = new THREE3.Vector3();
424
- this.moveDir = new THREE3.Vector3();
425
- this.targetQuat = new THREE3.Quaternion();
426
- this.targetMat = new THREE3.Matrix4();
757
+ this.allAnimations = [];
758
+ // ==================== 移动端控制 ====================
759
+ this.mobileControls = null;
760
+ this.nearCheckLocal = new THREE4.Vector3();
761
+ this.nearCheckWorld = new THREE4.Vector3();
762
+ this.isNearVehicle = false;
763
+ // ==================== 调试显示 ====================
764
+ this.displayPlayer = false;
765
+ this.displayCollider = false;
766
+ this.displayVisualizer = false;
767
+ // ==================== 方向常量 ====================
427
768
  this.rotationSpeed = 10;
428
- this.DIR_FWD = new THREE3.Vector3(0, 0, -1);
429
- this.DIR_BKD = new THREE3.Vector3(0, 0, 1);
430
- this.DIR_LFT = new THREE3.Vector3(-1, 0, 0);
431
- this.DIR_RGT = new THREE3.Vector3(1, 0, 0);
432
- this.DIR_UP = new THREE3.Vector3(0, 1, 0);
769
+ this.DIR_FWD = new THREE4.Vector3(0, 0, -1);
770
+ this.DIR_BKD = new THREE4.Vector3(0, 0, 1);
771
+ this.DIR_LFT = new THREE4.Vector3(-1, 0, 0);
772
+ this.DIR_RGT = new THREE4.Vector3(1, 0, 0);
773
+ this.DIR_UP = new THREE4.Vector3(0, 1, 0);
774
+ // ==================== 复用向量 ====================
775
+ this.upVector = new THREE4.Vector3(0, 1, 0);
776
+ this.playerVelocity = new THREE4.Vector3();
777
+ this.camDir = new THREE4.Vector3();
778
+ this.moveDir = new THREE4.Vector3();
779
+ this.targetQuat = new THREE4.Quaternion();
780
+ this.targetMat = new THREE4.Matrix4();
781
+ this.tempVector = new THREE4.Vector3();
782
+ this.tempVector2 = new THREE4.Vector3();
783
+ this.tempBox = new THREE4.Box3();
784
+ this.tempMat = new THREE4.Matrix4();
785
+ this.tempSegment = new THREE4.Line3();
433
786
  // ==================== 射线检测 ====================
434
- this._personToCam = new THREE3.Vector3();
435
- this._originTmp = new THREE3.Vector3();
436
- this._raycaster = new THREE3.Raycaster(
437
- new THREE3.Vector3(),
438
- new THREE3.Vector3(0, -1, 0)
439
- );
440
- this._raycasterPersonToCam = new THREE3.Raycaster(
441
- new THREE3.Vector3(),
442
- new THREE3.Vector3()
443
- );
444
- this.centerRay = new THREE3.Raycaster();
445
- this.centerMouse = new THREE3.Vector2();
446
- // ==================== 物理与碰撞检测 ====================
447
- this.ensureAttributesMinimal = (geom) => {
448
- if (!geom.attributes.position) return null;
449
- if (!geom.attributes.normal) geom.computeVertexNormals();
450
- if (!geom.attributes.uv) {
451
- const count = geom.attributes.position.count;
452
- const dummyUV = new Float32Array(count * 2);
453
- geom.setAttribute("uv", new THREE3.BufferAttribute(dummyUV, 2));
454
- }
455
- return geom;
456
- };
787
+ this.personToCam = new THREE4.Vector3();
788
+ this.originTmp = new THREE4.Vector3();
789
+ this.raycaster = new THREE4.Raycaster(new THREE4.Vector3(), new THREE4.Vector3(0, -1, 0));
790
+ this.raycasterPersonToCam = new THREE4.Raycaster(new THREE4.Vector3(), new THREE4.Vector3());
791
+ this.centerRay = new THREE4.Raycaster();
792
+ this.centerMouse = new THREE4.Vector2();
793
+ // 按键动画同步
457
794
  this.setAnimationByPressed = () => {
458
795
  this.maxCamDistance = this.orginMaxCamDistance;
459
- if (this.isMovingToBoardingPoint) {
460
- this.isMovingToBoardingPoint = false;
461
- this.boardingWaypoints = [];
462
- this.currentWaypointIndex = 0;
463
- this.boardingTargetDir = null;
464
- }
796
+ this.cancelBoarding();
465
797
  if (this.isExitAnimPlaying) {
466
798
  this.isExitAnimPlaying = false;
467
799
  this.closeExitDoorTriggered = false;
@@ -489,15 +821,11 @@ var PlayerController = class {
489
821
  return;
490
822
  }
491
823
  if (this.fwdPressed) {
492
- this.playPersonAnimationByName(
493
- this.shiftPressed ? "running" : "walking"
494
- );
824
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
495
825
  return;
496
826
  }
497
827
  if (!this.isFirstPerson && (this.lftPressed || this.rgtPressed || this.bkdPressed)) {
498
- this.playPersonAnimationByName(
499
- this.shiftPressed ? "running" : "walking"
500
- );
828
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
501
829
  return;
502
830
  }
503
831
  if (this.lftPressed) {
@@ -513,19 +841,16 @@ var PlayerController = class {
513
841
  return;
514
842
  }
515
843
  }
516
- if (this.recheckAnimTimer !== null) {
517
- clearTimeout(this.recheckAnimTimer);
518
- }
844
+ if (this.recheckAnimTimer !== null) clearTimeout(this.recheckAnimTimer);
519
845
  this.recheckAnimTimer = setTimeout(() => {
520
846
  this.setAnimationByPressed();
521
847
  this.recheckAnimTimer = null;
522
848
  }, 200);
523
849
  };
524
850
  // ==================== 事件处理 ====================
525
- this._boundOnKeydown = async (e) => {
526
- if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) {
527
- e.preventDefault();
528
- }
851
+ // 键盘按下事件
852
+ this.boundOnKeydown = async (e) => {
853
+ if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) e.preventDefault();
529
854
  switch (e.code) {
530
855
  case "KeyW":
531
856
  case "ArrowUp":
@@ -554,17 +879,11 @@ var PlayerController = class {
554
879
  this.controls.mouseButtons = { LEFT: 2, MIDDLE: 1, RIGHT: 0 };
555
880
  break;
556
881
  case "Space":
557
- if (this.isMovingToBoardingPoint) {
558
- this.isMovingToBoardingPoint = false;
559
- this.boardingWaypoints = [];
560
- this.currentWaypointIndex = 0;
561
- this.boardingTargetDir = null;
562
- }
882
+ this.cancelBoarding();
563
883
  this.spacePressed = true;
564
- if (this.controllerMode == 1) return;
884
+ if (this.controllerMode === 1) return;
565
885
  if (!this.playerIsOnGround || this.isFlying) return;
566
- const next = this.personActions?.get("jumping");
567
- if (next && this.actionState === next) return;
886
+ if (this.personActions?.get("jumping") === this.actionState) return;
568
887
  this.playPersonAnimationByName("jumping");
569
888
  this.playerVelocity.y = this.jumpHeight;
570
889
  this.playerIsOnGround = false;
@@ -576,25 +895,21 @@ var PlayerController = class {
576
895
  this.changeView();
577
896
  break;
578
897
  case "KeyF":
579
- if (this.controllerMode == 0 && this.playerFlyEnabled) {
898
+ if (this.controllerMode === 0 && this.playerFlyEnabled) {
580
899
  this.isFlying = !this.isFlying;
581
900
  this.setAnimationByPressed();
582
- if (!this.isFlying && !this.playerIsOnGround) {
583
- this.playPersonAnimationByName("jumping");
584
- }
901
+ if (!this.isFlying && !this.playerIsOnGround) this.playPersonAnimationByName("jumping");
585
902
  }
586
903
  break;
587
904
  case "KeyE":
588
905
  if (this.isFlying) return;
589
- if (this.controllerMode == 0) {
590
- this.enterVehicle();
591
- } else {
592
- this.exitVehicle();
593
- }
906
+ if (this.controllerMode === 0) this.enterVehicle();
907
+ else this.exitVehicle();
594
908
  break;
595
909
  }
596
910
  };
597
- this._boundOnKeyup = (e) => {
911
+ // 键盘抬起事件
912
+ this.boundOnKeyup = (e) => {
598
913
  switch (e.code) {
599
914
  case "KeyW":
600
915
  case "ArrowUp":
@@ -630,192 +945,111 @@ var PlayerController = class {
630
945
  break;
631
946
  }
632
947
  };
633
- this._mouseMove = (e) => {
634
- if (document.pointerLockElement !== document.body) return;
635
- this.setToward(e.movementX, e.movementY, 1e-4);
636
- };
637
- this._mouseClick = (_e) => {
638
- this.setPointerLock();
639
- };
640
- // ==================== 移动端控制 ====================
641
- this.onPointerDown = (e) => {
642
- if (e.pointerType !== "touch") return;
643
- this.isLookDown = true;
644
- this.lookPointerId = e.pointerId;
645
- this.lastTouchX = e.clientX;
646
- this.lastTouchY = e.clientY;
647
- this.lookAreaEl?.setPointerCapture?.(e.pointerId);
648
- e.preventDefault();
649
- };
650
- this.onPointerMove = (e) => {
651
- if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
652
- const dx = e.clientX - this.lastTouchX;
653
- const dy = e.clientY - this.lastTouchY;
654
- this.lastTouchX = e.clientX;
655
- this.lastTouchY = e.clientY;
656
- this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
657
- e.preventDefault();
658
- };
659
- this.onPointerUp = (e) => {
660
- if (e.pointerId !== this.lookPointerId) return;
661
- this.isLookDown = false;
662
- this.lookPointerId = null;
663
- this.lookAreaEl?.releasePointerCapture?.(e.pointerId);
948
+ this.mouseMove = (e) => {
949
+ if (document.pointerLockElement === document.body) this.setToward(e.movementX, e.movementY, 1e-4);
664
950
  };
665
- this._raycaster.firstHitOnly = true;
666
- this._raycasterPersonToCam.firstHitOnly = true;
951
+ this.mouseClick = () => this.setPointerLock();
952
+ this.raycaster.firstHitOnly = true;
953
+ this.raycasterPersonToCam.firstHitOnly = true;
667
954
  }
668
- // ==================== 初始化相关方法 ====================
955
+ // ==================== 初始化 ====================
956
+ // 初始化控制器
669
957
  async init(opts, callback) {
958
+ const m = opts.playerModel;
959
+ const s = m.scale ?? 1;
670
960
  this.scene = opts.scene;
671
961
  this.camera = opts.camera;
672
962
  this.camera.rotation.order = "YXZ";
673
963
  this.controls = opts.controls;
674
- this.playerModel = opts.playerModel;
675
- this.initPos = opts.initPos ?? new THREE3.Vector3(0, 0, 0);
676
- this.mouseSensity = opts.mouseSensity ?? 5;
677
- const s = this.playerModel.scale;
678
- this.gravity = (opts.playerModel.gravity ?? -2400) * s;
679
- this.jumpHeight = (opts.playerModel.jumpHeight ?? 600) * s;
680
- this.playerSpeed = (opts.playerModel.speed ?? 300) * s;
681
- this.playerFlySpeed = (opts.playerModel.flySpeed ?? 2100) * s;
964
+ this.playerModel = m;
965
+ this.initPos = opts.initPos ? new THREE4.Vector3(opts.initPos.x, opts.initPos.y, opts.initPos.z) : new THREE4.Vector3(0, 0, 0);
966
+ const pm = this.playerModel;
967
+ this.gravity = (pm.gravity ?? this.gravity) * s;
968
+ this.jumpHeight = (pm.jumpHeight ?? this.jumpHeight) * s;
969
+ this.playerSpeed = (pm.speed ?? this.playerSpeed) * s;
970
+ this.playerFlySpeed = (pm.flySpeed ?? this.playerFlySpeed) * s;
682
971
  this.curPlayerSpeed = this.playerSpeed;
683
- this.playerModel.rotateY = opts.playerModel.rotateY ?? 0;
684
- this.playerFlyEnabled = opts.playerModel.flyEnabled ?? true;
685
- this._camCollisionLerp = 0.18;
686
- this._camEpsilon = 35 * s;
687
- this.minCamDistance = (opts.minCamDistance ?? 100) * s;
688
- this.maxCamDistance = (opts.maxCamDistance ?? 440) * s;
972
+ this.playerFlyEnabled = pm.flyEnabled ?? this.playerFlyEnabled;
973
+ this.playerCapsuleRadiusRatio = pm.capsuleRadiusRatio ?? this.playerCapsuleRadiusRatio;
974
+ this.mouseSensitivity = opts.mouseSensitivity ?? this.mouseSensitivity;
975
+ this.thirdMouseMode = opts.thirdMouseMode ?? this.thirdMouseMode;
976
+ this.enableZoom = opts.enableZoom ?? this.enableZoom;
977
+ this.minCamDistance = (opts.minCamDistance ?? this.minCamDistance) * s;
978
+ this.maxCamDistance = (opts.maxCamDistance ?? this.maxCamDistance) * s;
689
979
  this.orginMaxCamDistance = this.maxCamDistance;
690
- this.thirdMouseMode = opts.thirdMouseMode ?? 1;
691
- this.enableZoom = opts.enableZoom ?? false;
692
- this.playerCapsuleRadiusRatio = opts.playerModel.capsuleRadiusRatio ?? 1;
693
- const isMobileDevice = () => navigator.maxTouchPoints && navigator.maxTouchPoints > 0 || "ontouchstart" in window || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
694
- this.isShowMobileControls = (opts.isShowMobileControls ?? true) && isMobileDevice();
980
+ this.camEpsilon = this.camEpsilon * s;
981
+ this.isShowMobileControls = (opts.isShowMobileControls ?? this.isShowMobileControls) && isMobileDevice();
982
+ this.enableOverShoulderView = opts.enableOverShoulderView ?? this.enableOverShoulderView;
695
983
  if (this.isShowMobileControls) {
696
- await this.initMobileControls();
984
+ this.mobileControls = new MobileControls((i) => this.setInput(i), this.controls);
985
+ await this.mobileControls.init();
697
986
  }
698
987
  await this.createBVH(opts.colliderMeshUrl);
699
988
  await this.loadPersonGLB();
700
989
  this.onAllEvent();
701
990
  this.setCameraPos();
702
991
  this.setControls();
703
- if (callback) callback();
704
- this.enableOverShoulderView = opts.enableOverShoulderView ?? false;
705
992
  this.setOverShoulderView(this.enableOverShoulderView);
993
+ callback?.();
706
994
  }
995
+ // 过肩视角切换
707
996
  setOverShoulderView(enable) {
708
- if (!enable || this.controllerMode == 1) {
997
+ if (!enable || this.controllerMode === 1) {
709
998
  this.camera.clearViewOffset();
710
999
  return;
711
1000
  }
712
1001
  const w = window.innerWidth;
713
1002
  const h = window.innerHeight;
714
- this.camera.setViewOffset(
715
- w,
716
- h,
717
- -w * -0.15,
718
- 0,
719
- w,
720
- h
721
- );
1003
+ this.camera.setViewOffset(w, h, -w * -0.15, 0, w, h);
722
1004
  }
1005
+ // 初始化加载器
723
1006
  async initLoader() {
724
1007
  const dracoLoader = new DRACOLoader();
725
- dracoLoader.setDecoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/gltf/");
726
- dracoLoader.setDecoderConfig({ type: "js" });
1008
+ dracoLoader.setDecoderPath("https://unpkg.com/three@0.182.0/examples/jsm/libs/draco/gltf/");
727
1009
  this.loader.setDRACOLoader(dracoLoader);
728
1010
  }
1011
+ // 初始化物理引擎
729
1012
  async initRapier() {
730
1013
  if (this.RAPIER) return;
731
1014
  this.RAPIER = await import("@dimforge/rapier3d-compat");
732
1015
  await this.RAPIER.init();
733
- const gravity = new this.RAPIER.Vector3(0, -9.81, 0);
734
- this.world = new this.RAPIER.World(gravity);
1016
+ this.world = new this.RAPIER.World(new this.RAPIER.Vector3(0, -9.81, 0));
735
1017
  this.world.maxCcdSubsteps = 2;
736
- const addGeometryAsTrimesh = (RAPIER, world, geom) => {
737
- let geometry = geom.index ? geom.clone().toNonIndexed() : geom.clone();
738
- const posAttr = geometry.attributes.position;
739
- const vertexCount = posAttr.count;
740
- if (vertexCount % 3 !== 0) {
741
- console.warn("\u9876\u70B9\u6570\u4E0D\u662F3\u7684\u500D\u6570\uFF0C\u4E09\u89D2\u5F62\u53EF\u80FD\u4E0D\u5B8C\u6574");
742
- }
743
- const vertices = new Float32Array(vertexCount * 3);
744
- const tmp = new THREE3.Vector3();
745
- for (let i = 0; i < vertexCount; i++) {
746
- tmp.fromBufferAttribute(posAttr, i);
747
- vertices[i * 3 + 0] = tmp.x;
748
- vertices[i * 3 + 1] = tmp.y;
749
- vertices[i * 3 + 2] = tmp.z;
1018
+ const addTrimesh = (RAPIER, world, geom) => {
1019
+ let g = geom.index ? geom.clone().toNonIndexed() : geom.clone();
1020
+ const pos = g.attributes.position;
1021
+ const count = pos.count;
1022
+ const verts = new Float32Array(count * 3);
1023
+ const tmp = new THREE4.Vector3();
1024
+ for (let i = 0; i < count; i++) {
1025
+ tmp.fromBufferAttribute(pos, i);
1026
+ verts[i * 3] = tmp.x;
1027
+ verts[i * 3 + 1] = tmp.y;
1028
+ verts[i * 3 + 2] = tmp.z;
750
1029
  }
751
- const indices = vertexCount > 65535 ? new Uint32Array(vertexCount) : new Uint16Array(vertexCount);
752
- for (let i = 0; i < vertexCount; i++) indices[i] = i;
753
- const bodyDesc = RAPIER.RigidBodyDesc.fixed();
754
- const body = world.createRigidBody(bodyDesc);
755
- const colliderDesc = RAPIER.ColliderDesc.trimesh(vertices, indices).setRestitution(0).setFriction(0.8);
756
- world.createCollider(colliderDesc, body);
1030
+ const indices = count > 65535 ? new Uint32Array(count) : new Uint16Array(count);
1031
+ for (let i = 0; i < count; i++) indices[i] = i;
1032
+ const body = world.createRigidBody(RAPIER.RigidBodyDesc.fixed());
1033
+ world.createCollider(
1034
+ RAPIER.ColliderDesc.trimesh(verts, indices).setRestitution(0).setFriction(0.8),
1035
+ body
1036
+ );
757
1037
  };
758
- for (const g of this.collected) {
759
- addGeometryAsTrimesh(this.RAPIER, this.world, g);
760
- }
761
- const groundDesc = this.RAPIER.RigidBodyDesc.fixed();
762
- const groundBody = this.world.createRigidBody(groundDesc);
1038
+ for (const g of this.collected) addTrimesh(this.RAPIER, this.world, g);
1039
+ const groundBody = this.world.createRigidBody(this.RAPIER.RigidBodyDesc.fixed());
763
1040
  groundBody.userData = { outOfBounds: true };
764
1041
  }
765
- // ==================== 玩家模型相关方法 ====================
1042
+ // ==================== 玩家模型 ====================
1043
+ // 加载玩家模型
766
1044
  async loadPersonGLB() {
767
1045
  try {
768
- const gltf = await this.loader.loadAsync(
769
- this.playerModel.url
770
- );
1046
+ const gltf = await this.loader.loadAsync(this.playerModel.url);
771
1047
  this.person = gltf.scene;
772
- const { size } = this.getBbox(this.person);
773
- const ratio = this.playerCapsuleHeight / size.y;
774
- const modelScale = ratio;
775
- this.playerCapsuleRadius = Number(Math.min(size.x, size.z).toFixed(0)) * modelScale * this.playerCapsuleRadiusRatio;
776
- this.playerCapsuleHeight = Number(size.y.toFixed(0)) * modelScale;
777
- const scale = this.playerModel.scale;
778
- const material = new THREE3.MeshStandardMaterial({
779
- color: new THREE3.Color(1, 0, 0),
780
- shadowSide: THREE3.DoubleSide,
781
- depthTest: false,
782
- transparent: true,
783
- opacity: this.displayPlayer ? 0.5 : 0,
784
- wireframe: true,
785
- depthWrite: false
786
- });
787
- const r = this.playerCapsuleRadius * scale;
788
- const h = this.playerCapsuleHeight * scale;
789
- this.player = new THREE3.Mesh(
790
- new RoundedBoxGeometry(r * 2, h, r * 2, 1, 75),
791
- material
792
- );
793
- this.player.geometry.translate(0, -h * 0.25, 0);
794
- this.player.capsuleInfo = {
795
- radius: r,
796
- segment: new THREE3.Line3(
797
- new THREE3.Vector3(),
798
- new THREE3.Vector3(0, -h * 0.5, 0)
799
- )
800
- };
801
- this.player.name = "capsule";
802
- this.scene.add(this.player);
803
- this.reset();
804
- this.player.rotateY(this.playerModel.rotateY ?? 0);
805
- this.person.scale.multiplyScalar(modelScale * scale);
806
- this.person.position.set(0, -h * 0.75, 0);
807
- this.person.traverse((child) => {
808
- if (child.name == this.playerModel?.headObjName) {
809
- this.personHead = child;
810
- }
811
- });
812
- this.player.add(this.person);
813
- this.reset();
814
- this.personMixer = new THREE3.AnimationMixer(this.person);
1048
+ this.personMixer = new THREE4.AnimationMixer(this.person);
815
1049
  const animations = gltf.animations ?? [];
816
- console.log("animations", animations);
1050
+ this.allAnimations = animations;
817
1051
  this.personActions = /* @__PURE__ */ new Map();
818
- const animationMappings = [
1052
+ const mappings = [
819
1053
  [this.playerModel.idleAnim, "idle"],
820
1054
  [this.playerModel.walkAnim, "walking"],
821
1055
  [this.playerModel.leftWalkAnim || this.playerModel.walkAnim, "left_walking"],
@@ -828,43 +1062,30 @@ var PlayerController = class {
828
1062
  [this.playerModel.enterCarAnim || this.playerModel.idleAnim, "enterCar"],
829
1063
  [this.playerModel.exitCarAnim || this.playerModel.idleAnim, "exitCar"]
830
1064
  ];
831
- const findClip = (name) => animations.find((a) => a.name === name);
832
- for (const [clipName, actionName] of animationMappings) {
833
- const clip = findClip(clipName);
1065
+ for (const [clipName, actionName] of mappings) {
1066
+ const clip = animations.find((a) => a.name === clipName);
834
1067
  if (!clip) continue;
835
1068
  const action = this.personMixer.clipAction(clip);
836
1069
  if (actionName === "jumping") {
837
- action.setLoop(THREE3.LoopOnce, 1);
1070
+ action.setLoop(THREE4.LoopOnce, 1);
838
1071
  action.clampWhenFinished = true;
839
1072
  action.setEffectiveTimeScale(1.2);
840
1073
  } else {
841
- action.setLoop(THREE3.LoopRepeat, Infinity);
842
- action.clampWhenFinished = false;
1074
+ action.setLoop(THREE4.LoopRepeat, Infinity);
843
1075
  action.setEffectiveTimeScale(1);
844
1076
  }
845
1077
  action.enabled = true;
846
1078
  action.setEffectiveWeight(0);
847
1079
  this.personActions.set(actionName, action);
848
1080
  }
849
- this.idleAction = this.personActions.get("idle");
850
- this.walkAction = this.personActions.get("walking");
851
- this.leftWalkAction = this.personActions.get("left_walking");
852
- this.rightWalkAction = this.personActions.get("right_walking");
853
- this.backwardAction = this.personActions.get("walking_backward");
854
- this.jumpAction = this.personActions.get("jumping");
855
- this.runAction = this.personActions.get("running");
856
- this.flyidleAction = this.personActions.get("flyidle");
857
- this.flyAction = this.personActions.get("flying");
858
- this.idleAction.setEffectiveWeight(1);
859
- this.idleAction.play();
860
- this.actionState = this.idleAction;
1081
+ this.personActions.get("idle")?.setEffectiveWeight(1);
1082
+ this.personActions.get("idle")?.play();
1083
+ this.actionState = this.personActions.get("idle");
861
1084
  this.personMixer.addEventListener("finished", (ev) => {
862
- const finishedAction = ev.action;
863
- if (finishedAction === this.jumpAction) {
1085
+ const done = ev.action;
1086
+ if (done === this.personActions?.get("jumping")) {
864
1087
  if (this.fwdPressed) {
865
- this.playPersonAnimationByName(
866
- this.shiftPressed ? "running" : "walking"
867
- );
1088
+ this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
868
1089
  return;
869
1090
  }
870
1091
  if (this.bkdPressed) {
@@ -877,26 +1098,56 @@ var PlayerController = class {
877
1098
  }
878
1099
  this.playPersonAnimationByName("idle");
879
1100
  }
880
- if (finishedAction === this.personActions?.get("enterCar")) {
881
- this.onEnterCarAnimFinished();
882
- }
883
- if (finishedAction === this.personActions?.get("exitCar")) {
884
- }
1101
+ if (done === this.personActions?.get("enterCar")) this.onEnterCarAnimFinished();
1102
+ });
1103
+ this.personMixer.update(0);
1104
+ this.person.updateMatrixWorld(true);
1105
+ const { size } = this.getBbox(this.person);
1106
+ const modelScale = this.playerCapsuleHeight / size.y;
1107
+ this.playerCapsuleRadius = Number(Math.min(size.x, size.z).toFixed(0)) * modelScale * this.playerCapsuleRadiusRatio;
1108
+ this.playerCapsuleHeight = Number(size.y.toFixed(0)) * modelScale;
1109
+ const s = this.playerModel.scale;
1110
+ const r = this.playerCapsuleRadius * s;
1111
+ const h = this.playerCapsuleHeight * s;
1112
+ this.player = new THREE4.Mesh(
1113
+ new RoundedBoxGeometry(r * 2, h, r * 2, 1, 75),
1114
+ new THREE4.MeshStandardMaterial({
1115
+ color: new THREE4.Color(1, 0, 0),
1116
+ shadowSide: THREE4.DoubleSide,
1117
+ depthTest: false,
1118
+ transparent: true,
1119
+ opacity: this.displayPlayer ? 0.5 : 0,
1120
+ wireframe: true,
1121
+ depthWrite: false
1122
+ })
1123
+ );
1124
+ this.player.geometry.translate(0, -h * 0.25, 0);
1125
+ this.player.capsuleInfo = {
1126
+ radius: r,
1127
+ segment: new THREE4.Line3(new THREE4.Vector3(), new THREE4.Vector3(0, -h * 0.5, 0))
1128
+ };
1129
+ this.player.name = "capsule";
1130
+ this.scene.add(this.player);
1131
+ this.reset();
1132
+ this.player.rotateY(this.playerModel.rotateY ?? 0);
1133
+ this.person.scale.multiplyScalar(modelScale * s);
1134
+ this.person.position.set(0, -h * 0.75, 0);
1135
+ this.person.traverse((child) => {
1136
+ if (child.name === this.playerModel?.headObjName) this.personHead = child;
885
1137
  });
886
- } catch (error) {
887
- console.error("\u52A0\u8F7D\u73A9\u5BB6\u6A21\u578B\u5931\u8D25:", error);
1138
+ this.player.add(this.person);
1139
+ this.reset();
1140
+ } catch (e) {
1141
+ console.error("\u52A0\u8F7D\u73A9\u5BB6\u6A21\u578B\u5931\u8D25:", e);
888
1142
  }
889
1143
  }
1144
+ // 切换玩家模型
890
1145
  async switchPlayerModel(newPlayerModel) {
891
1146
  const savedPos = this.player.position.clone();
892
1147
  const savedQuat = this.player.quaternion.clone();
893
1148
  const wasFirstPerson = this.isFirstPerson;
894
- if (wasFirstPerson) {
895
- this.scene.attach(this.camera);
896
- }
897
- if (this.player) {
898
- this.scene.remove(this.player);
899
- }
1149
+ if (wasFirstPerson) this.scene.attach(this.camera);
1150
+ if (this.player) this.scene.remove(this.player);
900
1151
  if (this.person) {
901
1152
  this.player.remove(this.person);
902
1153
  this.person = null;
@@ -915,45 +1166,76 @@ var PlayerController = class {
915
1166
  this.playerSpeed *= ratio;
916
1167
  this.playerFlySpeed *= ratio;
917
1168
  this.curPlayerSpeed *= ratio;
918
- this._camEpsilon *= ratio;
1169
+ this.camEpsilon *= ratio;
919
1170
  this.minCamDistance *= ratio;
920
1171
  this.maxCamDistance *= ratio;
921
1172
  this.orginMaxCamDistance *= ratio;
922
1173
  await this.loadPersonGLB();
923
1174
  this.player.position.copy(savedPos);
924
1175
  this.player.quaternion.copy(savedQuat);
925
- if (wasFirstPerson) {
926
- this.setFirstPersonCamera();
927
- }
1176
+ if (wasFirstPerson) this.setFirstPersonCamera();
928
1177
  this.setDebug(this.displayCollider);
929
1178
  }
1179
+ // 播放动画
930
1180
  playPersonAnimationByName(name, fade = 0.18) {
931
1181
  if (!this.personActions || this.ctPressed) return;
932
1182
  const next = this.personActions.get(name);
933
1183
  if (!next || this.actionState === next) return;
934
- const duration = next.getClip().duration;
935
1184
  const prev = this.actionState;
936
1185
  next.reset();
937
1186
  next.setEffectiveWeight(1);
938
- if (name == "enterCar" || name == "exitCar") {
1187
+ if (name === "enterCar" || name === "exitCar") {
1188
+ const duration = next.getClip().duration;
939
1189
  const enterTime = this.activeVehicle?.enterVehicleTime ?? 1.5;
940
1190
  next.setEffectiveTimeScale(duration / enterTime);
941
- next.setLoop(THREE3.LoopOnce, 1);
1191
+ next.setLoop(THREE4.LoopOnce, 1);
942
1192
  next.clampWhenFinished = true;
943
1193
  }
944
1194
  next.play();
945
1195
  if (prev && prev !== next) {
946
1196
  prev.fadeOut(fade);
947
1197
  next.fadeIn(fade);
948
- } else {
949
- next.fadeIn(fade);
950
- }
1198
+ } else next.fadeIn(fade);
951
1199
  this.actionState = next;
952
1200
  }
953
- // ==================== 车辆模型相关 ====================
954
- /**
955
- * 加载车辆模型
956
- */
1201
+ // 注册自定义动画
1202
+ registerAnimation(key, clipName, opts) {
1203
+ if (!this.personMixer || !this.personActions) return;
1204
+ const mixer = this.personMixer;
1205
+ const clip = this.allAnimations.find((c) => c.name === clipName);
1206
+ if (!clip) {
1207
+ console.warn(`\u627E\u4E0D\u5230 "${clipName}" \u52A8\u753B`);
1208
+ return;
1209
+ }
1210
+ const action = mixer.clipAction(clip);
1211
+ const timeScale = opts?.duration ? clip.duration / opts.duration : opts?.timeScale ?? 1;
1212
+ action.setLoop(opts?.loop === false ? THREE4.LoopOnce : THREE4.LoopRepeat, Infinity);
1213
+ action.clampWhenFinished = opts?.clampWhenFinished ?? false;
1214
+ action.setEffectiveTimeScale(timeScale);
1215
+ action.enabled = true;
1216
+ action.setEffectiveWeight(0);
1217
+ this.personActions.set(key, action);
1218
+ if (opts?.onFinished) {
1219
+ this.personMixer.addEventListener("finished", (ev) => {
1220
+ if (ev.action === action) opts.onFinished();
1221
+ });
1222
+ }
1223
+ }
1224
+ // 外部播放动画
1225
+ playAnimation(key, opts) {
1226
+ if (!this.personActions) return;
1227
+ if (!this.personActions.has(key)) {
1228
+ console.warn(`playAnimation: "${key}" \u672A\u6CE8\u518C`);
1229
+ return;
1230
+ }
1231
+ if (opts?.force) {
1232
+ const action = this.personActions.get(key);
1233
+ action.reset();
1234
+ }
1235
+ this.playPersonAnimationByName(key, opts?.fade ?? 0.18);
1236
+ }
1237
+ // ==================== 车辆 ====================
1238
+ // 加载车辆模型
957
1239
  async loadVehicleModel(opts) {
958
1240
  try {
959
1241
  if (!this.playerModel.enterCarAnim) {
@@ -961,287 +1243,31 @@ var PlayerController = class {
961
1243
  }
962
1244
  await this.initRapier();
963
1245
  if (!this.world) return;
964
- const scale = opts.scale ?? 1;
965
- const chassisRatio = opts.chassisRatio ?? 0.2;
966
- const suspensionRestLengthRatio = opts.suspensionRestLengthRatio ?? 0.2;
967
- const speedMultiplier = opts.speedMultiplier ?? 1;
968
- const followVehicleDirection = opts.followVehicleDirection ?? true;
969
- this.vehicleParams.power.accelerateForce = 50 * scale;
970
- this.vehicleParams.power.brakeForce = 200 * scale;
971
- this.vehicleParams.power.maxSpeed = 1e4 * scale;
972
- this.vehicleParams.followVehicleDirection = followVehicleDirection;
973
- const vehicleModel = await this.loader.loadAsync(opts.url);
974
- const { size: originalSize } = this.getBbox(vehicleModel.scene);
975
- const ratio = this.vehicleLength / Math.max(originalSize.x, originalSize.y, originalSize.z);
976
- const modelScale = ratio;
977
- const vehicleMixer = new THREE3.AnimationMixer(vehicleModel.scene);
978
- const animations = vehicleModel.animations ?? [];
979
- const vehicleActions = /* @__PURE__ */ new Map();
980
- const findClip = (name) => animations.find((a) => a.name === name);
981
- const openDoorClip = findClip(opts.animations?.openDoorAnim || "");
982
- if (openDoorClip) {
983
- const action = vehicleMixer.clipAction(openDoorClip);
984
- action.setLoop(THREE3.LoopOnce, 1);
985
- action.clampWhenFinished = true;
986
- action.setEffectiveTimeScale(openDoorClip.duration);
987
- action.enabled = true;
988
- action.setEffectiveWeight(0);
989
- vehicleActions.set("openDoor", action);
990
- }
991
- const wheelObjects = [];
992
- for (const wheelName of opts.wheelsNames) {
993
- let found = false;
994
- vehicleModel.scene.traverse((child) => {
995
- if (child.name === wheelName && !found) {
996
- wheelObjects.push(child);
997
- found = true;
998
- }
999
- });
1000
- if (!found) console.warn(`\u672A\u627E\u5230\u8F6E\u5B50: ${wheelName}`);
1001
- }
1002
- const tempGroup = new THREE3.Group();
1003
- this.scene.add(tempGroup);
1004
- vehicleModel.scene.scale.multiplyScalar(modelScale * scale);
1005
- vehicleModel.scene.rotateY(this.vehicleParams.model.rotation);
1006
- const { size, bbox, center } = this.getBbox(vehicleModel.scene);
1007
- vehicleModel.scene.position.set(-center.x, -center.y, -center.z);
1008
- tempGroup.add(vehicleModel.scene);
1009
- tempGroup.updateMatrixWorld(true);
1010
- const wheelsInfo = [];
1011
- let wheelRadius = 0;
1012
- let wheelWidth = 0;
1013
- let suspensionRestLength = 0;
1014
- let chassisHeight = 0;
1015
- let wheelSizeInit = false;
1016
- for (let i = 0; i < wheelObjects.length; i++) {
1017
- const wheel = wheelObjects[i];
1018
- const worldPos = new THREE3.Vector3();
1019
- const worldQuat = new THREE3.Quaternion();
1020
- const worldScale = new THREE3.Vector3();
1021
- wheel.getWorldPosition(worldPos);
1022
- wheel.getWorldQuaternion(worldQuat);
1023
- wheel.getWorldScale(worldScale);
1024
- if (!wheelSizeInit) {
1025
- const { size: ws } = this.getBbox(wheel);
1026
- wheelRadius = Number((Math.max(ws.x, ws.y, ws.z) / 2).toFixed(2));
1027
- wheelWidth = Number(Math.min(ws.x, ws.y, ws.z).toFixed(2));
1028
- suspensionRestLength = Number((wheelRadius * 2 * suspensionRestLengthRatio).toFixed(2));
1029
- chassisHeight = Number((wheelRadius * 2 * chassisRatio).toFixed(2));
1030
- wheelSizeInit = true;
1031
- }
1032
- wheelsInfo.push({
1033
- axleCs: new THREE3.Vector3(0, 0, -1),
1034
- position: worldPos,
1035
- quaternion: worldQuat,
1036
- scale: worldScale,
1037
- radius: wheelRadius,
1038
- width: wheelWidth,
1039
- suspensionRestLength,
1040
- object: wheel
1041
- });
1042
- }
1043
- tempGroup.remove(vehicleModel.scene);
1044
- this.scene.remove(tempGroup);
1045
- const vehicleGroup = new THREE3.Group();
1046
- this.scene.add(vehicleGroup);
1047
- vehicleGroup.add(vehicleModel.scene);
1048
- vehicleGroup.updateMatrixWorld(true);
1049
- const wheelWrappers = [];
1050
- for (let i = 0; i < wheelsInfo.length; i++) {
1051
- const wheel = wheelsInfo[i];
1052
- const localPos = vehicleGroup.worldToLocal(wheel.position.clone());
1053
- const wheelWrapper = new THREE3.Group();
1054
- wheelWrapper.position.copy(localPos);
1055
- const wheelObj = wheelsInfo[i].object;
1056
- if (wheelObj.parent) wheelObj.parent.remove(wheelObj);
1057
- wheelObj.position.set(0, 0, 0);
1058
- wheelObj.quaternion.copy(wheel.quaternion);
1059
- wheelObj.scale.copy(wheel.scale);
1060
- wheelObj.updateMatrixWorld();
1061
- wheelWrapper.add(wheelObj);
1062
- vehicleGroup.add(wheelWrapper);
1063
- wheelWrappers.push(wheelWrapper);
1064
- }
1065
- const halfExtents = size.clone().multiplyScalar(0.5);
1066
- halfExtents.y -= chassisHeight / 2;
1067
- vehicleModel.scene.position.y -= chassisHeight / 2;
1068
- halfExtents.x *= 0.95;
1069
- halfExtents.z *= 0.95;
1070
- const chassisDesc = this.RAPIER.RigidBodyDesc.dynamic().setTranslation(
1071
- opts.position.x,
1072
- opts.position.y,
1073
- opts.position.z
1074
- ).setLinearDamping(this.vehicleParams.chassis.linearDamping).setAngularDamping(this.vehicleParams.chassis.angularDamping).setCanSleep(true).setAdditionalMass(10);
1075
- const chassisBody = this.world.createRigidBody(chassisDesc);
1076
- const chassisCollider = this.RAPIER.ColliderDesc.cuboid(
1077
- halfExtents.x,
1078
- halfExtents.y,
1079
- halfExtents.z
1080
- );
1081
- this.world.createCollider(chassisCollider, chassisBody);
1082
- if (this.vehicleParams.debug.showPhysicsBox) {
1083
- const debugBox = new THREE3.Mesh(
1084
- new THREE3.BoxGeometry(
1085
- halfExtents.x * 2,
1086
- halfExtents.y * 2,
1087
- halfExtents.z * 2
1088
- ),
1089
- new THREE3.MeshBasicMaterial({
1090
- color: 16711680,
1091
- wireframe: true,
1092
- transparent: true,
1093
- opacity: 0.3
1094
- })
1095
- );
1096
- vehicleGroup.add(debugBox);
1097
- }
1098
- vehicleGroup.position.copy(opts.position);
1099
- vehicleGroup.updateMatrixWorld(true);
1100
- const { vehicle, updateWheelVisuals } = createVehicleController(
1101
- this.world,
1102
- chassisBody,
1103
- wheelWrappers,
1104
- wheelsInfo
1105
- );
1106
- const vehicleInstance = {
1107
- vehicleGroup,
1108
- chassisBody,
1109
- vehicleController: vehicle,
1110
- updateWheelVisuals,
1111
- vehicleMixer,
1112
- vehicleActions,
1113
- vehiclIsOpenDoor: false,
1114
- vehicleBBox: bbox.clone(),
1115
- pathPlanner: new PathPlanner(
1116
- this._createObstacleCheckerFor(vehicleGroup, bbox, scale),
1117
- {
1118
- debugEnabled: false,
1119
- scene: this.scene,
1120
- scale: this.playerModel.scale
1121
- }
1122
- ),
1123
- scale,
1124
- boardingPoint: opts.boardingPoint,
1125
- seatOffset: opts.seatOffset ?? new THREE3.Vector3(0, 0, 0),
1126
- enterVehicleTime: 1.5,
1127
- chassisRatio,
1128
- suspensionRestLengthRatio,
1129
- size: {
1130
- l: Math.max(size.x, size.z),
1131
- w: Math.min(size.x, size.z),
1132
- h: size.y
1133
- },
1134
- speedMultiplier
1135
- };
1136
- this.vehicles.push(vehicleInstance);
1246
+ const instance = await loadVehicleModel(opts, {
1247
+ loader: this.loader,
1248
+ scene: this.scene,
1249
+ world: this.world,
1250
+ RAPIER: this.RAPIER,
1251
+ vehicleParams: this.vehicleParams,
1252
+ vehicleLength: this.vehicleLength,
1253
+ playerScale: this.playerModel.scale
1254
+ });
1255
+ this.vehicles.push(instance);
1137
1256
  this.setControllerTransition();
1138
- } catch (error) {
1139
- console.error("\u52A0\u8F7D\u8F66\u8F86\u6A21\u578B\u5931\u8D25:", error);
1257
+ } catch (e) {
1258
+ console.error("\u52A0\u8F7D\u8F66\u8F86\u6A21\u578B\u5931\u8D25:", e);
1140
1259
  }
1141
1260
  }
1261
+ // 获取包围盒
1142
1262
  getBbox(object) {
1143
- const bbox = new THREE3.Box3().setFromObject(object);
1144
- const center = new THREE3.Vector3();
1145
- const size = new THREE3.Vector3();
1263
+ const bbox = new THREE4.Box3().setFromObject(object);
1264
+ const center = new THREE4.Vector3();
1265
+ const size = new THREE4.Vector3();
1146
1266
  bbox.getCenter(center);
1147
1267
  bbox.getSize(size);
1148
1268
  return { bbox, center, size };
1149
1269
  }
1150
- /**
1151
- * 为指定车辆创建障碍物检测器
1152
- */
1153
- _createObstacleCheckerFor(vehicleGroup, bbox, scale) {
1154
- return {
1155
- isBlocked: (start, end) => {
1156
- const vehiclePos = vehicleGroup.position;
1157
- const vehicleQuat = vehicleGroup.quaternion;
1158
- const center = new THREE3.Vector3();
1159
- const size = new THREE3.Vector3();
1160
- bbox.getCenter(center);
1161
- bbox.getSize(size);
1162
- center.applyQuaternion(vehicleQuat).add(vehiclePos);
1163
- const halfSize = size.clone().multiplyScalar(0.5 * scale);
1164
- const corners = [];
1165
- for (let x = -1; x <= 1; x += 2) {
1166
- for (let y = -1; y <= 1; y += 2) {
1167
- for (let z = -1; z <= 1; z += 2) {
1168
- const localCorner = new THREE3.Vector3(
1169
- halfSize.x * x,
1170
- halfSize.y * y,
1171
- halfSize.z * z
1172
- );
1173
- const worldCorner = localCorner.applyQuaternion(vehicleQuat).add(center);
1174
- corners.push(worldCorner);
1175
- }
1176
- }
1177
- }
1178
- const expandedBBox = new THREE3.Box3();
1179
- corners.forEach((corner) => expandedBBox.expandByPoint(corner));
1180
- expandedBBox.expandByScalar(100 * this.playerModel.scale);
1181
- const direction = new THREE3.Vector3().subVectors(end, start);
1182
- const length = direction.length();
1183
- direction.normalize();
1184
- const ray = new THREE3.Ray(start, direction);
1185
- const intersection = new THREE3.Vector3();
1186
- const intersects = ray.intersectBox(expandedBBox, intersection);
1187
- return intersects !== null && start.distanceTo(intersection) < length;
1188
- },
1189
- getNavigationNodes: (start, _goal) => {
1190
- const nodes = [];
1191
- const vehiclePos = vehicleGroup.position;
1192
- const vehicleQuat = vehicleGroup.quaternion;
1193
- const vehicleForward = new THREE3.Vector3(
1194
- 0,
1195
- 0,
1196
- 1
1197
- ).applyQuaternion(vehicleQuat);
1198
- const vehicleRight = new THREE3.Vector3(1, 0, 0).applyQuaternion(
1199
- vehicleQuat
1200
- );
1201
- const bboxSize = new THREE3.Vector3();
1202
- bbox.getSize(bboxSize);
1203
- const halfLength = bboxSize.z / 2 * scale;
1204
- const halfWidth = bboxSize.x / 2 * scale;
1205
- const bypassMargin = 300 * this.playerModel.scale;
1206
- const extendedMargin = 500 * this.playerModel.scale;
1207
- const groundY = start.y;
1208
- for (const margin of [bypassMargin, extendedMargin]) {
1209
- nodes.push(
1210
- vehiclePos.clone().add(
1211
- vehicleForward.clone().multiplyScalar(halfLength + margin)
1212
- ).add(
1213
- vehicleRight.clone().multiplyScalar(-halfWidth - margin)
1214
- ).setY(groundY)
1215
- );
1216
- nodes.push(
1217
- vehiclePos.clone().add(
1218
- vehicleForward.clone().multiplyScalar(halfLength + margin)
1219
- ).add(
1220
- vehicleRight.clone().multiplyScalar(halfWidth + margin)
1221
- ).setY(groundY)
1222
- );
1223
- nodes.push(
1224
- vehiclePos.clone().add(
1225
- vehicleForward.clone().multiplyScalar(-halfLength - margin)
1226
- ).add(
1227
- vehicleRight.clone().multiplyScalar(-halfWidth - margin)
1228
- ).setY(groundY)
1229
- );
1230
- nodes.push(
1231
- vehiclePos.clone().add(
1232
- vehicleForward.clone().multiplyScalar(-halfLength - margin)
1233
- ).add(
1234
- vehicleRight.clone().multiplyScalar(halfWidth + margin)
1235
- ).setY(groundY)
1236
- );
1237
- }
1238
- return nodes;
1239
- }
1240
- };
1241
- }
1242
- /**
1243
- * 开关车门动画(操作当前 activeVehicle)
1244
- */
1270
+ // 开关车门动画
1245
1271
  openVehicleDoor(isOpen = true) {
1246
1272
  const v = this.activeVehicle;
1247
1273
  if (!v?.vehicleActions) return;
@@ -1259,112 +1285,70 @@ var PlayerController = class {
1259
1285
  next.time = duration;
1260
1286
  v.vehiclIsOpenDoor = false;
1261
1287
  }
1262
- next.setLoop(THREE3.LoopOnce, 1);
1288
+ next.setLoop(THREE4.LoopOnce, 1);
1263
1289
  next.clampWhenFinished = true;
1264
1290
  next.play();
1265
1291
  }
1266
- /**
1267
- * 上车:自动寻找最近的车辆
1268
- */
1292
+ // 触发上车流程
1269
1293
  enterVehicle() {
1270
- if (this.vehicles.length === 0 || this.isMovingToBoardingPoint) return;
1294
+ if (!this.vehicles.length || this.isMovingToBoardingPoint) return;
1271
1295
  let nearestVehicle = null;
1272
1296
  let nearestDist = Infinity;
1273
1297
  let nearBoardingPointWorld = null;
1274
1298
  for (const v2 of this.vehicles) {
1275
- const boardingPointLocal = v2.boardingPoint.clone().multiplyScalar(v2.scale);
1276
- const boardingPointWorld = new THREE3.Vector3();
1277
- v2.vehicleGroup.localToWorld(
1278
- boardingPointWorld.copy(boardingPointLocal)
1279
- );
1280
- const dist = this.player.position.distanceTo(boardingPointWorld);
1299
+ const boardingLocal = v2.boardingPoint.clone().multiplyScalar(v2.scale);
1300
+ const boardingWorld = v2.vehicleGroup.localToWorld(boardingLocal);
1301
+ const dist = this.player.position.distanceTo(boardingWorld);
1281
1302
  if (dist < 800 * this.playerModel.scale && dist < nearestDist) {
1282
1303
  nearestDist = dist;
1283
1304
  nearestVehicle = v2;
1284
- nearBoardingPointWorld = boardingPointWorld;
1305
+ nearBoardingPointWorld = boardingWorld;
1285
1306
  }
1286
1307
  }
1287
1308
  if (!nearestVehicle || !nearBoardingPointWorld) return;
1288
1309
  this.activeVehicle = nearestVehicle;
1289
1310
  const v = nearestVehicle;
1290
1311
  const vel = v.chassisBody.linvel();
1291
- const horizSpeed = Math.sqrt(vel.x * vel.x + vel.z * vel.z);
1292
- if (horizSpeed > 0.1) return;
1312
+ if (Math.sqrt(vel.x ** 2 + vel.z ** 2) > 0.1) return;
1293
1313
  this.boardingPointWorld = nearBoardingPointWorld;
1294
- const vehicleForward = new THREE3.Vector3(0, 0, 1).applyQuaternion(v.vehicleGroup.quaternion).normalize();
1295
- const path = v.pathPlanner.findPath(
1296
- this.player.position.clone(),
1297
- this.boardingPointWorld
1298
- );
1299
- this.boardingWaypoints = path;
1314
+ this.boardingWaypoints = v.pathPlanner.findPath(this.player.position.clone(), nearBoardingPointWorld);
1300
1315
  this.currentWaypointIndex = 0;
1301
- this.boardingTargetDir = vehicleForward;
1316
+ this.boardingTargetDir = new THREE4.Vector3(0, 0, 1).applyQuaternion(v.vehicleGroup.quaternion).normalize();
1302
1317
  this.isMovingToBoardingPoint = true;
1303
1318
  this.playPersonAnimationByName("walking");
1304
1319
  }
1305
- /**
1306
- * 走向上车点
1307
- */
1320
+ // 寻路移动到上车点
1308
1321
  updateMoveToBoardingPoint(delta) {
1309
- if (!this.isMovingToBoardingPoint || !this.boardingTargetDir || this.boardingWaypoints.length === 0) {
1310
- return;
1311
- }
1322
+ if (!this.isMovingToBoardingPoint || !this.boardingTargetDir || !this.boardingWaypoints.length) return;
1312
1323
  if (this.currentWaypointIndex >= this.boardingWaypoints.length) {
1313
1324
  this.finalizeBoarding(delta);
1314
1325
  return;
1315
1326
  }
1316
- const currentWaypoint = this.boardingWaypoints[this.currentWaypointIndex];
1327
+ const waypoint = this.boardingWaypoints[this.currentWaypointIndex];
1317
1328
  const currentPos = this.player.position.clone();
1318
- const horizontalDistance = new THREE3.Vector2(
1319
- currentWaypoint.x - currentPos.x,
1320
- currentWaypoint.z - currentPos.z
1321
- ).length();
1322
- const isLastWaypoint = this.currentWaypointIndex === this.boardingWaypoints.length - 1;
1323
- const waypointThreshold = isLastWaypoint ? 0 : 10 * this.playerModel.scale;
1324
- if (horizontalDistance > waypointThreshold) {
1325
- const moveDir = new THREE3.Vector3(
1326
- currentWaypoint.x - currentPos.x,
1327
- 0,
1328
- currentWaypoint.z - currentPos.z
1329
- ).normalize();
1330
- const moveDistance = Math.min(
1331
- this.boardingMoveSpeed * this.playerModel.scale * delta,
1332
- horizontalDistance
1333
- );
1334
- this.player.position.add(moveDir.multiplyScalar(moveDistance));
1335
- const lookTarget = this.player.position.clone().add(moveDir);
1336
- this.targetMat.lookAt(
1337
- this.player.position,
1338
- lookTarget,
1339
- this.player.up
1340
- );
1341
- this.targetQuat.setFromRotationMatrix(this.targetMat);
1342
- this.targetQuat.multiply(this.flip180Quat);
1343
- const rotateAlpha = Math.min(1, this.boardingRotateSpeed * delta);
1344
- this.player.quaternion.slerp(this.targetQuat, rotateAlpha);
1329
+ const isLast = this.currentWaypointIndex === this.boardingWaypoints.length - 1;
1330
+ const threshold = isLast ? 0 : 10 * this.playerModel.scale;
1331
+ const horizDist = new THREE4.Vector2(waypoint.x - currentPos.x, waypoint.z - currentPos.z).length();
1332
+ if (horizDist > threshold) {
1333
+ const moveDir = new THREE4.Vector3(waypoint.x - currentPos.x, 0, waypoint.z - currentPos.z).normalize();
1334
+ this.player.position.add(moveDir.clone().multiplyScalar(Math.min(this.boardingMoveSpeed * this.playerModel.scale * delta, horizDist)));
1335
+ this.targetMat.lookAt(this.player.position, this.player.position.clone().add(moveDir), this.player.up);
1336
+ this.targetQuat.setFromRotationMatrix(this.targetMat).multiply(this.flip180Quat);
1337
+ this.player.quaternion.slerp(this.targetQuat, Math.min(1, this.boardingRotateSpeed * delta));
1345
1338
  } else {
1346
1339
  this.currentWaypointIndex++;
1347
1340
  }
1348
1341
  }
1349
- /**
1350
- * 完成上车
1351
- */
1342
+ // 最终对齐朝向
1352
1343
  finalizeBoarding(delta) {
1353
1344
  const v = this.activeVehicle;
1354
1345
  if (!this.boardingTargetDir || !v || !this.isMovingToBoardingPoint) return;
1355
- const currentDir = new THREE3.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion).normalize();
1356
- const targetDir = this.boardingTargetDir.clone().normalize();
1357
- const angleDiff = currentDir.angleTo(targetDir);
1358
- if (angleDiff > 0.01) {
1359
- const lookTarget = this.player.position.clone().add(targetDir);
1360
- this.targetMat.lookAt(
1361
- this.player.position,
1362
- lookTarget,
1363
- this.player.up
1364
- );
1346
+ const currentDir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion).normalize();
1347
+ if (currentDir.angleTo(this.boardingTargetDir) > 0.01) {
1348
+ const lookTarget = this.player.position.clone().add(this.boardingTargetDir);
1349
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1365
1350
  this.targetQuat.setFromRotationMatrix(this.targetMat);
1366
- const rotateAlpha = Math.min(1, this.boardingRotateSpeed * delta);
1367
- this.player.quaternion.slerp(this.targetQuat, rotateAlpha);
1351
+ this.player.quaternion.slerp(this.targetQuat, Math.min(1, this.boardingRotateSpeed * delta));
1368
1352
  } else {
1369
1353
  this.boardingWaypoints = [];
1370
1354
  this.currentWaypointIndex = 0;
@@ -1378,23 +1362,20 @@ var PlayerController = class {
1378
1362
  this.player.quaternion.multiply(this.flip180Quat);
1379
1363
  }
1380
1364
  }
1365
+ // 上车动画完成
1381
1366
  onEnterCarAnimFinished() {
1382
1367
  const v = this.activeVehicle;
1383
1368
  if (!v || !this.isMovingToBoardingPoint) return;
1384
1369
  this.player.updateMatrixWorld(true);
1385
1370
  const offsetY = this.boardingPointWorld.y - this.player.position.y;
1386
1371
  this.controllerMode = 1;
1387
- this.syncControllerModeBtnEl();
1372
+ this.syncMobileControllerMode();
1388
1373
  this.setOverShoulderView(false);
1389
1374
  v.vehicleGroup.attach(this.player);
1390
- this.player.position.add(
1391
- v.seatOffset.clone().multiplyScalar(v.scale).add(new THREE3.Vector3(0, offsetY, 0))
1392
- );
1375
+ this.player.position.add(v.seatOffset.clone().multiplyScalar(v.scale).add(new THREE4.Vector3(0, offsetY, 0)));
1393
1376
  this.isMovingToBoardingPoint = false;
1394
1377
  }
1395
- /**
1396
- * 下车
1397
- */
1378
+ // 触发下车流程
1398
1379
  exitVehicle() {
1399
1380
  const v = this.activeVehicle;
1400
1381
  if (!v) return;
@@ -1403,9 +1384,7 @@ var PlayerController = class {
1403
1384
  this.currentWaypointIndex = 0;
1404
1385
  this.boardingTargetDir = null;
1405
1386
  const vel = v.chassisBody.linvel();
1406
- const horizSpeed = Math.sqrt(vel.x * vel.x + vel.z * vel.z);
1407
- const isStationary = horizSpeed < 0.1;
1408
- if (isStationary) {
1387
+ if (Math.sqrt(vel.x ** 2 + vel.z ** 2) < 0.1) {
1409
1388
  this.playPersonAnimationByName("exitCar");
1410
1389
  this.isExitAnimPlaying = true;
1411
1390
  this.closeExitDoorTriggered = false;
@@ -1414,95 +1393,88 @@ var PlayerController = class {
1414
1393
  }
1415
1394
  this.openVehicleDoor(true);
1416
1395
  this.controllerMode = 0;
1417
- this.syncControllerModeBtnEl();
1396
+ this.syncMobileControllerMode();
1418
1397
  this.setOverShoulderView(this.enableOverShoulderView);
1419
1398
  this.scene.attach(this.player);
1420
- if (this.isFirstPerson) {
1421
- this.setFirstPersonCamera();
1422
- }
1399
+ if (this.isFirstPerson) this.setFirstPersonCamera();
1423
1400
  this.setControllerTransition();
1424
1401
  }
1425
- // ==================== 相机与视角控制 ====================
1402
+ // ==================== 相机与视角 ====================
1403
+ // 切换第一/三人称
1426
1404
  changeView() {
1427
1405
  this.isFirstPerson = !this.isFirstPerson;
1428
1406
  if (this.isFirstPerson) {
1407
+ const playerFwd = new THREE4.Vector3(0, 0, 1).applyQuaternion(this.player.quaternion);
1408
+ const flatDir = new THREE4.Vector3(playerFwd.x, 0, playerFwd.z).normalize();
1409
+ if (flatDir.lengthSq() > 1e-3) {
1410
+ const yAngle = Math.atan2(flatDir.x, flatDir.z);
1411
+ this.player.rotation.set(0, yAngle, 0);
1412
+ }
1429
1413
  this.setFirstPersonCamera();
1430
1414
  this.setOverShoulderView(false);
1431
1415
  } else {
1432
1416
  this.controls.enabled = true;
1433
1417
  this.scene.attach(this.camera);
1434
- const worldPos = this.player.position.clone();
1435
- const dir = new THREE3.Vector3(0, 0, -1).applyQuaternion(
1436
- this.player.quaternion
1437
- );
1418
+ const dir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1438
1419
  const angle = Math.atan2(dir.z, dir.x);
1439
- const offset = new THREE3.Vector3(
1440
- Math.cos(angle) * 400 * this.playerModel.scale,
1441
- 200 * this.playerModel.scale,
1442
- Math.sin(angle) * 400 * this.playerModel.scale
1443
- );
1444
- this.camera.position.copy(worldPos).add(offset);
1445
- this.controls.target.copy(worldPos);
1420
+ const s = this.playerModel.scale;
1421
+ this.camera.position.copy(this.player.position).add(new THREE4.Vector3(Math.cos(angle) * 400 * s, 200 * s, Math.sin(angle) * 400 * s));
1422
+ this.controls.target.copy(this.player.position);
1446
1423
  this.controls.enableZoom = this.enableZoom;
1447
1424
  this.setOverShoulderView(this.enableOverShoulderView);
1448
1425
  }
1449
1426
  this.setPointerLock();
1450
1427
  }
1451
- setFirstPersonCamera() {
1428
+ // 设置第一人称相机
1429
+ setFirstPersonCamera(vertAngle = 0) {
1452
1430
  this.controls.enabled = false;
1453
1431
  if (this.personHead) {
1454
- this.personHead?.attach(this.camera);
1432
+ this.personHead.attach(this.camera);
1455
1433
  this.camera.position.set(0, 10, 20);
1456
1434
  } else {
1457
1435
  this.player.attach(this.camera);
1458
- this.camera.position.set(
1459
- 0,
1460
- 40 * this.playerModel.scale,
1461
- 30 * this.playerModel.scale
1462
- );
1436
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1463
1437
  }
1464
- this.camera.rotation.set(0, Math.PI, 0);
1438
+ this.camera.rotation.set(
1439
+ THREE4.MathUtils.clamp(vertAngle, -1.1, 1.4),
1440
+ Math.PI,
1441
+ 0
1442
+ );
1465
1443
  this.controls.enableZoom = false;
1466
1444
  }
1445
+ // 设置鼠标锁定
1467
1446
  setPointerLock() {
1447
+ if (!document.body.requestPointerLock) return;
1468
1448
  if ((this.thirdMouseMode === 0 || this.thirdMouseMode === 1) && !this.isFirstPerson || this.isFirstPerson) {
1469
1449
  document.body.requestPointerLock();
1470
1450
  } else {
1471
1451
  document.exitPointerLock();
1472
1452
  }
1473
1453
  }
1454
+ // 初始相机位置
1474
1455
  setCameraPos() {
1475
1456
  requestAnimationFrame(() => {
1476
- if (this.isFirstPerson) {
1477
- this.person.add(this.camera);
1478
- this.camera.position.set(
1479
- 0,
1480
- 40 * this.playerModel.scale,
1481
- 30 * this.playerModel.scale
1482
- );
1483
- } else {
1484
- const worldPos = this.player.position.clone();
1485
- const dir = new THREE3.Vector3(0, 0, -1).applyQuaternion(
1486
- this.player.quaternion
1487
- );
1457
+ if (!this.isFirstPerson) {
1458
+ const dir = new THREE4.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
1488
1459
  const angle = Math.atan2(dir.z, dir.x);
1489
- const offset = new THREE3.Vector3(
1490
- Math.cos(angle) * 400 * this.playerModel.scale,
1491
- 200 * this.playerModel.scale,
1492
- Math.sin(angle) * 400 * this.playerModel.scale
1493
- );
1494
- this.camera.position.copy(worldPos).add(offset);
1460
+ const s = this.playerModel.scale;
1461
+ this.camera.position.copy(this.player.position).add(new THREE4.Vector3(Math.cos(angle) * 400 * s, 200 * s, Math.sin(angle) * 400 * s));
1495
1462
  this.controls.enableZoom = this.enableZoom;
1463
+ } else {
1464
+ this.person.add(this.camera);
1465
+ this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
1496
1466
  }
1497
1467
  this.camera.updateProjectionMatrix();
1498
1468
  });
1499
1469
  }
1470
+ // 初始化轨道控制
1500
1471
  setControls() {
1501
1472
  this.controls.enableZoom = this.enableZoom;
1502
- this.controls.rotateSpeed = this.mouseSensity * 0.05;
1473
+ this.controls.rotateSpeed = this.mouseSensitivity * 0.05;
1503
1474
  this.controls.maxPolarAngle = Math.PI * (300 / 360);
1504
1475
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
1505
1476
  }
1477
+ // 重置轨道控制
1506
1478
  resetControls() {
1507
1479
  if (!this.controls) return;
1508
1480
  this.controls.enabled = true;
@@ -1512,214 +1484,131 @@ var PlayerController = class {
1512
1484
  this.controls.enableZoom = true;
1513
1485
  this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
1514
1486
  }
1487
+ // 视角旋转处理
1515
1488
  setToward(dx, dy, speed) {
1516
- if (this.controllerMode == 0) {
1489
+ const sens = this.mouseSensitivity;
1490
+ if (this.controllerMode === 0) {
1517
1491
  if (this.isFirstPerson) {
1518
1492
  if (this.isMovingToBoardingPoint) return;
1519
- const yaw = -dx * speed * this.mouseSensity;
1520
- const pitch = -dy * speed * this.mouseSensity;
1521
- this.player.rotateY(yaw);
1522
- this.camera.rotation.x = THREE3.MathUtils.clamp(
1523
- this.camera.rotation.x + pitch,
1524
- -1.1,
1525
- 1.4
1526
- );
1493
+ this.player.rotateY(-dx * speed * sens);
1494
+ this.camera.rotation.x = THREE4.MathUtils.clamp(this.camera.rotation.x + -dy * speed * sens, -1.1, 1.4);
1527
1495
  } else {
1528
- const sensitivity = this.mouseSensity;
1529
- const deltaX = -dx * speed * sensitivity;
1530
- const deltaY = -dy * speed * sensitivity;
1531
- const target = this.player.position.clone();
1532
- const distance = this.camera.position.distanceTo(target);
1533
- const currentPosition = this.camera.position.clone().sub(target);
1534
- let theta = Math.atan2(currentPosition.x, currentPosition.z);
1535
- let phi = Math.acos(currentPosition.y / distance);
1536
- theta += deltaX;
1537
- phi += deltaY;
1538
- phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1539
- const newX = distance * Math.sin(phi) * Math.sin(theta);
1540
- const newY = distance * Math.cos(phi);
1541
- const newZ = distance * Math.sin(phi) * Math.cos(theta);
1542
- this.camera.position.set(
1543
- target.x + newX,
1544
- target.y + newY,
1545
- target.z + newZ
1546
- );
1547
- this.camera.lookAt(target);
1496
+ this.orbitCamera(this.player.position, -dx * speed * sens, -dy * speed * sens);
1548
1497
  }
1549
1498
  } else {
1550
1499
  const v = this.activeVehicle;
1551
1500
  if (!v) return;
1552
1501
  if (this.isFirstPerson) {
1553
- const yaw = -dx * speed * this.mouseSensity;
1554
- const pitch = -dy * speed * this.mouseSensity;
1555
- this.camera.rotation.y = THREE3.MathUtils.clamp(
1556
- this.camera.rotation.y + yaw,
1557
- Math.PI * (3 / 4),
1558
- Math.PI * (5 / 4)
1559
- );
1560
- this.camera.rotation.x = THREE3.MathUtils.clamp(
1561
- this.camera.rotation.x + pitch,
1562
- 0,
1563
- Math.PI * (1 / 3)
1564
- );
1502
+ this.camera.rotation.y = THREE4.MathUtils.clamp(this.camera.rotation.y + -dx * speed * sens, Math.PI * (3 / 4), Math.PI * (5 / 4));
1503
+ this.camera.rotation.x = THREE4.MathUtils.clamp(this.camera.rotation.x + -dy * speed * sens, 0, Math.PI * (1 / 3));
1565
1504
  } else {
1566
- const sensitivity = this.mouseSensity;
1567
- const deltaX = -dx * speed * sensitivity;
1568
- const deltaY = -dy * speed * sensitivity;
1569
- const target = v.vehicleGroup.position.clone();
1570
- const distance = this.camera.position.distanceTo(target);
1571
- const currentPosition = this.camera.position.clone().sub(target);
1572
- let theta = Math.atan2(currentPosition.x, currentPosition.z);
1573
- let phi = Math.acos(currentPosition.y / distance);
1574
- theta += deltaX;
1575
- phi += deltaY;
1576
- phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1577
- const newX = distance * Math.sin(phi) * Math.sin(theta);
1578
- const newY = distance * Math.cos(phi);
1579
- const newZ = distance * Math.sin(phi) * Math.cos(theta);
1580
- this.camera.position.set(
1581
- target.x + newX,
1582
- target.y + newY,
1583
- target.z + newZ
1584
- );
1585
- this.camera.lookAt(target);
1505
+ this.orbitCamera(v.vehicleGroup.position, -dx * speed * sens, -dy * speed * sens);
1586
1506
  }
1587
1507
  }
1588
1508
  }
1509
+ // 球面轨道旋转
1510
+ orbitCamera(target, deltaX, deltaY) {
1511
+ const distance = this.camera.position.distanceTo(target);
1512
+ const cur = this.camera.position.clone().sub(target);
1513
+ let theta = Math.atan2(cur.x, cur.z) + deltaX;
1514
+ let phi = Math.acos(THREE4.MathUtils.clamp(cur.y / distance, -1, 1)) + deltaY;
1515
+ phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
1516
+ this.camera.position.set(
1517
+ target.x + distance * Math.sin(phi) * Math.sin(theta),
1518
+ target.y + distance * Math.cos(phi),
1519
+ target.z + distance * Math.sin(phi) * Math.cos(theta)
1520
+ );
1521
+ this.camera.lookAt(target);
1522
+ }
1523
+ // ==================== 碰撞体构建 ====================
1524
+ // 补全几何属性
1525
+ ensureAttributesMinimal(geom) {
1526
+ if (!geom.attributes.position) return null;
1527
+ if (!geom.attributes.normal) geom.computeVertexNormals();
1528
+ if (!geom.attributes.uv) {
1529
+ const count = geom.attributes.position.count;
1530
+ geom.setAttribute("uv", new THREE4.BufferAttribute(new Float32Array(count * 2), 2));
1531
+ }
1532
+ return geom;
1533
+ }
1534
+ // 统一几何属性格式
1589
1535
  unifiedAttribute(collected) {
1590
1536
  const attrMap = /* @__PURE__ */ new Map();
1591
1537
  const attrConflict = /* @__PURE__ */ new Set();
1592
- const requiredAttrs = /* @__PURE__ */ new Set(["position", "normal", "uv"]);
1593
- for (const g of collected) {
1594
- const attrNames2 = Object.keys(g.attributes);
1595
- for (const name of attrNames2) {
1596
- if (!requiredAttrs.has(name)) {
1597
- g.deleteAttribute(name);
1598
- }
1599
- }
1600
- }
1538
+ const required = /* @__PURE__ */ new Set(["position", "normal", "uv"]);
1539
+ for (const g of collected)
1540
+ for (const name of Object.keys(g.attributes))
1541
+ if (!required.has(name)) g.deleteAttribute(name);
1601
1542
  for (const g of collected) {
1602
1543
  for (const name of Object.keys(g.attributes)) {
1603
1544
  const attr = g.attributes[name];
1604
1545
  const ctor = attr.array.constructor;
1605
- const itemSize = attr.itemSize;
1606
- const normalized = attr.normalized;
1607
1546
  if (!attrMap.has(name)) {
1608
- attrMap.set(name, {
1609
- itemSize,
1610
- arrayCtor: ctor,
1611
- examples: 1,
1612
- normalized
1613
- });
1547
+ attrMap.set(name, { itemSize: attr.itemSize, arrayCtor: ctor, examples: 1, normalized: attr.normalized });
1614
1548
  } else {
1615
1549
  const m = attrMap.get(name);
1616
- if (m.itemSize !== itemSize || m.arrayCtor !== ctor || m.normalized !== normalized) {
1617
- attrConflict.add(name);
1618
- } else {
1619
- m.examples++;
1620
- }
1550
+ if (m.itemSize !== attr.itemSize || m.arrayCtor !== ctor || m.normalized !== attr.normalized) attrConflict.add(name);
1551
+ else m.examples++;
1621
1552
  }
1622
1553
  }
1623
1554
  }
1624
- if (attrConflict.size) {
1625
- for (const g of collected) {
1626
- for (const name of Array.from(attrConflict)) {
1627
- if (g.attributes[name]) g.deleteAttribute(name);
1628
- }
1629
- }
1630
- for (const name of attrConflict) attrMap.delete(name);
1555
+ for (const name of attrConflict) {
1556
+ for (const g of collected) if (g.attributes[name]) g.deleteAttribute(name);
1557
+ attrMap.delete(name);
1631
1558
  }
1632
- const attrNames = Array.from(attrMap.keys());
1633
- for (const g of collected) {
1634
- const count = g.attributes.position.count;
1635
- for (const name of attrNames) {
1559
+ for (const [name, meta] of attrMap) {
1560
+ for (const g of collected) {
1636
1561
  if (!g.attributes[name]) {
1637
- const meta = attrMap.get(name);
1638
- const len = count * meta.itemSize;
1639
- const array = new meta.arrayCtor(len);
1640
- g.setAttribute(
1641
- name,
1642
- new THREE3.BufferAttribute(
1643
- array,
1644
- meta.itemSize,
1645
- meta.normalized
1646
- )
1647
- );
1562
+ const count = g.attributes.position.count;
1563
+ g.setAttribute(name, new THREE4.BufferAttribute(new meta.arrayCtor(count * meta.itemSize), meta.itemSize, meta.normalized));
1648
1564
  }
1649
1565
  }
1650
1566
  }
1651
1567
  return collected;
1652
1568
  }
1569
+ // 构建静态 BVH
1653
1570
  async createBVH(meshUrl = "") {
1654
1571
  await this.initLoader();
1572
+ const collectMesh = (mesh) => {
1573
+ try {
1574
+ let geom = mesh.geometry.clone();
1575
+ geom.applyMatrix4(mesh.matrixWorld);
1576
+ if (geom.index) geom = geom.toNonIndexed();
1577
+ const safe = this.ensureAttributesMinimal(geom);
1578
+ if (safe) this.collected.push(safe);
1579
+ } catch (e) {
1580
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1581
+ }
1582
+ };
1655
1583
  if (meshUrl === "") {
1656
1584
  if (this.collider) {
1657
1585
  this.scene.remove(this.collider);
1658
1586
  this.collider = null;
1659
1587
  }
1660
1588
  this.scene.traverse((c) => {
1661
- const mesh = c;
1662
- if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
1663
- try {
1664
- let geom = mesh.geometry.clone();
1665
- geom.applyMatrix4(mesh.matrixWorld);
1666
- if (geom.index) geom = geom.toNonIndexed();
1667
- const safe = this.ensureAttributesMinimal(geom);
1668
- if (safe) this.collected.push(safe);
1669
- } catch (e) {
1670
- console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1671
- }
1672
- }
1589
+ const m = c;
1590
+ if (m?.isMesh && m.geometry && c.name !== "capsule") collectMesh(m);
1673
1591
  });
1674
- if (!this.collected.length) return;
1675
- this.collected = this.unifiedAttribute(this.collected);
1676
1592
  } else {
1677
1593
  const gltf = await this.loader.loadAsync(meshUrl);
1678
- const obj = gltf.scene.children[0];
1679
- if (obj && obj?.geometry) {
1680
- const mesh = obj;
1681
- let geom = mesh.geometry.clone();
1682
- geom.applyMatrix4(mesh.matrixWorld);
1683
- if (geom.index) geom = geom.toNonIndexed();
1684
- const safe = this.ensureAttributesMinimal(geom);
1685
- if (safe) this.collected.push(safe);
1594
+ const root = gltf.scene.children[0];
1595
+ if (root?.geometry) {
1596
+ collectMesh(root);
1686
1597
  } else {
1687
- obj.traverse((c) => {
1688
- const mesh = c;
1689
- if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
1690
- try {
1691
- let geom = mesh.geometry.clone();
1692
- geom.applyMatrix4(mesh.matrixWorld);
1693
- if (geom.index) geom = geom.toNonIndexed();
1694
- const safe = this.ensureAttributesMinimal(geom);
1695
- if (safe) this.collected.push(safe);
1696
- } catch (e) {
1697
- console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1698
- }
1699
- }
1598
+ root?.traverse((c) => {
1599
+ if (c?.isMesh && c.geometry && c.name !== "capsule") collectMesh(c);
1700
1600
  });
1701
- if (!this.collected.length) return;
1702
- this.collected = this.unifiedAttribute(this.collected);
1703
1601
  }
1704
1602
  }
1705
- const merged = BufferGeometryUtils.mergeGeometries(
1706
- this.collected,
1707
- false
1708
- );
1603
+ if (!this.collected.length) return;
1604
+ this.collected = this.unifiedAttribute(this.collected);
1605
+ const merged = BufferGeometryUtils.mergeGeometries(this.collected, false);
1709
1606
  if (!merged) {
1710
1607
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
1711
1608
  return;
1712
1609
  }
1713
1610
  merged.boundsTree = new MeshBVH(merged, { maxDepth: 100 });
1714
- this.collider = new THREE3.Mesh(
1715
- merged,
1716
- new THREE3.MeshBasicMaterial({
1717
- opacity: 0.5,
1718
- transparent: true,
1719
- wireframe: true,
1720
- depthTest: true
1721
- })
1722
- );
1611
+ this.collider = new THREE4.Mesh(merged, new THREE4.MeshBasicMaterial({ opacity: 0.5, transparent: true, wireframe: true, depthTest: true }));
1723
1612
  if (this.displayCollider) this.scene.add(this.collider);
1724
1613
  if (this.displayVisualizer) {
1725
1614
  if (this.visualizer) this.scene.remove(this.visualizer);
@@ -1728,165 +1617,108 @@ var PlayerController = class {
1728
1617
  }
1729
1618
  this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
1730
1619
  }
1620
+ // 构建动态 BVH
1731
1621
  createDynamicBVH(objects = []) {
1732
1622
  if (this.dynamicCollider) {
1733
1623
  this.scene.remove(this.dynamicCollider);
1734
1624
  this.dynamicCollider = null;
1735
1625
  }
1736
1626
  this.dynamicCollected = [];
1737
- objects.forEach((object) => {
1738
- object.traverse((c) => {
1739
- const mesh = c;
1740
- if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
1741
- try {
1742
- let geom = mesh.geometry.clone();
1743
- geom.applyMatrix4(mesh.matrixWorld);
1744
- if (geom.index) geom = geom.toNonIndexed();
1745
- const safe = this.ensureAttributesMinimal(geom);
1746
- if (safe) this.dynamicCollected.push(safe);
1747
- } catch (e) {
1748
- console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
1749
- }
1627
+ objects.forEach((obj) => obj.traverse((c) => {
1628
+ const m = c;
1629
+ if (m?.isMesh && m.geometry && c.name !== "capsule") {
1630
+ try {
1631
+ let geom = m.geometry.clone();
1632
+ geom.applyMatrix4(m.matrixWorld);
1633
+ if (geom.index) geom = geom.toNonIndexed();
1634
+ const safe = this.ensureAttributesMinimal(geom);
1635
+ if (safe) this.dynamicCollected.push(safe);
1636
+ } catch (e) {
1637
+ console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", m, e);
1750
1638
  }
1751
- });
1752
- });
1639
+ }
1640
+ }));
1753
1641
  if (!this.dynamicCollected.length) return;
1754
1642
  this.dynamicCollected = this.unifiedAttribute(this.dynamicCollected);
1755
- const merged = BufferGeometryUtils.mergeGeometries(
1756
- this.dynamicCollected,
1757
- false
1758
- );
1643
+ const merged = BufferGeometryUtils.mergeGeometries(this.dynamicCollected, false);
1759
1644
  if (!merged) {
1760
1645
  console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
1761
1646
  return;
1762
1647
  }
1763
1648
  merged.boundsTree = new MeshBVH(merged);
1764
- this.dynamicCollider = new THREE3.Mesh(
1765
- merged,
1766
- new THREE3.MeshBasicMaterial({
1767
- opacity: 0.5,
1768
- transparent: true,
1769
- wireframe: true,
1770
- depthTest: true
1771
- })
1772
- );
1649
+ this.dynamicCollider = new THREE4.Mesh(merged, new THREE4.MeshBasicMaterial({ opacity: 0.5, transparent: true, wireframe: true, depthTest: true }));
1773
1650
  if (this.displayCollider) this.scene.add(this.dynamicCollider);
1774
1651
  }
1775
- getAngleWithYAxis(normal) {
1776
- const yAxis = { x: 0, y: 1, z: 0 };
1777
- const dotProduct = normal.x * yAxis.x + normal.y * yAxis.y + normal.z * yAxis.z;
1778
- const normalMagnitude = Math.sqrt(
1779
- normal.x * normal.x + normal.y * normal.y + normal.z * normal.z
1780
- );
1781
- const cosTheta = dotProduct / normalMagnitude;
1782
- return Math.acos(cosTheta);
1783
- }
1784
- // ==================== 设置控制器过渡 ====================
1652
+ // ==================== 控制器过渡 ====================
1653
+ // 车辆切换过渡
1785
1654
  setControllerTransition() {
1786
1655
  if (this.isChangeControllerTransitionTimer) {
1787
1656
  clearTimeout(this.isChangeControllerTransitionTimer);
1788
1657
  this.isChangeControllerTransitionTimer = null;
1789
1658
  }
1790
- let vGroups = [];
1791
- for (const v of this.vehicles) {
1792
- vGroups.push(v.vehicleGroup);
1793
- }
1659
+ const vGroups = this.vehicles.map((v) => v.vehicleGroup);
1794
1660
  this.createDynamicBVH(vGroups);
1795
1661
  this.isChangeControllerTransitionTimer = setTimeout(() => {
1796
1662
  this.isChangeControllerTransitionTimer = null;
1797
- for (const v of this.vehicles) {
1798
- this.clearVehicleVelocity(v);
1799
- }
1663
+ this.vehicles.forEach((v) => this.clearVehicleVelocity(v));
1800
1664
  this.createDynamicBVH(vGroups);
1801
1665
  }, 3e3);
1802
1666
  }
1803
- // 清除车辆速度
1667
+ // 清零车辆速度
1804
1668
  clearVehicleVelocity(v) {
1805
1669
  if (!v || !this.world || !this.RAPIER) return;
1806
1670
  const { chassisBody, vehicleController } = v;
1807
1671
  const ZERO = new this.RAPIER.Vector3(0, 0, 0);
1808
1672
  chassisBody.setLinvel(ZERO, true);
1809
1673
  chassisBody.setAngvel(ZERO, true);
1810
- const BIG_BRAKE = 1e6;
1811
1674
  for (let i = 0; i < 4; i++) {
1812
1675
  vehicleController.setWheelEngineForce(i, 0);
1813
- vehicleController.setWheelBrake(i, BIG_BRAKE);
1676
+ vehicleController.setWheelBrake(i, 1e6);
1814
1677
  }
1815
1678
  vehicleController.updateVehicle(1 / 60);
1816
1679
  this.world.timestep = 1 / 60;
1817
1680
  this.world.step();
1818
1681
  chassisBody.setLinvel(ZERO, true);
1819
1682
  chassisBody.setAngvel(ZERO, true);
1820
- for (let i = 0; i < 4; i++) {
1821
- vehicleController.setWheelBrake(i, 0);
1822
- }
1683
+ for (let i = 0; i < 4; i++) vehicleController.setWheelBrake(i, 0);
1823
1684
  }
1824
1685
  // ==================== 循环更新 ====================
1686
+ // 主循环更新
1825
1687
  async update(delta = clock.getDelta()) {
1826
1688
  if (!this.isupdate || !this.player || !this.collider) return;
1827
1689
  delta = Math.min(delta, 1 / 40);
1828
- if (this.controllerMode == 1) {
1690
+ if (this.controllerMode === 1) {
1829
1691
  this.updateVehicle(delta);
1830
1692
  } else {
1831
1693
  this.updatePlayer(delta);
1832
- if (this.isChangeControllerTransitionTimer)
1833
- this.updateVehicleInertia(delta);
1694
+ if (this.isChangeControllerTransitionTimer) this.updateVehicleInertia(delta);
1834
1695
  }
1835
1696
  }
1836
- /**
1837
- * 更新当前驾驶的车辆
1838
- */
1697
+ // 车辆帧更新
1839
1698
  updateVehicle(delta) {
1840
1699
  const v = this.activeVehicle;
1841
1700
  if (!v || !this.world) return;
1842
1701
  const { vehicleController, chassisBody, vehicleGroup } = v;
1843
1702
  const rotation = chassisBody.rotation();
1844
- const quat = new THREE3.Quaternion(
1845
- rotation.x,
1846
- rotation.y,
1847
- rotation.z,
1848
- rotation.w
1849
- );
1850
- const forward = new THREE3.Vector3(1, 0, 0).applyQuaternion(quat);
1703
+ const quat = new THREE4.Quaternion(rotation.x, rotation.y, rotation.z, rotation.w);
1704
+ const forward = new THREE4.Vector3(1, 0, 0).applyQuaternion(quat);
1851
1705
  const slopeAngle = Math.asin(forward.y);
1852
- let factor = 1;
1853
- if (slopeAngle < -0.05 && this.fwdPressed) factor = -Math.sin(slopeAngle) * 10;
1706
+ const factor = slopeAngle < -0.05 && this.fwdPressed ? -Math.sin(slopeAngle) * 10 : 1;
1854
1707
  const accelerateForce = this.vehicleParams.power.accelerateForce * v.speedMultiplier;
1855
1708
  const maxSpeed = this.vehicleParams.power.maxSpeed * v.speedMultiplier;
1856
- const engineForce = (Number(this.fwdPressed) * accelerateForce - Number(this.bkdPressed) * accelerateForce) * factor;
1857
- vehicleController.setWheelEngineForce(0, engineForce);
1858
- vehicleController.setWheelEngineForce(1, engineForce);
1859
- vehicleController.setWheelEngineForce(2, engineForce);
1860
- vehicleController.setWheelEngineForce(3, engineForce);
1709
+ const engineForce = (Number(this.fwdPressed) - Number(this.bkdPressed)) * accelerateForce * factor;
1710
+ for (let i = 0; i < 4; i++) vehicleController.setWheelEngineForce(i, engineForce);
1861
1711
  const wheelBrake = Number(this.spacePressed) * this.vehicleParams.power.brakeForce * delta;
1862
- vehicleController.setWheelBrake(0, wheelBrake);
1863
- vehicleController.setWheelBrake(1, wheelBrake);
1864
- vehicleController.setWheelBrake(2, wheelBrake);
1865
- vehicleController.setWheelBrake(3, wheelBrake);
1712
+ for (let i = 0; i < 4; i++) vehicleController.setWheelBrake(i, wheelBrake);
1866
1713
  const currentSteering = vehicleController.wheelSteering(0) || 0;
1867
- const steerDirection = Number(this.lftPressed) - Number(this.rgtPressed);
1868
- let steerSpeed;
1869
- if (steerDirection === 0) {
1870
- steerSpeed = this.vehicleParams.steering.steerReturnSpeed || 0.15;
1871
- } else {
1872
- steerSpeed = this.vehicleParams.steering.steerSpeed || 0.08;
1873
- }
1874
- const steerLerpFactor = 1 - Math.pow(1 - steerSpeed, delta);
1875
- const targetSteering = this.vehicleParams.steering.maxSteerAngle * steerDirection;
1876
- const steering = THREE3.MathUtils.lerp(
1877
- currentSteering,
1878
- targetSteering,
1879
- steerLerpFactor
1880
- );
1714
+ const steerDir = Number(this.lftPressed) - Number(this.rgtPressed);
1715
+ const steerSpeed = steerDir === 0 ? this.vehicleParams.steering.steerReturnSpeed : this.vehicleParams.steering.steerSpeed;
1716
+ const steering = THREE4.MathUtils.lerp(currentSteering, this.vehicleParams.steering.maxSteerAngle * steerDir, 1 - Math.pow(1 - steerSpeed, delta));
1881
1717
  vehicleController.setWheelSteering(0, steering);
1882
1718
  vehicleController.setWheelSteering(1, steering);
1883
- if ((this.rgtPressed || this.lftPressed) && this.shiftPressed) {
1884
- vehicleController.setWheelSideFrictionStiffness(2, 0.5);
1885
- vehicleController.setWheelSideFrictionStiffness(3, 0.5);
1886
- } else {
1887
- vehicleController.setWheelSideFrictionStiffness(2, 2);
1888
- vehicleController.setWheelSideFrictionStiffness(3, 2);
1889
- }
1719
+ const driftFriction = (this.rgtPressed || this.lftPressed) && this.shiftPressed ? 0.5 : 2;
1720
+ vehicleController.setWheelSideFrictionStiffness(2, driftFriction);
1721
+ vehicleController.setWheelSideFrictionStiffness(3, driftFriction);
1890
1722
  this.updateVehicleInertia(delta);
1891
1723
  if (!this.isFirstPerson) {
1892
1724
  const lookTarget = vehicleGroup.position.clone();
@@ -1895,99 +1727,49 @@ var PlayerController = class {
1895
1727
  this.camera.position.add(lookTarget);
1896
1728
  this.controls.update();
1897
1729
  const velocity = chassisBody.linvel();
1898
- const currentSpeed = Math.sqrt(
1899
- velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z
1900
- );
1730
+ const currentSpeed = new THREE4.Vector3(velocity.x, velocity.y, velocity.z).length();
1901
1731
  const speedRatio = Math.min(currentSpeed / maxSpeed, 1);
1902
- const baseCamDistance = v.size.l * 0.8;
1903
- const maxCamDistanceLimit = v.size.l * 5;
1904
- const targetDistance = THREE3.MathUtils.lerp(
1905
- baseCamDistance,
1906
- maxCamDistanceLimit,
1907
- speedRatio
1908
- );
1909
- this._personToCam.subVectors(
1910
- this.camera.position,
1911
- vehicleGroup.position
1912
- );
1913
- const origin = vehicleGroup.position.clone().add(new THREE3.Vector3(0, 0, 0));
1914
- const direction = this._personToCam.clone().normalize();
1915
- const desiredDist = targetDistance;
1916
- this._raycasterPersonToCam.set(origin, direction);
1917
- this._raycasterPersonToCam.far = desiredDist;
1918
- const intersects = this._raycasterPersonToCam.intersectObject(
1919
- this.collider,
1920
- false
1921
- );
1922
- if (intersects.length > 0) {
1923
- const hit = intersects[0];
1924
- const safeDist = Math.max(
1925
- hit.distance - this._camEpsilon,
1926
- this.minCamDistance
1927
- );
1928
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1929
- this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1732
+ const baseDist = v.size.l * 0.8;
1733
+ const maxDist = v.size.l * 5;
1734
+ const desiredDist = THREE4.MathUtils.lerp(baseDist, maxDist, speedRatio);
1735
+ const minSafeDist = baseDist;
1736
+ this.personToCam.subVectors(this.camera.position, vehicleGroup.position);
1737
+ const direction = this.personToCam.clone().normalize();
1738
+ this.raycasterPersonToCam.set(vehicleGroup.position, direction);
1739
+ this.raycasterPersonToCam.far = desiredDist;
1740
+ const hits = this.raycasterPersonToCam.intersectObject(this.collider, false);
1741
+ if (hits.length > 0) {
1742
+ const safeDist = Math.max(hits[0].distance - this.camEpsilon, minSafeDist);
1743
+ this.camera.position.lerp(vehicleGroup.position.clone().add(direction.clone().multiplyScalar(safeDist)), this.camCollisionLerp);
1930
1744
  } else {
1931
- this._raycasterPersonToCam.far = maxCamDistanceLimit;
1932
- const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(
1933
- this.collider,
1934
- false
1935
- );
1936
- let safeDist = desiredDist;
1937
- if (intersectsMaxDis.length) {
1938
- const hitMax = intersectsMaxDis[0];
1939
- safeDist = Math.min(
1940
- desiredDist,
1941
- hitMax.distance - this._camEpsilon
1942
- );
1943
- }
1944
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
1945
- this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
1745
+ this.raycasterPersonToCam.far = maxDist;
1746
+ const maxHits = this.raycasterPersonToCam.intersectObject(this.collider, false);
1747
+ const safeDist = maxHits.length > 0 ? Math.min(desiredDist, maxHits[0].distance - this.camEpsilon) : desiredDist;
1748
+ this.camera.position.lerp(vehicleGroup.position.clone().add(direction.clone().multiplyScalar(safeDist)), this.camCollisionLerp);
1946
1749
  }
1947
1750
  if ((this.fwdPressed || this.bkdPressed) && this.vehicleParams.followVehicleDirection) {
1948
1751
  const vel = chassisBody.linvel();
1949
- const velHorizontal = new THREE3.Vector3(vel.x, vel.y, vel.z);
1950
- const velSpeed = velHorizontal.length();
1951
- if (velSpeed > 0.3) {
1952
- const targetBehindDir = velHorizontal.clone().normalize().negate();
1953
- this.camBehindDir.lerp(targetBehindDir, this._camCollisionLerp).normalize();
1954
- const camHeightOffset = v.size.h;
1955
- const targetCamPos = lookTarget.clone().add(
1956
- this.camBehindDir.clone().multiplyScalar(desiredDist)
1957
- ).add(new THREE3.Vector3(0, camHeightOffset, 0));
1958
- this.camera.position.lerp(
1959
- targetCamPos,
1960
- this._camCollisionLerp
1961
- );
1752
+ const velVec = new THREE4.Vector3(vel.x, vel.y, vel.z);
1753
+ if (velVec.length() > 0.3) {
1754
+ this.camBehindDir.lerp(velVec.normalize().negate(), this.camCollisionLerp).normalize();
1755
+ const targetCamPos = lookTarget.clone().add(this.camBehindDir.clone().multiplyScalar(desiredDist)).add(new THREE4.Vector3(0, v.size.h, 0));
1756
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
1962
1757
  this.controls.update();
1963
1758
  }
1964
1759
  }
1965
1760
  }
1966
1761
  const vehicleUp = this.upVector.clone().applyQuaternion(vehicleGroup.quaternion);
1967
- const angleWithUp = vehicleUp.angleTo(this.upVector);
1968
- if (angleWithUp > Math.PI / 2) {
1969
- const size = new THREE3.Vector3();
1762
+ if (vehicleUp.angleTo(this.upVector) > Math.PI / 2) {
1763
+ const size = new THREE4.Vector3();
1970
1764
  v.vehicleBBox?.getSize(size);
1971
- const translation2 = chassisBody.translation();
1972
- chassisBody.setTranslation(
1973
- new this.RAPIER.Vector3(
1974
- translation2.x,
1975
- translation2.y + size.y,
1976
- translation2.z
1977
- ),
1978
- true
1979
- );
1980
- chassisBody.setRotation(
1981
- new this.RAPIER.Quaternion(0, 0, 0, 1),
1982
- true
1983
- );
1765
+ const t = chassisBody.translation();
1766
+ chassisBody.setTranslation(new this.RAPIER.Vector3(t.x, t.y + size.y, t.z), true);
1767
+ chassisBody.setRotation(new this.RAPIER.Quaternion(0, 0, 0, 1), true);
1984
1768
  chassisBody.setLinvel(new this.RAPIER.Vector3(0, 0, 0), true);
1985
1769
  chassisBody.setAngvel(new this.RAPIER.Vector3(0, 0, 0), true);
1986
1770
  }
1987
1771
  }
1988
- /**
1989
- * 更新所有车辆物理和位置
1990
- */
1772
+ // 物理步进 & 同步
1991
1773
  updateVehicleInertia(delta) {
1992
1774
  if (!this.world) return;
1993
1775
  this.world.timestep = delta;
@@ -1996,41 +1778,21 @@ var PlayerController = class {
1996
1778
  const { vehicleController, chassisBody, vehicleGroup, updateWheelVisuals } = v;
1997
1779
  vehicleController.updateVehicle(delta);
1998
1780
  if (chassisBody.isSleeping()) continue;
1999
- const velocity = chassisBody.linvel();
2000
- const currentSpeed = Math.sqrt(
2001
- velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z
2002
- );
2003
- const maxSpeed = this.vehicleParams.power.maxSpeed * v.speedMultiplier;
2004
- if (currentSpeed > maxSpeed) {
2005
- const s = maxSpeed / currentSpeed;
2006
- chassisBody.setLinvel(
2007
- new this.RAPIER.Vector3(
2008
- velocity.x * s,
2009
- velocity.y * s,
2010
- velocity.z * s
2011
- ),
2012
- true
2013
- );
1781
+ const vel = chassisBody.linvel();
1782
+ const speed = new THREE4.Vector3(vel.x, vel.y, vel.z).length();
1783
+ const max = this.vehicleParams.power.maxSpeed * v.speedMultiplier;
1784
+ if (speed > max) {
1785
+ const s = max / speed;
1786
+ chassisBody.setLinvel(new this.RAPIER.Vector3(vel.x * s, vel.y * s, vel.z * s), true);
2014
1787
  }
2015
- const translation = chassisBody.translation();
2016
- const rotationSync = chassisBody.rotation();
2017
- vehicleGroup.position.set(
2018
- translation.x,
2019
- translation.y,
2020
- translation.z
2021
- );
2022
- vehicleGroup.quaternion.set(
2023
- rotationSync.x,
2024
- rotationSync.y,
2025
- rotationSync.z,
2026
- rotationSync.w
2027
- );
2028
- if (updateWheelVisuals) updateWheelVisuals();
1788
+ const t = chassisBody.translation();
1789
+ const r = chassisBody.rotation();
1790
+ vehicleGroup.position.set(t.x, t.y, t.z);
1791
+ vehicleGroup.quaternion.set(r.x, r.y, r.z, r.w);
1792
+ updateWheelVisuals?.();
2029
1793
  }
2030
1794
  }
2031
- /**
2032
- * 设置人物缩放
2033
- */
1795
+ // 缩放玩家比例
2034
1796
  setPlayerScale(newScale) {
2035
1797
  if (newScale <= 0) return;
2036
1798
  const ratio = newScale / this.playerModel.scale;
@@ -2040,7 +1802,7 @@ var PlayerController = class {
2040
1802
  this.playerSpeed *= ratio;
2041
1803
  this.playerFlySpeed *= ratio;
2042
1804
  this.curPlayerSpeed *= ratio;
2043
- this._camEpsilon *= ratio;
1805
+ this.camEpsilon *= ratio;
2044
1806
  this.minCamDistance *= ratio;
2045
1807
  this.maxCamDistance *= ratio;
2046
1808
  this.orginMaxCamDistance *= ratio;
@@ -2049,38 +1811,32 @@ var PlayerController = class {
2049
1811
  if (this.player?.capsuleInfo) this.player.capsuleInfo.radius *= ratio;
2050
1812
  if (this.isFirstPerson) this.setFirstPersonCamera();
2051
1813
  }
2052
- /**
2053
- * 更新人物
2054
- */
1814
+ // 玩家帧更新
2055
1815
  updatePlayer(delta) {
2056
- if (this.isMovingToBoardingPoint) {
2057
- this.updateMoveToBoardingPoint(delta);
2058
- }
2059
- if (!this.isFlying) {
2060
- this.player.position.addScaledVector(this.playerVelocity, delta);
2061
- }
1816
+ if (this.isMovingToBoardingPoint) this.updateMoveToBoardingPoint(delta);
1817
+ if (!this.isFlying) this.player.position.addScaledVector(this.playerVelocity, delta);
2062
1818
  if (this.isBoardingAnimPlaying) {
2063
1819
  const action = this.personActions?.get("enterCar");
2064
- const duration = action.getClip().duration;
2065
- const timeScale = action.getEffectiveTimeScale();
2066
- const remaining = (duration - action.time) / timeScale * 1e3;
2067
- if (!this.closeDoorTriggered && remaining <= 500) {
2068
- this.closeDoorTriggered = true;
2069
- this.openVehicleDoor(false);
2070
- }
2071
- if (action.time >= duration) {
2072
- this.isBoardingAnimPlaying = false;
2073
- this.closeDoorTriggered = false;
2074
- this.onEnterCarAnimFinished();
2075
- return;
1820
+ if (action) {
1821
+ const duration = action.getClip().duration;
1822
+ const remaining = (duration - action.time) / action.getEffectiveTimeScale() * 1e3;
1823
+ if (!this.closeDoorTriggered && remaining <= 500) {
1824
+ this.closeDoorTriggered = true;
1825
+ this.openVehicleDoor(false);
1826
+ }
1827
+ if (action.time >= duration) {
1828
+ this.isBoardingAnimPlaying = false;
1829
+ this.closeDoorTriggered = false;
1830
+ this.onEnterCarAnimFinished();
1831
+ return;
1832
+ }
2076
1833
  }
2077
1834
  }
2078
1835
  if (this.isExitAnimPlaying) {
2079
1836
  const action = this.personActions?.get("exitCar");
2080
1837
  if (action) {
2081
1838
  const duration = action.getClip().duration;
2082
- const timeScale = action.getEffectiveTimeScale();
2083
- const remaining = (duration - action.time) / timeScale * 1e3;
1839
+ const remaining = (duration - action.time) / action.getEffectiveTimeScale() * 1e3;
2084
1840
  if (!this.closeExitDoorTriggered && remaining <= 500) {
2085
1841
  this.closeExitDoorTriggered = true;
2086
1842
  this.openVehicleDoor(false);
@@ -2094,71 +1850,47 @@ var PlayerController = class {
2094
1850
  this.updateMixers(delta);
2095
1851
  if (this.controllerMode === 1) return;
2096
1852
  this.camera.getWorldDirection(this.camDir);
2097
- let angle = Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2;
2098
- angle = 2 * Math.PI - angle;
1853
+ const angle = 2 * Math.PI - (Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2);
2099
1854
  this.moveDir.set(0, 0, 0);
2100
1855
  if (this.fwdPressed) this.moveDir.add(this.DIR_FWD);
2101
1856
  if (this.bkdPressed) this.moveDir.add(this.DIR_BKD);
2102
1857
  if (this.lftPressed) this.moveDir.add(this.DIR_LFT);
2103
1858
  if (this.rgtPressed) this.moveDir.add(this.DIR_RGT);
2104
1859
  if (this.isFlying) {
2105
- if (this.fwdPressed) this.moveDir.y = this.camDir.y;
2106
- else this.moveDir.y = 0;
1860
+ this.moveDir.y = this.fwdPressed ? this.camDir.y : 0;
2107
1861
  if (this.spacePressed) this.moveDir.add(this.DIR_UP);
2108
- }
2109
- if (this.isFlying && this.fwdPressed) {
2110
1862
  this.curPlayerSpeed = this.shiftPressed ? this.playerFlySpeed * 2 : this.playerFlySpeed;
2111
1863
  } else {
2112
1864
  this.curPlayerSpeed = this.shiftPressed ? this.playerSpeed * 2 : this.playerSpeed;
2113
1865
  }
2114
1866
  this.moveDir.normalize().applyAxisAngle(this.upVector, angle);
2115
- this.player.position.addScaledVector(
2116
- this.moveDir,
2117
- this.curPlayerSpeed * delta
2118
- );
2119
- let playerDistanceFromGround = Infinity;
2120
- this._originTmp.set(
2121
- this.player.position.x,
2122
- this.player.position.y,
2123
- this.player.position.z
2124
- );
2125
- this._raycaster.ray.origin.copy(this._originTmp);
2126
- const intersects = this._raycaster.intersectObject(
2127
- this.collider,
2128
- false
2129
- );
2130
- if (intersects.length > 0) {
2131
- playerDistanceFromGround = this.player.position.y - intersects[0].point.y;
2132
- const maxH = this.playerCapsuleHeight * this.playerModel.scale * 0.9;
2133
- const h = this.playerCapsuleHeight * this.playerModel.scale * 0.75;
2134
- const minH = this.playerCapsuleHeight * this.playerModel.scale * 0.7;
2135
- if (!this.isFlying) {
2136
- if (playerDistanceFromGround >= maxH) {
2137
- this.playerVelocity.y += delta * this.gravity;
2138
- this.player.position.addScaledVector(
2139
- this.playerVelocity,
2140
- delta
2141
- );
2142
- this.playerIsOnGround = false;
2143
- } else if (playerDistanceFromGround >= h && playerDistanceFromGround < maxH) {
2144
- if (!this.spacePressed) {
2145
- this.playerVelocity.set(0, 0, 0);
2146
- this.playerIsOnGround = true;
2147
- this.player.position.y = intersects[0].point.y + h;
2148
- }
2149
- } else if (playerDistanceFromGround >= minH && playerDistanceFromGround < h) {
2150
- this.playerVelocity.set(0, 0, 0);
2151
- this.playerIsOnGround = true;
2152
- this.player.position.y = intersects[0].point.y + h;
2153
- } else if (playerDistanceFromGround < minH) {
1867
+ this.player.position.addScaledVector(this.moveDir, this.curPlayerSpeed * delta);
1868
+ this.raycaster.ray.origin.copy(this.player.position);
1869
+ const hits = this.raycaster.intersectObject(this.collider, false);
1870
+ if (hits.length > 0 && !this.isFlying) {
1871
+ const dist = this.player.position.y - hits[0].point.y;
1872
+ const s = this.playerModel.scale;
1873
+ const maxH = this.playerCapsuleHeight * s * 0.9;
1874
+ const h = this.playerCapsuleHeight * s * 0.75;
1875
+ const minH = this.playerCapsuleHeight * s * 0.7;
1876
+ if (dist >= maxH) {
1877
+ this.playerVelocity.y += delta * this.gravity;
1878
+ this.player.position.addScaledVector(this.playerVelocity, delta);
1879
+ this.playerIsOnGround = false;
1880
+ } else if (dist >= h && dist < maxH) {
1881
+ if (!this.spacePressed) {
2154
1882
  this.playerVelocity.set(0, 0, 0);
2155
- this.player.position.set(
2156
- this.player.position.x,
2157
- intersects[0].point.y + h,
2158
- this.player.position.z
2159
- );
2160
1883
  this.playerIsOnGround = true;
1884
+ this.player.position.y = hits[0].point.y + h;
2161
1885
  }
1886
+ } else if (dist >= minH) {
1887
+ this.playerVelocity.set(0, 0, 0);
1888
+ this.playerIsOnGround = true;
1889
+ this.player.position.y = hits[0].point.y + h;
1890
+ } else {
1891
+ this.playerVelocity.set(0, 0, 0);
1892
+ this.player.position.y = hits[0].point.y + h;
1893
+ this.playerIsOnGround = true;
2162
1894
  }
2163
1895
  }
2164
1896
  this.player.updateMatrixWorld();
@@ -2168,113 +1900,49 @@ var PlayerController = class {
2168
1900
  this.tempSegment.copy(capsuleInfo.segment);
2169
1901
  this.tempSegment.start.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
2170
1902
  this.tempSegment.end.applyMatrix4(this.player.matrixWorld).applyMatrix4(this.tempMat);
2171
- this.tempBox.expandByPoint(this.tempSegment.start);
2172
- this.tempBox.expandByPoint(this.tempSegment.end);
1903
+ this.tempBox.expandByPoint(this.tempSegment.start).expandByPoint(this.tempSegment.end);
2173
1904
  this.tempBox.expandByScalar(capsuleInfo.radius);
2174
1905
  if (!this.isMovingToBoardingPoint) {
2175
- this.collider?.geometry?.boundsTree?.shapecast({
2176
- intersectsBounds: (box) => box.intersectsBox(this.tempBox),
2177
- intersectsTriangle: (tri) => {
2178
- const triPoint = this.tempVector;
2179
- const capsulePoint = this.tempVector2;
2180
- const distance = tri.closestPointToSegment(
2181
- this.tempSegment,
2182
- triPoint,
2183
- capsulePoint
2184
- );
2185
- if (distance < capsuleInfo.radius) {
2186
- const normal = tri.getNormal(new THREE3.Vector3());
2187
- if (normal.y > 0.5 && !this.isFlying) return;
2188
- const depth = capsuleInfo.radius - distance;
2189
- const direction = capsulePoint.sub(triPoint).normalize();
2190
- this.tempSegment.start.addScaledVector(
2191
- direction,
2192
- depth
2193
- );
2194
- this.tempSegment.end.addScaledVector(direction, depth);
2195
- }
2196
- }
2197
- });
2198
- this.dynamicCollider?.geometry?.boundsTree?.shapecast({
2199
- intersectsBounds: (box) => box.intersectsBox(this.tempBox),
2200
- intersectsTriangle: (tri) => {
2201
- const triPoint = this.tempVector;
2202
- const capsulePoint = this.tempVector2;
2203
- const distance = tri.closestPointToSegment(
2204
- this.tempSegment,
2205
- triPoint,
2206
- capsulePoint
2207
- );
2208
- if (distance < capsuleInfo.radius) {
2209
- const depth = capsuleInfo.radius - distance;
2210
- const direction = capsulePoint.sub(triPoint).normalize();
2211
- this.tempSegment.start.addScaledVector(
2212
- direction,
2213
- depth
2214
- );
2215
- this.tempSegment.end.addScaledVector(direction, depth);
1906
+ const resolveCollision = (collider) => {
1907
+ if (!collider) return;
1908
+ collider.geometry?.boundsTree?.shapecast({
1909
+ intersectsBounds: (box) => box.intersectsBox(this.tempBox),
1910
+ intersectsTriangle: (tri) => {
1911
+ const distance = tri.closestPointToSegment(this.tempSegment, this.tempVector, this.tempVector2);
1912
+ if (distance < capsuleInfo.radius) {
1913
+ const normal = tri.getNormal(new THREE4.Vector3());
1914
+ if (normal.y > 0.5 && !this.isFlying) return;
1915
+ const dir = this.tempVector2.sub(this.tempVector).normalize();
1916
+ const depth = capsuleInfo.radius - distance;
1917
+ this.tempSegment.start.addScaledVector(dir, depth);
1918
+ this.tempSegment.end.addScaledVector(dir, depth);
1919
+ }
2216
1920
  }
2217
- }
2218
- });
1921
+ });
1922
+ };
1923
+ resolveCollision(this.collider);
1924
+ resolveCollision(this.dynamicCollider);
2219
1925
  }
2220
- const newPosition = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
2221
- const deltaVector = this.tempVector2.subVectors(
2222
- newPosition,
2223
- this.player.position
2224
- );
2225
- const offset = Math.max(0, deltaVector.length() - 1e-5);
2226
- deltaVector.normalize().multiplyScalar(offset);
2227
- this.player.position.add(deltaVector);
2228
- if (!this.isFirstPerson && !this.isFlying) {
2229
- this.camDir.y = 0;
2230
- this.camDir.normalize();
2231
- this.camDir.negate();
2232
- this.moveDir.normalize();
2233
- this.moveDir.negate();
2234
- let lookTarget;
2235
- if (this.thirdMouseMode === 0 || this.thirdMouseMode === 2) {
2236
- if (this.moveDir.lengthSq() > 0) {
2237
- lookTarget = this.player.position.clone().add(this.moveDir);
2238
- } else {
2239
- lookTarget = this.player.position.clone().add(this.camDir);
1926
+ const newPos = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
1927
+ const deltaVec = this.tempVector2.subVectors(newPos, this.player.position);
1928
+ const offset = Math.max(0, deltaVec.length() - 1e-5);
1929
+ this.player.position.add(deltaVec.normalize().multiplyScalar(offset));
1930
+ if (!this.isFirstPerson) {
1931
+ const camDirFlat = this.camDir.clone().setY(0).normalize().negate();
1932
+ const moveDirFlat = this.moveDir.clone().normalize().negate();
1933
+ if (!this.isFlying) {
1934
+ if (this.thirdMouseMode === 0 || this.thirdMouseMode === 2) {
1935
+ const lookTarget = this.player.position.clone().add(moveDirFlat.lengthSq() > 0 ? moveDirFlat : camDirFlat);
1936
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1937
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
1938
+ } else if (moveDirFlat.lengthSq() > 0) {
1939
+ this.targetMat.lookAt(this.player.position, this.player.position.clone().add(moveDirFlat), this.player.up);
1940
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
2240
1941
  }
2241
- this.targetMat.lookAt(
2242
- this.player.position,
2243
- lookTarget,
2244
- this.player.up
2245
- );
2246
- this.targetQuat.setFromRotationMatrix(this.targetMat);
2247
- const alpha = Math.min(1, this.rotationSpeed * delta);
2248
- this.player.quaternion.slerp(this.targetQuat, alpha);
2249
- }
2250
- if ((this.thirdMouseMode === 1 || this.thirdMouseMode === 3) && this.moveDir.lengthSq() > 0) {
2251
- lookTarget = this.player.position.clone().add(this.moveDir);
2252
- this.targetMat.lookAt(
2253
- this.player.position,
2254
- lookTarget,
2255
- this.player.up
2256
- );
2257
- this.targetQuat.setFromRotationMatrix(this.targetMat);
2258
- const alpha = Math.min(1, this.rotationSpeed * delta);
2259
- this.player.quaternion.slerp(this.targetQuat, alpha);
2260
- }
2261
- }
2262
- if (this.isFlying) {
2263
- if (!this.isFirstPerson) {
2264
- this.camDir.y = 0;
2265
- this.camDir.normalize();
2266
- this.camDir.negate();
2267
- this.moveDir.normalize();
2268
- this.moveDir.negate();
2269
- const lookTarget = this.player.position.clone().add(this.fwdPressed ? this.moveDir : this.camDir);
2270
- this.targetMat.lookAt(
2271
- this.player.position,
2272
- lookTarget,
2273
- this.player.up
2274
- );
2275
- this.targetQuat.setFromRotationMatrix(this.targetMat);
2276
- const alpha = Math.min(1, this.rotationSpeed * delta);
2277
- this.player.quaternion.slerp(this.targetQuat, alpha);
1942
+ } else {
1943
+ const lookTarget = this.player.position.clone().add(this.fwdPressed ? moveDirFlat : camDirFlat);
1944
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
1945
+ this.player.quaternion.slerp(this.targetQuat.setFromRotationMatrix(this.targetMat), Math.min(1, this.rotationSpeed * delta));
2278
1946
  }
2279
1947
  }
2280
1948
  if (!this.isFirstPerson) {
@@ -2285,131 +1953,84 @@ var PlayerController = class {
2285
1953
  this.camera.position.add(lookTarget);
2286
1954
  this.controls.update();
2287
1955
  if (!this.enableZoom) {
2288
- this._personToCam.subVectors(
2289
- this.camera.position,
2290
- this.player.position
2291
- );
2292
- const origin = this.player.position.clone();
2293
- const direction = this._personToCam.clone().normalize();
2294
- const desiredDist = this._personToCam.length();
2295
- this._raycasterPersonToCam.set(origin, direction);
2296
- this._raycasterPersonToCam.far = desiredDist;
2297
- const intersectsCamera = this._raycasterPersonToCam.intersectObject(
2298
- this.collider,
2299
- false
1956
+ this.updateCameraWithRaycast(
1957
+ this.player.position,
1958
+ this.personToCam.subVectors(this.camera.position, this.player.position).length(),
1959
+ this.maxCamDistance
2300
1960
  );
2301
- if (intersectsCamera.length > 0) {
2302
- const hit = intersectsCamera[0];
2303
- const safeDist = Math.max(
2304
- hit.distance - this._camEpsilon,
2305
- this.minCamDistance
2306
- );
2307
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
2308
- this.camera.position.lerp(
2309
- targetCamPos,
2310
- this._camCollisionLerp
2311
- );
2312
- } else {
2313
- this._raycasterPersonToCam.far = this.maxCamDistance;
2314
- const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(
2315
- this.collider,
2316
- false
2317
- );
2318
- let safeDist = this.maxCamDistance;
2319
- if (intersectsMaxDis.length) {
2320
- const hitMax = intersectsMaxDis[0];
2321
- safeDist = hitMax.distance - this._camEpsilon;
2322
- }
2323
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
2324
- this.camera.position.lerp(
2325
- targetCamPos,
2326
- this._camCollisionLerp
2327
- );
2328
- }
2329
1961
  }
2330
1962
  }
2331
1963
  if (this.player.position.y < this.boundingBoxMinY - 1) {
2332
- this._originTmp.set(
1964
+ this.raycaster.ray.origin.set(this.player.position.x, 1e4, this.player.position.z);
1965
+ const fallHits = this.raycaster.intersectObject(this.collider, false);
1966
+ this.reset(new THREE4.Vector3(
2333
1967
  this.player.position.x,
2334
- 1e4,
1968
+ fallHits.length > 0 ? fallHits[0].point.y + 5 : this.player.position.y + 15,
2335
1969
  this.player.position.z
2336
- );
2337
- this._raycaster.ray.origin.copy(this._originTmp);
2338
- const intersectsFall = this._raycaster.intersectObject(
2339
- this.collider,
2340
- false
2341
- );
2342
- if (intersectsFall.length > 0) {
2343
- console.log("\u73A9\u5BB6\u4E3Abug\u610F\u5916\u6389\u843D");
2344
- this.reset(
2345
- new THREE3.Vector3(
2346
- this.player.position.x,
2347
- intersectsFall[0].point.y + 5,
2348
- this.player.position.z
2349
- )
2350
- );
2351
- } else {
2352
- console.log("\u73A9\u5BB6\u6B63\u5E38\u6389\u843D");
2353
- this.reset(
2354
- new THREE3.Vector3(
2355
- this.player.position.x,
2356
- this.player.position.y + 15,
2357
- this.player.position.z
2358
- )
2359
- );
2360
- }
1970
+ ));
2361
1971
  }
2362
- if (this.isShowMobileControls) {
2363
- if (this.vehicles.length) {
2364
- let near = false;
2365
- for (const v of this.vehicles) {
2366
- this.nearCheckLocal.copy(v.boardingPoint).multiplyScalar(v.scale);
2367
- v.vehicleGroup.localToWorld(
2368
- this.nearCheckWorld.copy(this.nearCheckLocal)
2369
- );
2370
- if (this.player.position.distanceTo(this.nearCheckWorld) < 800 * this.playerModel.scale) {
2371
- near = true;
2372
- this.syncVehicleBtnEl(near);
2373
- break;
2374
- }
2375
- }
2376
- if (near !== this.isNearVehicle) {
2377
- this.isNearVehicle = near;
2378
- this.syncVehicleBtnEl(near);
1972
+ if (this.isShowMobileControls && this.vehicles.length) {
1973
+ let near = false;
1974
+ for (const v of this.vehicles) {
1975
+ this.nearCheckLocal.copy(v.boardingPoint).multiplyScalar(v.scale);
1976
+ v.vehicleGroup.localToWorld(this.nearCheckWorld.copy(this.nearCheckLocal));
1977
+ if (this.player.position.distanceTo(this.nearCheckWorld) < 800 * this.playerModel.scale) {
1978
+ near = true;
1979
+ break;
2379
1980
  }
2380
- } else {
2381
- this.isNearVehicle = false;
2382
- this.syncVehicleBtnEl(false);
2383
1981
  }
1982
+ if (near !== this.isNearVehicle) {
1983
+ this.isNearVehicle = near;
1984
+ this.mobileControls?.syncVehicleBtn(near);
1985
+ }
1986
+ }
1987
+ }
1988
+ // 相机碰撞射线
1989
+ updateCameraWithRaycast(origin, desiredDist, maxDist) {
1990
+ this.personToCam.subVectors(this.camera.position, origin);
1991
+ const direction = this.personToCam.clone().normalize();
1992
+ this.raycasterPersonToCam.set(origin, direction);
1993
+ this.raycasterPersonToCam.far = desiredDist;
1994
+ const hits = this.raycasterPersonToCam.intersectObject(this.collider, false);
1995
+ if (hits.length > 0) {
1996
+ const safeDist = Math.max(hits[0].distance - this.camEpsilon, this.minCamDistance);
1997
+ const targetCamPos = origin.clone().add(direction.multiplyScalar(safeDist));
1998
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
1999
+ } else {
2000
+ this.raycasterPersonToCam.far = maxDist;
2001
+ const maxHits = this.raycasterPersonToCam.intersectObject(this.collider, false);
2002
+ const safeDist = maxHits.length > 0 ? Math.min(maxDist, maxHits[0].distance - this.camEpsilon) : maxDist;
2003
+ const targetCamPos = origin.clone().add(direction.multiplyScalar(safeDist));
2004
+ this.camera.position.lerp(targetCamPos, this.camCollisionLerp);
2384
2005
  }
2385
2006
  }
2386
- /**
2387
- * 获取屏幕中心点向前射线与碰撞体的交点
2388
- */
2007
+ // 屏幕中心射线
2389
2008
  getCenterScreenRaycastHit() {
2390
2009
  this.camera.updateMatrixWorld();
2391
2010
  this.centerRay.setFromCamera(this.centerMouse, this.camera);
2392
- const intersects = this.centerRay.intersectObject(this.collider, false);
2393
- return intersects[0];
2011
+ return this.centerRay.intersectObject(this.collider, false)[0];
2012
+ }
2013
+ // 获取当前人物动画名称
2014
+ getCurrentPersonAnimationName() {
2015
+ return this.actionState._clip.name;
2394
2016
  }
2395
- /**
2396
- * 更新模型动画
2397
- */
2017
+ // 更新动画混合器
2398
2018
  updateMixers(delta) {
2399
- if (this.personMixer) this.personMixer.update(delta);
2400
- for (const v of this.vehicles) {
2401
- v.vehicleMixer?.update(delta);
2402
- }
2019
+ this.personMixer?.update(delta);
2020
+ for (const v of this.vehicles) v.vehicleMixer?.update(delta);
2403
2021
  }
2022
+ // 重置玩家位置
2404
2023
  reset(position) {
2405
2024
  if (!this.player) return;
2406
2025
  this.playerVelocity.set(0, 0, 0);
2407
2026
  this.player.position.copy(position ?? this.initPos);
2408
2027
  }
2028
+ // 获取玩家位置
2409
2029
  getPosition() {
2410
2030
  return this.player?.position;
2411
2031
  }
2412
2032
  // ==================== 输入处理 ====================
2033
+ // 外部输入接口
2413
2034
  setInput(input) {
2414
2035
  if (typeof input.moveX === "number") {
2415
2036
  this.lftPressed = input.moveX === -1;
@@ -2426,14 +2047,9 @@ var PlayerController = class {
2426
2047
  }
2427
2048
  if (typeof input.jump === "boolean") {
2428
2049
  if (input.jump) {
2429
- if (this.isMovingToBoardingPoint) {
2430
- this.isMovingToBoardingPoint = false;
2431
- this.boardingWaypoints = [];
2432
- this.currentWaypointIndex = 0;
2433
- this.boardingTargetDir = null;
2434
- }
2050
+ this.cancelBoarding();
2435
2051
  this.spacePressed = true;
2436
- if (this.controllerMode == 1) return;
2052
+ if (this.controllerMode === 1) return;
2437
2053
  if (!this.playerIsOnGround || this.isFlying) return;
2438
2054
  this.playPersonAnimationByName("jumping");
2439
2055
  this.playerVelocity.y = this.jumpHeight;
@@ -2442,344 +2058,91 @@ var PlayerController = class {
2442
2058
  this.spacePressed = false;
2443
2059
  }
2444
2060
  }
2445
- if (typeof input.shift === "boolean") {
2446
- this.shiftPressed = input.shift;
2447
- }
2448
- if (input.toggleView) {
2449
- this.changeView();
2450
- }
2451
- if (input.toggleFly && this.playerFlyEnabled && this.controllerMode == 0) {
2061
+ if (typeof input.shift === "boolean") this.shiftPressed = input.shift;
2062
+ if (input.toggleView) this.changeView();
2063
+ if (input.toggleFly && this.playerFlyEnabled && this.controllerMode === 0) {
2452
2064
  this.isFlying = !this.isFlying;
2453
2065
  this.setAnimationByPressed();
2454
- if (!this.isFlying && !this.playerIsOnGround) {
2455
- this.playPersonAnimationByName("jumping");
2456
- }
2066
+ if (!this.isFlying && !this.playerIsOnGround) this.playPersonAnimationByName("jumping");
2457
2067
  }
2458
2068
  if (input.toggleVehicle) {
2459
- if (this.controllerMode == 0) {
2460
- this.enterVehicle();
2461
- } else {
2462
- this.exitVehicle();
2463
- }
2069
+ if (this.controllerMode === 0) this.enterVehicle();
2070
+ else this.exitVehicle();
2464
2071
  }
2465
2072
  }
2073
+ // 取消上车寻路
2074
+ cancelBoarding() {
2075
+ this.isMovingToBoardingPoint = false;
2076
+ this.boardingWaypoints = [];
2077
+ this.currentWaypointIndex = 0;
2078
+ this.boardingTargetDir = null;
2079
+ }
2080
+ // 注册所有事件
2466
2081
  onAllEvent() {
2467
2082
  this.isupdate = true;
2468
2083
  this.setPointerLock();
2469
- window.addEventListener("keydown", this._boundOnKeydown);
2470
- window.addEventListener("keyup", this._boundOnKeyup);
2471
- window.addEventListener("mousemove", this._mouseMove);
2472
- window.addEventListener("click", this._mouseClick);
2084
+ window.addEventListener("keydown", this.boundOnKeydown);
2085
+ window.addEventListener("keyup", this.boundOnKeyup);
2086
+ window.addEventListener("mousemove", this.mouseMove);
2087
+ window.addEventListener("click", this.mouseClick);
2473
2088
  }
2089
+ // 注销所有事件
2474
2090
  offAllEvent() {
2475
2091
  this.isupdate = false;
2476
2092
  document.exitPointerLock();
2477
- window.removeEventListener("keydown", this._boundOnKeydown);
2478
- window.removeEventListener("keyup", this._boundOnKeyup);
2479
- window.removeEventListener("mousemove", this._mouseMove);
2480
- window.removeEventListener("click", this._mouseClick);
2481
- }
2482
- async initMobileControls() {
2483
- this.controls.maxPolarAngle = Math.PI * (300 / 360);
2484
- this.controls.touches = { ONE: null, TWO: null };
2485
- this.nippleModule = await import("nipplejs");
2486
- const nipple = this.nippleModule?.default;
2487
- const JOY_SIZE = 120;
2488
- const container = document.body;
2489
- this.joystickZoneEl = document.createElement("div");
2490
- this.joystickZoneEl.id = "joy-zone";
2491
- Object.assign(this.joystickZoneEl.style, {
2492
- position: "absolute",
2493
- left: "16px",
2494
- bottom: "16px",
2495
- width: `${JOY_SIZE + 40}px`,
2496
- height: `${JOY_SIZE + 40}px`,
2497
- touchAction: "none",
2498
- zIndex: "999",
2499
- pointerEvents: "auto",
2500
- WebkitUserSelect: "none",
2501
- userSelect: "none"
2502
- });
2503
- container.appendChild(this.joystickZoneEl);
2504
- ["touchstart", "touchmove", "touchend", "touchcancel"].forEach(
2505
- (evtName) => {
2506
- this.joystickZoneEl?.addEventListener(
2507
- evtName,
2508
- (e) => e.preventDefault(),
2509
- {
2510
- passive: false
2511
- }
2512
- );
2513
- }
2514
- );
2515
- this.joystickManager = nipple.create({
2516
- zone: this.joystickZoneEl,
2517
- mode: "static",
2518
- position: {
2519
- left: `${(JOY_SIZE + 40) / 2}px`,
2520
- bottom: `${(JOY_SIZE + 40) / 2}px`
2521
- },
2522
- color: "#ffffff",
2523
- size: JOY_SIZE,
2524
- multitouch: true,
2525
- maxNumberOfNipples: 1
2526
- });
2527
- this.joystickManager.on("move", (_evt, data) => {
2528
- if (!data) return;
2529
- const rawX = data.vector?.x ?? 0;
2530
- const rawY = data.vector?.y ?? 0;
2531
- const distance = data.distance ?? 0;
2532
- const deadzone = 0.5;
2533
- const dirX = rawX > deadzone ? 1 : rawX < -deadzone ? -1 : 0;
2534
- const dirY = rawY > deadzone ? 1 : rawY < -deadzone ? -1 : 0;
2535
- const sprintThreshold = JOY_SIZE / 2;
2536
- const isSprinting = distance >= sprintThreshold;
2537
- const prev = this.prevJoyState || {
2538
- dirX: 0,
2539
- dirY: 0,
2540
- shift: false
2541
- };
2542
- if (dirX === prev.dirX && dirY === prev.dirY && isSprinting === prev.shift)
2543
- return;
2544
- this.prevJoyState = { dirX, dirY, shift: isSprinting };
2545
- this.setInput({ moveX: dirX, moveY: dirY, shift: isSprinting });
2546
- });
2547
- this.joystickManager.on("end", () => {
2548
- const prev = this.prevJoyState || {
2549
- dirX: 0,
2550
- dirY: 0,
2551
- shift: false
2552
- };
2553
- if (prev.dirX !== 0 || prev.dirY !== 0 || prev.shift !== false) {
2554
- this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
2555
- this.setInput({ moveX: 0, moveY: 0, shift: false });
2556
- }
2557
- });
2558
- this.lookAreaEl = document.createElement("div");
2559
- Object.assign(this.lookAreaEl.style, {
2560
- position: "absolute",
2561
- right: "0",
2562
- bottom: "0",
2563
- width: "50%",
2564
- height: "100%",
2565
- zIndex: "998",
2566
- touchAction: "none",
2567
- WebkitUserSelect: "none",
2568
- userSelect: "none"
2569
- });
2570
- container.appendChild(this.lookAreaEl);
2571
- ["touchstart", "touchmove", "touchend", "touchcancel"].forEach(
2572
- (evtName) => {
2573
- this.lookAreaEl?.addEventListener(
2574
- evtName,
2575
- (e) => e.preventDefault(),
2576
- {
2577
- passive: false
2578
- }
2579
- );
2580
- }
2581
- );
2582
- this.lookAreaEl.addEventListener("pointerdown", this.onPointerDown, {
2583
- passive: false
2584
- });
2585
- this.lookAreaEl.addEventListener("pointermove", this.onPointerMove, {
2586
- passive: false
2587
- });
2588
- this.lookAreaEl.addEventListener("pointerup", this.onPointerUp, {
2589
- passive: false
2590
- });
2591
- this.lookAreaEl.addEventListener("pointercancel", this.onPointerUp, {
2592
- passive: false
2593
- });
2594
- const createBtn = (rightPx, bottomPx, bgUrl) => {
2595
- const btn = document.createElement("button");
2596
- const styles = {
2597
- position: "absolute",
2598
- right: `${rightPx}px`,
2599
- bottom: `${bottomPx}px`,
2600
- width: "56px",
2601
- height: "56px",
2602
- zIndex: "1000",
2603
- borderRadius: "50%",
2604
- border: "2px solid black",
2605
- background: "rgba(0,0,0)",
2606
- padding: "20px",
2607
- opacity: "0.95",
2608
- touchAction: "none",
2609
- fontSize: "14px",
2610
- userSelect: "none",
2611
- overflow: "hidden",
2612
- boxSizing: "border-box",
2613
- backgroundColor: "transparent",
2614
- backgroundRepeat: "no-repeat, no-repeat",
2615
- backgroundPosition: "center center, center center",
2616
- backgroundSize: "100% 100%, 80% 80%"
2617
- };
2618
- if (bgUrl) {
2619
- const overlayColor = "rgba(0,0,0,0.5)";
2620
- styles.backgroundImage = `linear-gradient(${overlayColor}, ${overlayColor}), url("${bgUrl}")`;
2621
- }
2622
- Object.assign(btn.style, styles);
2623
- container.appendChild(btn);
2624
- ["touchstart", "touchend", "touchcancel"].forEach((evtName) => {
2625
- btn.addEventListener(evtName, (e) => e.preventDefault(), {
2626
- passive: false
2627
- });
2628
- });
2629
- return btn;
2630
- };
2631
- this.jumpBtnEl = createBtn(14, 14, jump_default);
2632
- this.jumpBtnEl.addEventListener(
2633
- "touchstart",
2634
- (e) => {
2635
- e.preventDefault();
2636
- this.setInput({ jump: true });
2637
- },
2638
- { passive: false }
2639
- );
2640
- this.jumpBtnEl.addEventListener(
2641
- "touchend",
2642
- (e) => {
2643
- e.preventDefault();
2644
- this.setInput({ jump: false });
2645
- },
2646
- { passive: false }
2647
- );
2648
- this.jumpBtnEl.addEventListener(
2649
- "touchcancel",
2650
- (e) => {
2651
- e.preventDefault();
2652
- this.setInput({ jump: false });
2653
- },
2654
- { passive: false }
2655
- );
2656
- this.flyBtnEl = createBtn(14, 14 + 80, fly_default);
2657
- this.flyBtnEl.addEventListener(
2658
- "touchstart",
2659
- (e) => {
2660
- e.preventDefault();
2661
- this.setInput({ toggleFly: true });
2662
- },
2663
- { passive: false }
2664
- );
2665
- this.viewBtnEl = createBtn(14, 14 + 200, view_default);
2666
- this.viewBtnEl.addEventListener(
2667
- "touchstart",
2668
- (e) => {
2669
- e.preventDefault();
2670
- this.setInput({ toggleView: true });
2671
- },
2672
- { passive: false }
2673
- );
2674
- this.vehicleBtnEl = createBtn(14 + 100, 14 + 120, vehicle_default);
2675
- this.vehicleBtnEl.addEventListener(
2676
- "touchstart",
2677
- (e) => {
2678
- e.preventDefault();
2679
- this.setInput({ toggleVehicle: true });
2680
- },
2681
- { passive: false }
2682
- );
2683
- }
2684
- destroyMobileControls() {
2685
- try {
2686
- if (this.joystickManager && this.joystickManager.destroy) {
2687
- this.joystickManager.destroy();
2688
- this.joystickManager = null;
2689
- }
2690
- if (this.joystickZoneEl?.parentElement) {
2691
- this.joystickZoneEl.parentElement.removeChild(
2692
- this.joystickZoneEl
2693
- );
2694
- this.joystickZoneEl = null;
2695
- }
2696
- if (this.lookAreaEl?.parentElement) {
2697
- this.lookAreaEl.parentElement.removeChild(this.lookAreaEl);
2698
- this.lookAreaEl = null;
2699
- }
2700
- if (this.jumpBtnEl?.parentElement) {
2701
- this.jumpBtnEl.parentElement.removeChild(this.jumpBtnEl);
2702
- this.jumpBtnEl = null;
2703
- }
2704
- if (this.flyBtnEl?.parentElement) {
2705
- this.flyBtnEl.parentElement.removeChild(this.flyBtnEl);
2706
- this.flyBtnEl = null;
2707
- }
2708
- if (this.viewBtnEl?.parentElement) {
2709
- this.viewBtnEl.parentElement.removeChild(this.viewBtnEl);
2710
- this.viewBtnEl = null;
2711
- }
2712
- if (this.vehicleBtnEl?.parentElement) {
2713
- this.vehicleBtnEl.parentElement.removeChild(this.vehicleBtnEl);
2714
- this.vehicleBtnEl = null;
2715
- }
2716
- this.lookAreaEl?.removeEventListener(
2717
- "pointerdown",
2718
- this.onPointerDown
2719
- );
2720
- this.lookAreaEl?.removeEventListener(
2721
- "pointermove",
2722
- this.onPointerMove
2723
- );
2724
- this.lookAreaEl?.removeEventListener("pointerup", this.onPointerUp);
2725
- this.lookAreaEl?.removeEventListener(
2726
- "pointercancel",
2727
- this.onPointerUp
2728
- );
2729
- } catch (e) {
2730
- console.warn("\u9500\u6BC1\u79FB\u52A8\u7AEF\u6447\u6746\u63A7\u5236\u65F6\u51FA\u9519\uFF1A", e);
2731
- }
2732
- }
2733
- syncVehicleBtnEl(show) {
2734
- if (!this.vehicleBtnEl) return;
2735
- this.vehicleBtnEl.style.display = show ? "block" : "none";
2736
- }
2737
- syncControllerModeBtnEl() {
2738
- if (!this.isShowMobileControls) return;
2739
- if (this.controllerMode == 0) {
2740
- this.flyBtnEl.style.display = "block";
2741
- const overlayColor = "rgba(0,0,0,0.5)";
2742
- this.jumpBtnEl.style.backgroundImage = `linear-gradient(${overlayColor}, ${overlayColor}), url("${jump_default}")`;
2743
- } else {
2744
- this.flyBtnEl.style.display = "none";
2745
- const overlayColor = "rgba(0,0,0,0.5)";
2746
- this.jumpBtnEl.style.backgroundImage = `linear-gradient(${overlayColor}, ${overlayColor}), url("${break_default}")`;
2747
- this.jumpBtnEl.style.backgroundImage = `url(${break_default})`;
2748
- }
2749
- }
2750
- // ==================== 更新参数 ====================
2751
- setMouseSensitivity(mouseSensity) {
2752
- this.mouseSensity = mouseSensity;
2753
- this.controls.rotateSpeed = this.mouseSensity * 0.05;
2754
- }
2093
+ window.removeEventListener("keydown", this.boundOnKeydown);
2094
+ window.removeEventListener("keyup", this.boundOnKeyup);
2095
+ window.removeEventListener("mousemove", this.mouseMove);
2096
+ window.removeEventListener("click", this.mouseClick);
2097
+ }
2098
+ // ==================== 移动端同步 ====================
2099
+ // 同步移动端按钮
2100
+ syncMobileControllerMode() {
2101
+ this.mobileControls?.syncControllerModeBtn(this.controllerMode);
2102
+ }
2103
+ // ==================== Setters ====================
2104
+ // 设置鼠标灵敏度
2105
+ setMouseSensitivity(value) {
2106
+ this.mouseSensitivity = value;
2107
+ this.controls.rotateSpeed = value * 0.05;
2108
+ }
2109
+ // 设置重力
2755
2110
  setGravity(gravity) {
2756
2111
  this.gravity = gravity * this.playerModel.scale;
2757
2112
  }
2113
+ // 设置跳跃高度
2758
2114
  setJumpHeight(jumpHeight) {
2759
2115
  this.jumpHeight = jumpHeight * this.playerModel.scale;
2760
2116
  }
2761
- setPlayerSpeed(playerSpeed) {
2762
- this.playerSpeed = playerSpeed * this.playerModel.scale;
2117
+ // 设置移动速度
2118
+ setPlayerSpeed(speed) {
2119
+ this.playerSpeed = speed * this.playerModel.scale;
2763
2120
  this.curPlayerSpeed = this.playerSpeed;
2764
2121
  }
2765
- setPlayerFlySpeed(playerFlySpeed) {
2766
- this.playerFlySpeed = playerFlySpeed * this.playerModel.scale;
2122
+ // 设置飞行速度
2123
+ setPlayerFlySpeed(flySpeed) {
2124
+ this.playerFlySpeed = flySpeed * this.playerModel.scale;
2767
2125
  }
2768
- setMinCamDistance(minCamDistance) {
2769
- this.minCamDistance = minCamDistance * this.playerModel.scale;
2126
+ // 设置最小相机距离
2127
+ setMinCamDistance(dist) {
2128
+ this.minCamDistance = dist * this.playerModel.scale;
2770
2129
  }
2771
- setMaxCamDistance(maxCamDistance) {
2772
- this.maxCamDistance = maxCamDistance * this.playerModel.scale;
2130
+ // 设置最大相机距离
2131
+ setMaxCamDistance(dist) {
2132
+ this.maxCamDistance = dist * this.playerModel.scale;
2773
2133
  this.orginMaxCamDistance = this.maxCamDistance;
2774
2134
  }
2775
- setThirdMouseMode(thirdMouseMode) {
2776
- this.thirdMouseMode = thirdMouseMode;
2135
+ // 设置鼠标模式
2136
+ setThirdMouseMode(mode) {
2137
+ this.thirdMouseMode = mode;
2777
2138
  this.setPointerLock();
2778
2139
  }
2779
- setEnableZoom(enableZoom) {
2780
- this.enableZoom = enableZoom;
2781
- this.controls.enableZoom = this.enableZoom;
2140
+ // 设置滚轮缩放
2141
+ setEnableZoom(enable) {
2142
+ this.enableZoom = enable;
2143
+ this.controls.enableZoom = enable;
2782
2144
  }
2145
+ // 设置调试显示
2783
2146
  setDebug(debug) {
2784
2147
  if (this.collider) this.scene.remove(this.collider);
2785
2148
  if (debug) {
@@ -2790,6 +2153,7 @@ var PlayerController = class {
2790
2153
  }
2791
2154
  }
2792
2155
  // ==================== 销毁 ====================
2156
+ // 销毁控制器
2793
2157
  destroy() {
2794
2158
  this.offAllEvent();
2795
2159
  if (this.player) {
@@ -2810,7 +2174,8 @@ var PlayerController = class {
2810
2174
  this.scene.remove(this.collider);
2811
2175
  this.collider = null;
2812
2176
  }
2813
- this.destroyMobileControls();
2177
+ this.mobileControls?.destroy();
2178
+ this.mobileControls = null;
2814
2179
  for (const v of this.vehicles) {
2815
2180
  this.scene.remove(v.vehicleGroup);
2816
2181
  v.pathPlanner?.dispose();
@@ -2824,31 +2189,34 @@ function playerController() {
2824
2189
  if (!controllerInstance) controllerInstance = new PlayerController();
2825
2190
  const c = controllerInstance;
2826
2191
  return {
2827
- init: (opts, callback) => c.init(opts, callback),
2828
- loadVehicleModel: (params) => c.loadVehicleModel(params),
2829
- changeView: () => c.changeView(),
2830
- reset: (pos) => c.reset(pos),
2192
+ init: (opts, cb) => c.init(opts, cb),
2193
+ loadVehicleModel: (opts) => c.loadVehicleModel(opts),
2831
2194
  update: (dt) => c.update(dt),
2832
2195
  destroy: () => c.destroy(),
2196
+ reset: (pos) => c.reset(pos),
2833
2197
  setInput: (i) => c.setInput(i),
2198
+ changeView: () => c.changeView(),
2834
2199
  getPosition: () => c.getPosition(),
2835
2200
  getCenterScreenRaycastHit: () => c.getCenterScreenRaycastHit(),
2201
+ getCurrentPersonAnimationName: () => c.getCurrentPersonAnimationName(),
2836
2202
  getPerson: () => c.person,
2837
2203
  getActiveVehicle: () => c.activeVehicle,
2838
2204
  getAllVehicles: () => c.vehicles,
2839
2205
  switchPlayerModel: (model) => c.switchPlayerModel(model),
2840
- setMouseSensitivity: (mouseSensity) => c.setMouseSensitivity(mouseSensity),
2841
- setGravity: (gravity) => c.setGravity(gravity),
2842
- setJumpHeight: (jumpHeight) => c.setJumpHeight(jumpHeight),
2843
- setPlayerSpeed: (playerSpeed) => c.setPlayerSpeed(playerSpeed),
2844
- setPlayerFlySpeed: (playerFlySpeed) => c.setPlayerFlySpeed(playerFlySpeed),
2845
- setMinCamDistance: (minCamDistance) => c.setMinCamDistance(minCamDistance),
2846
- setMaxCamDistance: (maxCamDistance) => c.setMaxCamDistance(maxCamDistance),
2847
- setThirdMouseMode: (thirdMouseMode) => c.setThirdMouseMode(thirdMouseMode),
2848
- setEnableZoom: (enableZoom) => c.setEnableZoom(enableZoom),
2849
- setDebug: (debug) => c.setDebug(debug),
2850
- setOverShoulderView: (enable) => c.setOverShoulderView(enable),
2851
- setPlayerScale: (scale) => c.setPlayerScale(scale)
2206
+ setPlayerScale: (scale) => c.setPlayerScale(scale),
2207
+ setMouseSensitivity: (v) => c.setMouseSensitivity(v),
2208
+ setGravity: (v) => c.setGravity(v),
2209
+ setJumpHeight: (v) => c.setJumpHeight(v),
2210
+ setPlayerSpeed: (v) => c.setPlayerSpeed(v),
2211
+ setPlayerFlySpeed: (v) => c.setPlayerFlySpeed(v),
2212
+ setMinCamDistance: (v) => c.setMinCamDistance(v),
2213
+ setMaxCamDistance: (v) => c.setMaxCamDistance(v),
2214
+ setThirdMouseMode: (v) => c.setThirdMouseMode(v),
2215
+ setEnableZoom: (v) => c.setEnableZoom(v),
2216
+ setDebug: (v) => c.setDebug(v),
2217
+ setOverShoulderView: (v) => c.setOverShoulderView(v),
2218
+ registerAnimation: (key, clipName, opts) => c.registerAnimation(key, clipName, opts),
2219
+ playAnimation: (key, opts) => c.playAnimation(key, opts)
2852
2220
  };
2853
2221
  }
2854
2222
  function onAllEvent() {
@@ -2856,8 +2224,7 @@ function onAllEvent() {
2856
2224
  controllerInstance.onAllEvent();
2857
2225
  }
2858
2226
  function offAllEvent() {
2859
- if (!controllerInstance) return;
2860
- controllerInstance.offAllEvent();
2227
+ controllerInstance?.offAllEvent();
2861
2228
  }
2862
2229
  export {
2863
2230
  offAllEvent,