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/README.md +80 -30
- package/dist/index.d.mts +63 -84
- package/dist/index.d.ts +63 -84
- package/dist/index.js +1162 -1794
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1161 -1794
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/playerController.ts
|
|
2
|
-
import * as
|
|
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,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAQAElEQVR4AeydC3qkthKFm7uxZFY2npVNsrK+5+9IHoyhKYEEetR8VKBpocep+lUCY+d/D//nCrgCmwo4IJvS+BeuwOPhgHgUuAJvFHBA3ohz5qvn8/l3sA/tsd/aL43zc/v7TJt+bX4FHJAMmirwgYFABwB9fD5V7e9gP7XHCP6lcX5un9erEo4/tP/Q9b7dpEBBQG4a0UXNKnCBgiCOMBDoAJCrB9RFnT/VFhttOSy51DXW44AYhaKYonQJBUHMV1cYbTksVyg9a8MBmYmxdRjB0PcsmwhUHd660YcIi2eVgq5wQN6IWyEYa711UNZUyXTOAVkRshEwlj13UJaKZPjcJiAZBr5VheBgGYWxjNkqVvN5ByWjdxyQIKbA4JEqT6RaBSOM5HMHKH5/8inHsQMHRLoBh3Y8UtUu6/aPasN+aR/th46x+DnuKaevsm5AwuPhXqDPKo6lsqEBERivx7YSKgccBDhG8P+Y/vvHHvvQx2j/6BiLn+OecvpqmtSfnNAAB5B4NpGwqduwgACHxMpxr/GCYpomAhwj+Dmn6o9t0zR9gUa1AMypOlUH2cQhkRAp25CACA4CBTi+aWU8QbD+UCCzseez8dL0YmoEYF6ZSVcDi3aHNockUbbhAAlwHF1SAQJAYBwnyn2uuEAhOwHLpJqOguKQSDzrNhQgJ+AABqDAOLbqW6ycYDkDCpCcyaDFxlVbxcMAcgKOXwrGasBYBpD6xnLxSDbhAQXXLqv0zzMFhgBEcPAk58iyCjCqDyIgkR1ZdpFJqh/fLF4vPxwCEKmaupxgrU/M5VxOqRtlN3WYYE/NJg7JG7d0D4iyRyocryXVG82q/uoEJGTZqsd2R+e6BkRwMKOmOB44uOYOX2RrM0DCY+GUOo8sQVPqb7Jst4AEOFKc3gUcMQoFCcvDFEi4aU/NtrG5bvfdAiKPDQuHxv7aDkKSknFf7fT8ny4BCdnD6rcOMsf2UA9AkjKxbDfcyTfdASI4mAGtTu4ajhijARLr0y2WWs3fh8Wxn913B4gEscLxUOAMEwhhrNyXSKLdjUe/TDS7BXsv0BUgyh4EvNWx1hm1mxgQJNy0myHpZuAnBtIVIAk6DLG02tDDOjGw1LJONhtNtX+6G0BC9jAtrzSTkmna996BEWjsZBDMcvXPh6VUx2W6ASTBR9YZNKHK5opaNRg+i/QEiGcPI6chi1ghMelqbLq5Yl0AEpZXFvGtQWGpq+kygmTYZWaK47oARAM2zXIeFFLq62a5Fxl6mdU8IJ49vkZ84idrRjVNQIltP1oo3zwgVpE9e3xXSpqQQbDvX349M+zj3h4A+eurL1c/WWfK1Ys7P2nSRpl6SEh6AGRIx+WCNmSRXNV1V0/TgGhWMz2JURCYynXnXfuALMusIe9DmgbE6H+L841VdVvMssxqKVNnc1TrgFjuP/7NptbgFSljDwdJ64AM57ASjGoJ6ll2Q9jWAdkY1p/Tcr7ff/yR492RQ7KiTrOAjJjuV/x39anhbtSbBcQYGT4rGoVSMb9XkwjL7TsgyxL1fvb7j7y+8clkRc+WAVkZzrdTPit+k8RPpCjQOyApWnhZV+CbAg7IN0n8xBsFhlvWOiBvosG/cgUuBcTlbl6B4W7kewfE8ipK81HrAyinQO+AlFOuv5qHu7+wuLBlQIZL9xaHepm8CrQMiEUJnxUtKtnLDPdzpWYBmaZpnkHsLvaSWwr4/dqKMs0CsjKW1VP+UuOqLGsnLdl2uEmpdUAsDrM4fi1ghjlnnURGzNqtA2IJYl867Kvkk8iGRq0D4r9LveHYAqctWhdo9t4qmwbEmvKtS4h1VwxxdrhfhLJ6tWlAwiAt9yEeAEGs5U6Th/VXki06L6tv/nMPgFic4Gtsi0pvyliz9ZsqmvyqB0BMa2PNlA7JeohasqtJ4/Xq2z7bPCBhZrOkf0sgtO3NxN5r0rAurxJr7qd484AkuKK+/89FQucLFTVNGpqEhgWpF0CsSwBTQBQKxqqqTcgeVm2rGl+uznQBiGY4lljYni5kkWFnw4U4pslC2g6tVxeABMdbZzpTYIQ6u9x59rC7tRtANNORQbDd0ScEyG5drRUIYzdNEtJ06OyBb7sBhMHIzFlEgdL1Y19psbWZ4NDFVi1VtN+tK0A045FBMIvHfo8GicZrzgjS0lzWInarZboCJDghZeazzqah6nZ3AQ7reFM0bFcUQ8+7A0QzHxnE6uAhnmolwvGQhp49AjzdAcK4Eh38MwQQl3ZnGhv3WtbMwfitkwtlu7cuAQle+xH2ll2XkAQ4flsECGV+GSaXUHSMXbeAyNEpSy283RUkB+DwpRVRsLBuAWGcgoS1NKDw0WJdQHIEDomTknFVfIyta0CCC1PX1E1DIjiYFFKWVcjE0iplIuGaIax7QJRFcHzq7AgkirUnwdZMIKjDgJFyQ87YgKOpcdLpq6x7QBAyQJKaSbgUUKoPHoHB4+qnOswTK+3MW2VwmPt9WcEhAEFNQUKgH4VEMfjkeqqqxtQpwCBrYKn9cjgMig0DCFqcgITLX9lEQXk7KOrDHIzUrMFY/glacOz2RoGhAEGHEBhHMgmXs74HFN7juhyUDGAwBjJH6j0Z1w1pwwGClwMkZ4KEWRtQFLPPD/2Hz1Sd3ag7GMso7ExbwPGRvZMdVzgkIPhTkMSnW+w5ddTIKmQUxfETWLAzQfxQRa8llPYAEe1UnRrc2HBIgCPbsIAgFpDIyCRHl1xUMzdgwSIw7AFmaQCAxfOUi/ZUhUABEJg+ntqYAH5onB+nahn04qEBiT4PwZMLklgtewIcYJYGAFg8T7loXJfLyBrAASS56hyqHgckuBtIZJM+lgBF1V66AQRgeNY4KbsDshBQkBBULUPiWWPh0zMfHZAV9YBE1lo28ayx4suzpyyAnG2j2esFyYesdlDIGHSTJRWQNKt3jR13QAxeUfTNQalh+QUIAKGuTSwJDaPwIkcUcEASVFM0Ago26TJAwXR4yQYUtAcYGJ8vaXjkRhyQg95fwBJ/lpIzaKkLAwY1N7EHTs4d7LVflqqAA5Kq2Er5aZpeL/9pTxBrN00qFqFh1o9GcC8tfkf5l03//aMujPKqzrc7FLgZkDuGfE2bivEIDbN+NAJ+afE7yr/smh56KxYFHBCLSl5mWAUckGFd7wO3KOCAWFQylHk+n7x8iPECYnzxcLlXsS/b/Huumxt18X6WoXUvUkoBB+SAsgpxgpdgjgEe38CNLyAS2Gu2bG1eJr64GPfURf1q7rXRHsY1y3r8cyEF+gUko2AKT4AgWLEIA4FMsGIZW9usivawVx/UJ/YA87F5hX9xWgEHZEVCBR9AYARhBAIQsJUrbjlFXwAm/majuv10WDK7wgGZCaoIe0GhU6/ljfYEoXbNbBEWwHZYMrhteEAiFNo/pSdgtAaFuv1tYwwRFl+GfZPHfmJYQATEPFsQUHbV2io5X4Z5Vkn03XCA5AAjUeOain9mlZo6VXNfhgFkcDCWMeigLBXZ+Nw9IBeDwYuF0XgJ8fXyobT/sp/0b3GOsnOLdahY0c1B2ZG3W0AuACMG8Sv4FfNs8xcReQnx9fKhvviyxyeLc5SdW6xn+VYwbXJ5bnNQNhTtEhDBwc1o7idSBCdG8Cq+J/bYK/g39D19evrzKj0A0d4SmtNtzCoAFJ569fzQYjbc/cOuABEY8ckUT272R79f4gWEihGY0TinU0U2U6ULaACG5VmufqEdP0dxSOSNbgARHDmzBsH2CQQBKa2q3dS/eXYBlhx9BZQc9TRdR/OACIxcWSNCoXibgIPPzTl3miZgmdRxQMF0eGhD1+GzSNOAAIdcf/ZeAxB+KLAwjlVl+5vGAyhYhKXaQeFHGcs6jHsgVgNV9LdZQCQoIgLHUSGBASgwjo/WU/11AZYqQZEf8SFGtsJY2r0eFtQgbJOABFER8oiGwAAUGMdH6mjwmscDUB6PB8suTIfvN5Uvqo/8yCT390YvgGTru41L8p9uDhCJGmebI2rwVwiHA2MulIKeZReBuQcJP9+ZX1rieG+Sw9cl2jXX2QwgAoObRgQ7MqswEwIGgWEWp+eCAZS47EKfOFyO0Yp9PHfbXn7H57e13wQgEgkoEIp9qlg4G6vC4amdL10+gII+OnxtHNekFRPjEb9nka56QGZwpA4YJ9fm7NQx9F4eH1nGyORoKZe9TNWAnIBj+HuN7JHyrsLj3+3dB33WrFi4BZJqAZEgpNUjopA1/F7jM7TqPdCCjgxiheSWpVa1gMitqXAgNnCw1+W+taCAIGEys/osNSZOS1AlIMoeqULwRq3DcTocbqvAmkUeB2Lj1KCqAyQIwPLKOrAXHNbCXq4+BZRFyCBWSC5dalUFiOAg3abA8boZr8/l3qNUBQQJvgeUL5dufNj7AePGZemnqwEkwJEycOBA1PRR+xW1KpCSRS7xfRWACA6yhsNRa9he1C9lETKIFZJL3tWqAhDpn3JTzj3HJbOH+uXbxQoIEnwLKJaWUyZVS33fytwOiLIHgnzr2MYJ4LjiJbqN5v30RQpYswg37Cnxk9z9WwEJcJhnAc0uDkeyi9u7QH4mg2CWzh9dalnqftwKiHpohkNlHQ6JMMomSFL8nRJHSRLeBkjIHtbO8sTKOqNY6/Ry9StghaTYUusWQAIcVuqBo+g6s/44GbOHyiJMiphFgCJLrVsA0WitcDwkksMhwUbd5H9rFkEic1xR2GKXAxKyh6VvlEkRh/JufSpgjYPsS61jgJxzgpVyllbW9HquR3511QooixAHmKWfWZdalwKSkj0kii+tLOEwSBnFgzWLoIh1EqbsW7sUEPXE2nHrD4pUpW8DKWCNC15dyiLLZYAkZA+WVp49sri3r0qURYgL01JL8ZYFkssAkatM2SOIoOK+uQKrClyaRS4BRDRD/upoFyd/LT4391Fj5UkKf2PW7fnMroECwjTRqlyW7RJA1FPToFrPHk8FhMbKm8mkd7fHo5QGD8M/01Jsr57igChohsgeYZwExJ7m/v0FCmiybQMQaTFE9rCOU+V8K69AFjjoZtEMEmZV2tkz643XXj3+vSuAAv/ynxxWFBBrB5UOrcswa5Vr5fzcGApk/TFBaUAsy6teske2tD5GHBcZJb9xmnWyLQZIwvKqiFI3VNoL6DdIl6VJMkfK6yimRosBotb/ku1uvSyvNA4yCA5ivztuL5BNAfTmr2pmzRyxdyUBsTzy7GrWBRIZzuJ/TAMsbo9HMQ2kNRt6A0mM6az7IoBYl1caXRHqsypkqux7IY2N9bDbNBXT4Lvq+c8UAcTYzWLUG9v3Yq7ArgKlALE8vcr2rHp3lF7AFTioQHZAtLyy3HvQXc8gqOBWtQLZAdFoTYCwRldZ31yBqhUoAYhlwF09vbIM+HAZv/BWBUoAYrn/uHXQ3rgrYFWgBCCWtoe5/+CebGYfOnZ7PotoYAm81DJZAZHz/f4jeAAtZPzy1NzIrm6PRxEN3xxFmgAAAvRJREFUpDfbR3BBll1WQNQjCyDdZw95CScBhkUPyeZbRgV+Bv2zVJkbEEunuv75R3AOM6RFi1vLdNw4kGSZnHID8lfHou8OzeHYlai5ArkBsVDb8xLLM0c9CFhicbe3uQHZbbDXAiF79Dq8FseVZSLOBogCxERsxz9B9+xRD0b88lRdgNSjjfekAgXu7AKv1/MUMUsfsmUQY2+yUG1sq8ZijN/t8SihAa8v8ctT/ILWI9e/qwHJ1e9W6/lXS8yXE30/5dbhY5omwMsaGw5IVjl3K8v2fH63JS+QRYGcgJhu0rP0us5KrLOX38zX6b/VXuUEZLWBgU6yBrYMl7/+nu0m0tJgX2WuHc3VgHT7mklY/5qziPWx+LXh4K0tFcgJiDU4ln3o6bM1izBmX2qhQuWWE5DdoWqW7XppofExSVgh8aXWbsTcXyAbIIbgsAbO/aqc6IF0YBIAFEst/lTLotKNZbIBwhhCcKyBwI/+CRyKjWBrGmyN25daW8pcfX6lvayAUD+QyF6bPvPDII5HguOhAZNBrJD4UkuBUuuWHZD5QEOgzE8Nc6yxMykAimXMvtSyqHRDmaKA3DCe2pq0ZhH67UstVKjMHJCCDlEWIYNYIWGpNfrbCAW9caxqB+SYbuarBEnKUos/9GCu2wuWVyAXIOV72nYL1izy0E/YHZKKfO2AXOAMZRFfal2gc4kmHJASqq7UKUh8qbWiS+2nHJBrPeRLrWv1Pt2aA3JaQnsFyiK+1LLLVUXJBgCpQqdsnRAkvtTKpmb5ihyQ8hqvtZCy1AKotTr83AUKOCAXiLxsQlkkZanlr6EsBbzwswNyodjzpgQJmQFQ5qf9uDIFHJB7HWJdavkrKDf5aWxAbhI9NqssQgbB4infV6aAA3KzQwTJ7l8CVBmWYzf3dMzmHZA6/P4Oknff1dH7jnvhgFTgXGUI/uDypK5wT8KSC4u/jcmxvvLtDgUckDtU32hToPD3ZQEDczA2dLrytANSSG2vtg8F/g8AAP//r/s6dQAAAAZJREFUAwCqeBInE6huXwAAAABJRU5ErkJggg==";
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
180
|
-
const
|
|
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
|
|
191
|
-
|
|
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
|
|
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
|
|
259
|
-
const
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
wheelObj.quaternion.copy(
|
|
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
|
-
|
|
643
|
+
vehicleGroup,
|
|
644
|
+
chassisBody,
|
|
645
|
+
vehicleController: vehicle,
|
|
285
646
|
updateWheelVisuals,
|
|
286
|
-
|
|
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
|
-
|
|
667
|
+
THREE4.Mesh.prototype.raycast = acceleratedRaycast;
|
|
292
668
|
var controllerInstance = null;
|
|
293
|
-
var clock = new
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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.
|
|
311
|
-
|
|
312
|
-
this.
|
|
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
|
-
|
|
336
|
-
},
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
424
|
-
this.
|
|
425
|
-
this.
|
|
426
|
-
this.
|
|
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
|
|
429
|
-
this.DIR_BKD = new
|
|
430
|
-
this.DIR_LFT = new
|
|
431
|
-
this.DIR_RGT = new
|
|
432
|
-
this.DIR_UP = new
|
|
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.
|
|
435
|
-
this.
|
|
436
|
-
this.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
);
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
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
|
|
884
|
+
if (this.controllerMode === 1) return;
|
|
565
885
|
if (!this.playerIsOnGround || this.isFlying) return;
|
|
566
|
-
|
|
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
|
|
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
|
|
590
|
-
|
|
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
|
-
|
|
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.
|
|
634
|
-
if (document.pointerLockElement
|
|
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.
|
|
666
|
-
this.
|
|
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 =
|
|
675
|
-
this.initPos = opts.initPos
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
this.
|
|
679
|
-
this.
|
|
680
|
-
this.
|
|
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.
|
|
684
|
-
this.
|
|
685
|
-
this.
|
|
686
|
-
this.
|
|
687
|
-
this.
|
|
688
|
-
this.
|
|
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.
|
|
691
|
-
this.
|
|
692
|
-
this.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
737
|
-
let
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
tmp.
|
|
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 =
|
|
752
|
-
for (let i = 0; i <
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1050
|
+
this.allAnimations = animations;
|
|
817
1051
|
this.personActions = /* @__PURE__ */ new Map();
|
|
818
|
-
const
|
|
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
|
|
832
|
-
|
|
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(
|
|
1070
|
+
action.setLoop(THREE4.LoopOnce, 1);
|
|
838
1071
|
action.clampWhenFinished = true;
|
|
839
1072
|
action.setEffectiveTimeScale(1.2);
|
|
840
1073
|
} else {
|
|
841
|
-
action.setLoop(
|
|
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.
|
|
850
|
-
this.
|
|
851
|
-
this.
|
|
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
|
|
863
|
-
if (
|
|
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 (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
887
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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 (
|
|
1139
|
-
console.error("\u52A0\u8F7D\u8F66\u8F86\u6A21\u578B\u5931\u8D25:",
|
|
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
|
|
1144
|
-
const center = new
|
|
1145
|
-
const size = new
|
|
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(
|
|
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
|
|
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
|
|
1276
|
-
const
|
|
1277
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
1327
|
+
const waypoint = this.boardingWaypoints[this.currentWaypointIndex];
|
|
1317
1328
|
const currentPos = this.player.position.clone();
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
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
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1428
|
+
// 设置第一人称相机
|
|
1429
|
+
setFirstPersonCamera(vertAngle = 0) {
|
|
1452
1430
|
this.controls.enabled = false;
|
|
1453
1431
|
if (this.personHead) {
|
|
1454
|
-
this.personHead
|
|
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(
|
|
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
|
-
|
|
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
|
|
1490
|
-
|
|
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.
|
|
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
|
-
|
|
1489
|
+
const sens = this.mouseSensitivity;
|
|
1490
|
+
if (this.controllerMode === 0) {
|
|
1517
1491
|
if (this.isFirstPerson) {
|
|
1518
1492
|
if (this.isMovingToBoardingPoint) return;
|
|
1519
|
-
|
|
1520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1554
|
-
|
|
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
|
-
|
|
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
|
|
1593
|
-
for (const g of collected)
|
|
1594
|
-
const
|
|
1595
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1625
|
-
for (const g of collected)
|
|
1626
|
-
|
|
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
|
|
1633
|
-
|
|
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
|
|
1638
|
-
|
|
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
|
|
1662
|
-
if (
|
|
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
|
|
1679
|
-
if (
|
|
1680
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
|
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((
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
|
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
|
-
|
|
1776
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
1845
|
-
|
|
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
|
-
|
|
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)
|
|
1857
|
-
vehicleController.setWheelEngineForce(
|
|
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(
|
|
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
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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 =
|
|
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
|
|
1903
|
-
const
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
);
|
|
1909
|
-
this.
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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.
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
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
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
const
|
|
1953
|
-
this.
|
|
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
|
-
|
|
1968
|
-
|
|
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
|
|
1972
|
-
chassisBody.setTranslation(
|
|
1973
|
-
|
|
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
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
)
|
|
2003
|
-
|
|
2004
|
-
|
|
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
|
|
2016
|
-
const
|
|
2017
|
-
vehicleGroup.position.set(
|
|
2018
|
-
|
|
2019
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2117
|
-
|
|
2118
|
-
)
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
this.
|
|
2122
|
-
this.
|
|
2123
|
-
this.
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
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
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
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
|
|
2221
|
-
const
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
)
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
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
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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.
|
|
2289
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
-
|
|
2393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2460
|
-
|
|
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.
|
|
2470
|
-
window.addEventListener("keyup", this.
|
|
2471
|
-
window.addEventListener("mousemove", this.
|
|
2472
|
-
window.addEventListener("click", this.
|
|
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.
|
|
2478
|
-
window.removeEventListener("keyup", this.
|
|
2479
|
-
window.removeEventListener("mousemove", this.
|
|
2480
|
-
window.removeEventListener("click", this.
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
this.
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
this.
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
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
|
-
|
|
2762
|
-
|
|
2117
|
+
// 设置移动速度
|
|
2118
|
+
setPlayerSpeed(speed) {
|
|
2119
|
+
this.playerSpeed = speed * this.playerModel.scale;
|
|
2763
2120
|
this.curPlayerSpeed = this.playerSpeed;
|
|
2764
2121
|
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2122
|
+
// 设置飞行速度
|
|
2123
|
+
setPlayerFlySpeed(flySpeed) {
|
|
2124
|
+
this.playerFlySpeed = flySpeed * this.playerModel.scale;
|
|
2767
2125
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2126
|
+
// 设置最小相机距离
|
|
2127
|
+
setMinCamDistance(dist) {
|
|
2128
|
+
this.minCamDistance = dist * this.playerModel.scale;
|
|
2770
2129
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2130
|
+
// 设置最大相机距离
|
|
2131
|
+
setMaxCamDistance(dist) {
|
|
2132
|
+
this.maxCamDistance = dist * this.playerModel.scale;
|
|
2773
2133
|
this.orginMaxCamDistance = this.maxCamDistance;
|
|
2774
2134
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2135
|
+
// 设置鼠标模式
|
|
2136
|
+
setThirdMouseMode(mode) {
|
|
2137
|
+
this.thirdMouseMode = mode;
|
|
2777
2138
|
this.setPointerLock();
|
|
2778
2139
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
this.
|
|
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.
|
|
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,
|
|
2828
|
-
loadVehicleModel: (
|
|
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
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
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
|
-
|
|
2860
|
-
controllerInstance.offAllEvent();
|
|
2227
|
+
controllerInstance?.offAllEvent();
|
|
2861
2228
|
}
|
|
2862
2229
|
export {
|
|
2863
2230
|
offAllEvent,
|