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