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