three-player-controller 0.2.51 → 0.3.0
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/LICENSE +21 -0
- package/README.md +52 -44
- package/dist/index.d.mts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +461 -442
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +461 -442
- package/dist/index.mjs.map +1 -1
- package/package.json +47 -42
package/dist/index.mjs
CHANGED
|
@@ -20,27 +20,28 @@ THREE.Mesh.prototype.raycast = acceleratedRaycast;
|
|
|
20
20
|
var controllerInstance = null;
|
|
21
21
|
var clock = new THREE.Clock();
|
|
22
22
|
var PlayerController = class {
|
|
23
|
-
// 射线检测时只返回第一个碰撞
|
|
24
23
|
constructor() {
|
|
24
|
+
// ==================== 基本配置与参数 ====================
|
|
25
25
|
this.loader = new GLTFLoader();
|
|
26
26
|
// 0: 隐藏鼠标控制朝向及视角,1: 隐藏鼠标仅控制视角,2: 显示鼠标拖拽控制朝向及视角, 3: 显示鼠标拖拽仅控制视角
|
|
27
|
+
// ==================== 玩家基本属性 ====================
|
|
27
28
|
this.playerRadius = 45;
|
|
28
29
|
this.playerHeight = 180;
|
|
29
30
|
this.isFirstPerson = false;
|
|
30
31
|
this.boundingBoxMinY = 0;
|
|
31
|
-
// 测试参数
|
|
32
|
+
// ==================== 测试参数 ====================
|
|
32
33
|
this.displayPlayer = false;
|
|
33
34
|
this.displayCollider = false;
|
|
34
35
|
this.displayVisualizer = false;
|
|
35
|
-
// 场景对象
|
|
36
|
+
// ==================== 场景对象 ====================
|
|
36
37
|
this.collider = null;
|
|
37
38
|
this.visualizer = null;
|
|
38
39
|
this.person = null;
|
|
39
|
-
// 状态开关
|
|
40
|
+
// ==================== 状态开关 ====================
|
|
40
41
|
this.playerIsOnGround = false;
|
|
41
42
|
this.isupdate = true;
|
|
42
43
|
this.isFlying = false;
|
|
43
|
-
// 输入状态
|
|
44
|
+
// ==================== 输入状态 ====================
|
|
44
45
|
this.fwdPressed = false;
|
|
45
46
|
this.bkdPressed = false;
|
|
46
47
|
this.lftPressed = false;
|
|
@@ -48,10 +49,9 @@ var PlayerController = class {
|
|
|
48
49
|
this.spacePressed = false;
|
|
49
50
|
this.ctPressed = false;
|
|
50
51
|
this.shiftPressed = false;
|
|
51
|
-
// 移动端输入
|
|
52
|
+
// ==================== 移动端输入 ====================
|
|
52
53
|
this.prevJoyState = { dirX: 0, dirY: 0, shift: false };
|
|
53
54
|
this.nippleModule = null;
|
|
54
|
-
// 移动控制相关
|
|
55
55
|
this.joystickManager = null;
|
|
56
56
|
this.joystickZoneEl = null;
|
|
57
57
|
this.lookAreaEl = null;
|
|
@@ -62,29 +62,28 @@ var PlayerController = class {
|
|
|
62
62
|
this.isLookDown = false;
|
|
63
63
|
this.lastTouchX = 0;
|
|
64
64
|
this.lastTouchY = 0;
|
|
65
|
-
//
|
|
65
|
+
// ==================== 第三人称相机参数 ====================
|
|
66
66
|
this._camCollisionLerp = 0.18;
|
|
67
67
|
// 平滑系数
|
|
68
68
|
this._camEpsilon = 0.35;
|
|
69
|
-
//
|
|
69
|
+
// 摄像机与障碍物之间的安全距离
|
|
70
70
|
this._minCamDistance = 1;
|
|
71
71
|
// 摄像机最小距离
|
|
72
72
|
this._maxCamDistance = 4.4;
|
|
73
73
|
// 摄像机最大距离
|
|
74
74
|
this.orginMaxCamDistance = 4.4;
|
|
75
|
-
// 物理/运动
|
|
75
|
+
// ==================== 物理/运动 ====================
|
|
76
76
|
this.playerVelocity = new THREE.Vector3();
|
|
77
77
|
// 玩家速度向量
|
|
78
78
|
this.upVector = new THREE.Vector3(0, 1, 0);
|
|
79
|
-
//
|
|
79
|
+
// ==================== 临时复用向量/矩阵 ====================
|
|
80
80
|
this.tempVector = new THREE.Vector3();
|
|
81
81
|
this.tempVector2 = new THREE.Vector3();
|
|
82
82
|
this.tempBox = new THREE.Box3();
|
|
83
83
|
this.tempMat = new THREE.Matrix4();
|
|
84
84
|
this.tempSegment = new THREE.Line3();
|
|
85
|
-
// 检测动画定时
|
|
86
85
|
this.recheckAnimTimer = null;
|
|
87
|
-
//
|
|
86
|
+
// ==================== 相机朝向/移动复用向量 ====================
|
|
88
87
|
this.camDir = new THREE.Vector3();
|
|
89
88
|
this.moveDir = new THREE.Vector3();
|
|
90
89
|
this.targetQuat = new THREE.Quaternion();
|
|
@@ -95,13 +94,65 @@ var PlayerController = class {
|
|
|
95
94
|
this.DIR_LFT = new THREE.Vector3(-1, 0, 0);
|
|
96
95
|
this.DIR_RGT = new THREE.Vector3(1, 0, 0);
|
|
97
96
|
this.DIR_UP = new THREE.Vector3(0, 1, 0);
|
|
97
|
+
// ==================== 射线检测 ====================
|
|
98
98
|
this._personToCam = new THREE.Vector3();
|
|
99
99
|
this._originTmp = new THREE.Vector3();
|
|
100
100
|
this._raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0));
|
|
101
101
|
this._raycasterPersonToCam = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3());
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* 根据按键设置人物动画
|
|
104
|
+
*/
|
|
105
|
+
this.setAnimationByPressed = () => {
|
|
106
|
+
this._maxCamDistance = this.orginMaxCamDistance;
|
|
107
|
+
if (this.isFlying) {
|
|
108
|
+
if (!this.fwdPressed) {
|
|
109
|
+
this.playPersonAnimationByName("flyidle");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.playPersonAnimationByName("flying");
|
|
113
|
+
this._maxCamDistance = this.orginMaxCamDistance * 2;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (this.playerIsOnGround) {
|
|
117
|
+
if (!this.fwdPressed && !this.bkdPressed && !this.lftPressed && !this.rgtPressed) {
|
|
118
|
+
this.playPersonAnimationByName("idle");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (this.fwdPressed) {
|
|
122
|
+
this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!this.isFirstPerson && (this.lftPressed || this.rgtPressed || this.bkdPressed)) {
|
|
126
|
+
this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (this.lftPressed) {
|
|
130
|
+
this.playPersonAnimationByName("left_walking");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (this.rgtPressed) {
|
|
134
|
+
this.playPersonAnimationByName("right_walking");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (this.bkdPressed) {
|
|
138
|
+
this.playPersonAnimationByName("walking_backward");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (this.recheckAnimTimer !== null) {
|
|
143
|
+
clearTimeout(this.recheckAnimTimer);
|
|
144
|
+
}
|
|
145
|
+
this.recheckAnimTimer = setTimeout(() => {
|
|
146
|
+
this.setAnimationByPressed();
|
|
147
|
+
this.recheckAnimTimer = null;
|
|
148
|
+
}, 200);
|
|
149
|
+
};
|
|
150
|
+
// ==================== 事件处理 ====================
|
|
151
|
+
/**
|
|
152
|
+
* 键盘按下事件
|
|
153
|
+
*/
|
|
103
154
|
this._boundOnKeydown = async (e) => {
|
|
104
|
-
if (e.ctrlKey &&
|
|
155
|
+
if (e.ctrlKey && ["KeyW", "KeyA", "KeyS", "KeyD"].includes(e.code)) {
|
|
105
156
|
e.preventDefault();
|
|
106
157
|
}
|
|
107
158
|
switch (e.code) {
|
|
@@ -124,11 +175,7 @@ var PlayerController = class {
|
|
|
124
175
|
case "ShiftLeft":
|
|
125
176
|
this.shiftPressed = true;
|
|
126
177
|
this.setAnimationByPressed();
|
|
127
|
-
this.controls.mouseButtons = {
|
|
128
|
-
LEFT: 2,
|
|
129
|
-
MIDDLE: 1,
|
|
130
|
-
RIGHT: 0
|
|
131
|
-
};
|
|
178
|
+
this.controls.mouseButtons = { LEFT: 2, MIDDLE: 1, RIGHT: 0 };
|
|
132
179
|
break;
|
|
133
180
|
case "Space":
|
|
134
181
|
this.spacePressed = true;
|
|
@@ -148,11 +195,15 @@ var PlayerController = class {
|
|
|
148
195
|
case "KeyF":
|
|
149
196
|
this.isFlying = !this.isFlying;
|
|
150
197
|
this.setAnimationByPressed();
|
|
151
|
-
if (!this.isFlying && !this.playerIsOnGround)
|
|
198
|
+
if (!this.isFlying && !this.playerIsOnGround) {
|
|
199
|
+
this.playPersonAnimationByName("jumping");
|
|
200
|
+
}
|
|
152
201
|
break;
|
|
153
202
|
}
|
|
154
203
|
};
|
|
155
|
-
|
|
204
|
+
/**
|
|
205
|
+
* 键盘抬起事件
|
|
206
|
+
*/
|
|
156
207
|
this._boundOnKeyup = (e) => {
|
|
157
208
|
switch (e.code) {
|
|
158
209
|
case "KeyW":
|
|
@@ -174,11 +225,7 @@ var PlayerController = class {
|
|
|
174
225
|
case "ShiftLeft":
|
|
175
226
|
this.shiftPressed = false;
|
|
176
227
|
this.setAnimationByPressed();
|
|
177
|
-
this.controls.mouseButtons = {
|
|
178
|
-
LEFT: 0,
|
|
179
|
-
MIDDLE: 1,
|
|
180
|
-
RIGHT: 2
|
|
181
|
-
};
|
|
228
|
+
this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
|
|
182
229
|
break;
|
|
183
230
|
case "Space":
|
|
184
231
|
this.spacePressed = false;
|
|
@@ -188,127 +235,85 @@ var PlayerController = class {
|
|
|
188
235
|
break;
|
|
189
236
|
}
|
|
190
237
|
};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (this.isFlying) {
|
|
195
|
-
if (!this.fwdPressed) {
|
|
196
|
-
this.playPersonAnimationByName("flyidle");
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
this.playPersonAnimationByName("flying");
|
|
200
|
-
this._maxCamDistance = this.orginMaxCamDistance * 2;
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
if (this.playerIsOnGround) {
|
|
204
|
-
if (!this.fwdPressed && !this.bkdPressed && !this.lftPressed && !this.rgtPressed) {
|
|
205
|
-
this.playPersonAnimationByName("idle");
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (this.fwdPressed) {
|
|
209
|
-
if (this.shiftPressed) {
|
|
210
|
-
this.playPersonAnimationByName("running");
|
|
211
|
-
} else {
|
|
212
|
-
this.playPersonAnimationByName("walking");
|
|
213
|
-
}
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (!this.isFirstPerson && (this.lftPressed || this.rgtPressed || this.bkdPressed)) {
|
|
217
|
-
if (this.shiftPressed) {
|
|
218
|
-
this.playPersonAnimationByName("running");
|
|
219
|
-
} else {
|
|
220
|
-
this.playPersonAnimationByName("walking");
|
|
221
|
-
}
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (this.lftPressed) {
|
|
225
|
-
this.playPersonAnimationByName("left_walking");
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (this.rgtPressed) {
|
|
229
|
-
this.playPersonAnimationByName("right_walking");
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (this.bkdPressed) {
|
|
233
|
-
this.playPersonAnimationByName("walking_backward");
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (this.recheckAnimTimer !== null) {
|
|
238
|
-
clearTimeout(this.recheckAnimTimer);
|
|
239
|
-
}
|
|
240
|
-
this.recheckAnimTimer = setTimeout(() => {
|
|
241
|
-
this.setAnimationByPressed();
|
|
242
|
-
this.recheckAnimTimer = null;
|
|
243
|
-
}, 200);
|
|
244
|
-
};
|
|
245
|
-
// 鼠标移动事件
|
|
238
|
+
/**
|
|
239
|
+
* 鼠标移动事件
|
|
240
|
+
*/
|
|
246
241
|
this._mouseMove = (e) => {
|
|
247
242
|
if (document.pointerLockElement !== document.body) return;
|
|
248
243
|
this.setToward(e.movementX, e.movementY, 1e-4);
|
|
249
244
|
};
|
|
245
|
+
/**
|
|
246
|
+
* 鼠标点击事件
|
|
247
|
+
*/
|
|
250
248
|
this._mouseClick = (e) => {
|
|
251
249
|
this.setPointerLock();
|
|
252
250
|
};
|
|
251
|
+
// ==================== 移动端控制 ====================
|
|
252
|
+
/**
|
|
253
|
+
* 指针按下事件
|
|
254
|
+
*/
|
|
253
255
|
this.onPointerDown = (e) => {
|
|
254
256
|
if (e.pointerType !== "touch") return;
|
|
255
257
|
this.isLookDown = true;
|
|
256
258
|
this.lookPointerId = e.pointerId;
|
|
257
259
|
this.lastTouchX = e.clientX;
|
|
258
260
|
this.lastTouchY = e.clientY;
|
|
259
|
-
this.lookAreaEl?.setPointerCapture
|
|
261
|
+
this.lookAreaEl?.setPointerCapture?.(e.pointerId);
|
|
260
262
|
e.preventDefault();
|
|
261
263
|
};
|
|
264
|
+
/**
|
|
265
|
+
* 指针移动事件
|
|
266
|
+
*/
|
|
262
267
|
this.onPointerMove = (e) => {
|
|
263
268
|
if (!this.isLookDown || e.pointerId !== this.lookPointerId) return;
|
|
264
269
|
const dx = e.clientX - this.lastTouchX;
|
|
265
270
|
const dy = e.clientY - this.lastTouchY;
|
|
266
271
|
this.lastTouchX = e.clientX;
|
|
267
272
|
this.lastTouchY = e.clientY;
|
|
268
|
-
this.setInput({
|
|
269
|
-
lookDeltaX: dx,
|
|
270
|
-
lookDeltaY: dy
|
|
271
|
-
});
|
|
273
|
+
this.setInput({ lookDeltaX: dx, lookDeltaY: dy });
|
|
272
274
|
e.preventDefault();
|
|
273
275
|
};
|
|
276
|
+
/**
|
|
277
|
+
* 指针抬起事件
|
|
278
|
+
*/
|
|
274
279
|
this.onPointerUp = (e) => {
|
|
275
280
|
if (e.pointerId !== this.lookPointerId) return;
|
|
276
281
|
this.isLookDown = false;
|
|
277
282
|
this.lookPointerId = null;
|
|
278
|
-
this.lookAreaEl?.releasePointerCapture
|
|
283
|
+
this.lookAreaEl?.releasePointerCapture?.(e.pointerId);
|
|
279
284
|
};
|
|
280
285
|
this._raycaster.firstHitOnly = true;
|
|
281
286
|
this._raycasterPersonToCam.firstHitOnly = true;
|
|
282
287
|
}
|
|
283
|
-
//
|
|
288
|
+
// ==================== 初始化相关方法 ====================
|
|
289
|
+
/**
|
|
290
|
+
* 初始化控制器
|
|
291
|
+
*/
|
|
284
292
|
async init(opts, callback) {
|
|
285
293
|
this.scene = opts.scene;
|
|
286
294
|
this.camera = opts.camera;
|
|
287
295
|
this.camera.rotation.order = "YXZ";
|
|
288
296
|
this.controls = opts.controls;
|
|
289
297
|
this.playerModel = opts.playerModel;
|
|
290
|
-
this.initPos = opts.initPos
|
|
291
|
-
this.
|
|
292
|
-
this.mouseSensity = opts.mouseSensity ? opts.mouseSensity : 5;
|
|
298
|
+
this.initPos = opts.initPos ?? new THREE.Vector3(0, 0, 0);
|
|
299
|
+
this.mouseSensity = opts.mouseSensity ?? 5;
|
|
293
300
|
const s = this.playerModel.scale;
|
|
294
301
|
this.visualizeDepth = 0 * s;
|
|
295
|
-
this.gravity = opts.playerModel.gravity
|
|
296
|
-
this.jumpHeight = opts.playerModel.jumpHeight
|
|
297
|
-
this.originPlayerSpeed = opts.playerModel.speed
|
|
302
|
+
this.gravity = (opts.playerModel.gravity ?? -2400) * s;
|
|
303
|
+
this.jumpHeight = (opts.playerModel.jumpHeight ?? 800) * s;
|
|
304
|
+
this.originPlayerSpeed = (opts.playerModel.speed ?? 400) * s;
|
|
298
305
|
this.playerSpeed = this.originPlayerSpeed;
|
|
299
|
-
this.playerModel.rotateY = opts.playerModel.rotateY
|
|
306
|
+
this.playerModel.rotateY = opts.playerModel.rotateY ?? 0;
|
|
300
307
|
this._camCollisionLerp = 0.18;
|
|
301
308
|
this._camEpsilon = 35 * s;
|
|
302
|
-
this._minCamDistance = opts.minCamDistance
|
|
303
|
-
this._maxCamDistance = opts.maxCamDistance
|
|
309
|
+
this._minCamDistance = (opts.minCamDistance ?? 100) * s;
|
|
310
|
+
this._maxCamDistance = (opts.maxCamDistance ?? 440) * s;
|
|
304
311
|
this.orginMaxCamDistance = this._maxCamDistance;
|
|
305
312
|
this.thirdMouseMode = opts.thirdMouseMode ?? 1;
|
|
306
|
-
|
|
307
|
-
return navigator.maxTouchPoints && navigator.maxTouchPoints > 0 || "ontouchstart" in window || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
308
|
-
}
|
|
313
|
+
const isMobileDevice = () => navigator.maxTouchPoints && navigator.maxTouchPoints > 0 || "ontouchstart" in window || /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
309
314
|
this.isShowMobileControls = (opts.isShowMobileControls ?? true) && isMobileDevice();
|
|
310
315
|
if (this.isShowMobileControls) {
|
|
311
|
-
this.initMobileControls();
|
|
316
|
+
await this.initMobileControls();
|
|
312
317
|
}
|
|
313
318
|
await this.createBVH(opts.colliderMeshUrl);
|
|
314
319
|
this.createPlayer();
|
|
@@ -321,73 +326,19 @@ var PlayerController = class {
|
|
|
321
326
|
this.setControls();
|
|
322
327
|
if (callback) callback();
|
|
323
328
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (this.isFirstPerson) {
|
|
328
|
-
this.player.attach(this.camera);
|
|
329
|
-
this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
|
|
330
|
-
this.camera.rotation.set(0, Math.PI, 0);
|
|
331
|
-
this.setPointerLock();
|
|
332
|
-
} else {
|
|
333
|
-
this.scene.attach(this.camera);
|
|
334
|
-
const worldPos = this.player.position.clone();
|
|
335
|
-
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
|
|
336
|
-
const angle = Math.atan2(dir.z, dir.x);
|
|
337
|
-
const offset = new THREE.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, 200 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
|
|
338
|
-
this.camera.position.copy(worldPos).add(offset);
|
|
339
|
-
this.controls.target.copy(worldPos);
|
|
340
|
-
this.setPointerLock();
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
setPointerLock() {
|
|
344
|
-
if ((this.thirdMouseMode == 0 || this.thirdMouseMode == 1) && !this.isFirstPerson || this.isFirstPerson) document.body.requestPointerLock();
|
|
345
|
-
}
|
|
346
|
-
// 摄像机/控制器设置
|
|
347
|
-
setCameraPos() {
|
|
348
|
-
if (this.isFirstPerson) {
|
|
349
|
-
this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
|
|
350
|
-
} else {
|
|
351
|
-
const worldPos = this.player.position.clone();
|
|
352
|
-
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
|
|
353
|
-
const angle = Math.atan2(dir.z, dir.x);
|
|
354
|
-
const offset = new THREE.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, -100 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
|
|
355
|
-
this.camera.position.copy(worldPos).add(offset);
|
|
356
|
-
}
|
|
357
|
-
this.camera.updateProjectionMatrix();
|
|
358
|
-
}
|
|
359
|
-
// 设置控制器
|
|
360
|
-
setControls() {
|
|
361
|
-
if (this.thirdMouseMode == 0 || this.thirdMouseMode == 1) {
|
|
362
|
-
this.controls.enabled = false;
|
|
363
|
-
} else {
|
|
364
|
-
this.controls.enabled = true;
|
|
365
|
-
}
|
|
366
|
-
this.controls.rotateSpeed = this.mouseSensity * 0.05;
|
|
367
|
-
this.controls.maxPolarAngle = Math.PI * (300 / 360);
|
|
368
|
-
}
|
|
369
|
-
// 重置控制器
|
|
370
|
-
resetControls() {
|
|
371
|
-
if (!this.controls) return;
|
|
372
|
-
this.controls.enabled = true;
|
|
373
|
-
this.controls.enablePan = true;
|
|
374
|
-
this.controls.maxPolarAngle = Math.PI / 2;
|
|
375
|
-
this.controls.rotateSpeed = 1;
|
|
376
|
-
this.controls.enableZoom = true;
|
|
377
|
-
this.controls.mouseButtons = {
|
|
378
|
-
LEFT: 0,
|
|
379
|
-
MIDDLE: 1,
|
|
380
|
-
RIGHT: 2
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
// 初始化加载器
|
|
329
|
+
/**
|
|
330
|
+
* 初始化加载器
|
|
331
|
+
*/
|
|
384
332
|
async initLoader() {
|
|
385
333
|
const dracoLoader = new DRACOLoader();
|
|
386
334
|
dracoLoader.setDecoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/gltf/");
|
|
387
335
|
dracoLoader.setDecoderConfig({ type: "js" });
|
|
388
336
|
this.loader.setDRACOLoader(dracoLoader);
|
|
389
337
|
}
|
|
390
|
-
//
|
|
338
|
+
// ==================== 玩家模型相关方法 ====================
|
|
339
|
+
/**
|
|
340
|
+
* 加载玩家模型与动画
|
|
341
|
+
*/
|
|
391
342
|
async loadPersonGLB() {
|
|
392
343
|
try {
|
|
393
344
|
const gltf = await this.loader.loadAsync(this.playerModel.url);
|
|
@@ -407,8 +358,7 @@ var PlayerController = class {
|
|
|
407
358
|
this.personMixer = new THREE.AnimationMixer(this.person);
|
|
408
359
|
const animations = gltf.animations ?? [];
|
|
409
360
|
this.personActions = /* @__PURE__ */ new Map();
|
|
410
|
-
const
|
|
411
|
-
const regs = [
|
|
361
|
+
const animationMappings = [
|
|
412
362
|
[this.playerModel.idleAnim, "idle"],
|
|
413
363
|
[this.playerModel.walkAnim, "walking"],
|
|
414
364
|
[this.playerModel.leftWalkAnim || this.playerModel.walkAnim, "left_walking"],
|
|
@@ -419,11 +369,12 @@ var PlayerController = class {
|
|
|
419
369
|
[this.playerModel.flyIdleAnim || this.playerModel.idleAnim, "flyidle"],
|
|
420
370
|
[this.playerModel.flyAnim || this.playerModel.idleAnim, "flying"]
|
|
421
371
|
];
|
|
422
|
-
|
|
423
|
-
|
|
372
|
+
const findClip = (name) => animations.find((a) => a.name === name);
|
|
373
|
+
for (const [clipName, actionName] of animationMappings) {
|
|
374
|
+
const clip = findClip(clipName);
|
|
424
375
|
if (!clip) continue;
|
|
425
376
|
const action = this.personMixer.clipAction(clip);
|
|
426
|
-
if (
|
|
377
|
+
if (actionName === "jumping") {
|
|
427
378
|
action.setLoop(THREE.LoopOnce, 1);
|
|
428
379
|
action.clampWhenFinished = true;
|
|
429
380
|
action.setEffectiveTimeScale(1.2);
|
|
@@ -434,7 +385,7 @@ var PlayerController = class {
|
|
|
434
385
|
}
|
|
435
386
|
action.enabled = true;
|
|
436
387
|
action.setEffectiveWeight(0);
|
|
437
|
-
this.personActions.set(
|
|
388
|
+
this.personActions.set(actionName, action);
|
|
438
389
|
}
|
|
439
390
|
this.idleAction = this.personActions.get("idle");
|
|
440
391
|
this.walkAction = this.personActions.get("walking");
|
|
@@ -452,8 +403,7 @@ var PlayerController = class {
|
|
|
452
403
|
const finishedAction = ev.action;
|
|
453
404
|
if (finishedAction === this.jumpAction) {
|
|
454
405
|
if (this.fwdPressed) {
|
|
455
|
-
|
|
456
|
-
else this.playPersonAnimationByName("walking");
|
|
406
|
+
this.playPersonAnimationByName(this.shiftPressed ? "running" : "walking");
|
|
457
407
|
return;
|
|
458
408
|
}
|
|
459
409
|
if (this.bkdPressed) {
|
|
@@ -468,15 +418,16 @@ var PlayerController = class {
|
|
|
468
418
|
}
|
|
469
419
|
});
|
|
470
420
|
} catch (error) {
|
|
421
|
+
console.error("\u52A0\u8F7D\u73A9\u5BB6\u6A21\u578B\u5931\u8D25:", error);
|
|
471
422
|
}
|
|
472
423
|
}
|
|
473
|
-
|
|
424
|
+
/**
|
|
425
|
+
* 平滑切换人物动画
|
|
426
|
+
*/
|
|
474
427
|
playPersonAnimationByName(name, fade = 0.18) {
|
|
475
|
-
if (!this.personActions) return;
|
|
476
|
-
if (this.ctPressed) return;
|
|
428
|
+
if (!this.personActions || this.ctPressed) return;
|
|
477
429
|
const next = this.personActions.get(name);
|
|
478
|
-
if (!next) return;
|
|
479
|
-
if (this.actionState === next) return;
|
|
430
|
+
if (!next || this.actionState === next) return;
|
|
480
431
|
const prev = this.actionState;
|
|
481
432
|
next.reset();
|
|
482
433
|
next.setEffectiveWeight(1);
|
|
@@ -487,46 +438,254 @@ var PlayerController = class {
|
|
|
487
438
|
} else {
|
|
488
439
|
next.fadeIn(fade);
|
|
489
440
|
}
|
|
490
|
-
this.actionState = next;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
this.
|
|
507
|
-
this.player
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
this.
|
|
514
|
-
this.
|
|
441
|
+
this.actionState = next;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* 创建玩家胶囊体
|
|
445
|
+
*/
|
|
446
|
+
createPlayer() {
|
|
447
|
+
const material = new THREE.MeshStandardMaterial({
|
|
448
|
+
color: new THREE.Color(1, 0, 0),
|
|
449
|
+
shadowSide: THREE.DoubleSide,
|
|
450
|
+
depthTest: false,
|
|
451
|
+
transparent: true,
|
|
452
|
+
opacity: this.displayPlayer ? 0.5 : 0,
|
|
453
|
+
wireframe: true,
|
|
454
|
+
depthWrite: false
|
|
455
|
+
});
|
|
456
|
+
const r = this.playerRadius * this.playerModel.scale;
|
|
457
|
+
const h = this.playerHeight * this.playerModel.scale;
|
|
458
|
+
this.player = new THREE.Mesh(new RoundedBoxGeometry(r * 2, h, r * 2, 1, 75), material);
|
|
459
|
+
this.player.geometry.translate(0, -h * 0.25, 0);
|
|
460
|
+
this.player.capsuleInfo = {
|
|
461
|
+
radius: r,
|
|
462
|
+
segment: new THREE.Line3(new THREE.Vector3(), new THREE.Vector3(0, -h * 0.5, 0))
|
|
463
|
+
};
|
|
464
|
+
this.player.name = "capsule";
|
|
465
|
+
this.scene.add(this.player);
|
|
466
|
+
this.reset();
|
|
467
|
+
this.player.rotateY(this.playerModel.rotateY ?? 0);
|
|
468
|
+
}
|
|
469
|
+
// ==================== 相机与视角控制 ====================
|
|
470
|
+
/**
|
|
471
|
+
* 第一/三人称视角切换
|
|
472
|
+
*/
|
|
473
|
+
changeView() {
|
|
474
|
+
this.isFirstPerson = !this.isFirstPerson;
|
|
475
|
+
if (this.isFirstPerson) {
|
|
476
|
+
this.player.attach(this.camera);
|
|
477
|
+
this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
|
|
478
|
+
this.camera.rotation.set(0, Math.PI, 0);
|
|
479
|
+
} else {
|
|
480
|
+
this.scene.attach(this.camera);
|
|
481
|
+
const worldPos = this.player.position.clone();
|
|
482
|
+
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
|
|
483
|
+
const angle = Math.atan2(dir.z, dir.x);
|
|
484
|
+
const offset = new THREE.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, 200 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
|
|
485
|
+
this.camera.position.copy(worldPos).add(offset);
|
|
486
|
+
this.controls.target.copy(worldPos);
|
|
487
|
+
}
|
|
488
|
+
this.setPointerLock();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 设置指针锁定
|
|
492
|
+
*/
|
|
493
|
+
setPointerLock() {
|
|
494
|
+
if ((this.thirdMouseMode === 0 || this.thirdMouseMode === 1) && !this.isFirstPerson || this.isFirstPerson) {
|
|
495
|
+
document.body.requestPointerLock();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* 设置摄像机初始位置
|
|
500
|
+
*/
|
|
501
|
+
setCameraPos() {
|
|
502
|
+
if (this.isFirstPerson) {
|
|
503
|
+
this.camera.position.set(0, 40 * this.playerModel.scale, 30 * this.playerModel.scale);
|
|
504
|
+
} else {
|
|
505
|
+
const worldPos = this.player.position.clone();
|
|
506
|
+
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(this.player.quaternion);
|
|
507
|
+
const angle = Math.atan2(dir.z, dir.x);
|
|
508
|
+
const offset = new THREE.Vector3(Math.cos(angle) * 400 * this.playerModel.scale, -100 * this.playerModel.scale, Math.sin(angle) * 400 * this.playerModel.scale);
|
|
509
|
+
this.camera.position.copy(worldPos).add(offset);
|
|
510
|
+
}
|
|
511
|
+
this.camera.updateProjectionMatrix();
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* 设置控制器
|
|
515
|
+
*/
|
|
516
|
+
setControls() {
|
|
517
|
+
this.controls.enabled = !(this.thirdMouseMode === 0 || this.thirdMouseMode === 1);
|
|
518
|
+
this.controls.rotateSpeed = this.mouseSensity * 0.05;
|
|
519
|
+
this.controls.maxPolarAngle = Math.PI * (300 / 360);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 重置控制器
|
|
523
|
+
*/
|
|
524
|
+
resetControls() {
|
|
525
|
+
if (!this.controls) return;
|
|
526
|
+
this.controls.enabled = true;
|
|
527
|
+
this.controls.enablePan = true;
|
|
528
|
+
this.controls.maxPolarAngle = Math.PI / 2;
|
|
529
|
+
this.controls.rotateSpeed = 1;
|
|
530
|
+
this.controls.enableZoom = true;
|
|
531
|
+
this.controls.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* 设置朝向
|
|
535
|
+
*/
|
|
536
|
+
setToward(dx, dy, speed) {
|
|
537
|
+
if (this.isFirstPerson) {
|
|
538
|
+
const yaw = -dx * speed * this.mouseSensity;
|
|
539
|
+
const pitch = -dy * speed * this.mouseSensity;
|
|
540
|
+
this.player.rotateY(yaw);
|
|
541
|
+
this.camera.rotation.x = THREE.MathUtils.clamp(this.camera.rotation.x + pitch, -1.1, 1.4);
|
|
542
|
+
} else {
|
|
543
|
+
const sensitivity = this.mouseSensity;
|
|
544
|
+
const deltaX = -dx * speed * sensitivity;
|
|
545
|
+
const deltaY = -dy * speed * sensitivity;
|
|
546
|
+
const target = this.player.position.clone();
|
|
547
|
+
const distance = this.camera.position.distanceTo(target);
|
|
548
|
+
const currentPosition = this.camera.position.clone().sub(target);
|
|
549
|
+
let theta = Math.atan2(currentPosition.x, currentPosition.z);
|
|
550
|
+
let phi = Math.acos(currentPosition.y / distance);
|
|
551
|
+
theta += deltaX;
|
|
552
|
+
phi += deltaY;
|
|
553
|
+
phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
|
|
554
|
+
const newX = distance * Math.sin(phi) * Math.sin(theta);
|
|
555
|
+
const newY = distance * Math.cos(phi);
|
|
556
|
+
const newZ = distance * Math.sin(phi) * Math.cos(theta);
|
|
557
|
+
this.camera.position.set(target.x + newX, target.y + newY, target.z + newZ);
|
|
558
|
+
this.camera.lookAt(target);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
// ==================== 物理与碰撞检测 ====================
|
|
562
|
+
/**
|
|
563
|
+
* BVH碰撞体构建
|
|
564
|
+
*/
|
|
565
|
+
async createBVH(meshUrl = "") {
|
|
566
|
+
await this.initLoader();
|
|
567
|
+
const ensureAttributesMinimal = (geom) => {
|
|
568
|
+
if (!geom.attributes.position) return null;
|
|
569
|
+
if (!geom.attributes.normal) geom.computeVertexNormals();
|
|
570
|
+
if (!geom.attributes.uv) {
|
|
571
|
+
const count = geom.attributes.position.count;
|
|
572
|
+
const dummyUV = new Float32Array(count * 2);
|
|
573
|
+
geom.setAttribute("uv", new THREE.BufferAttribute(dummyUV, 2));
|
|
574
|
+
}
|
|
575
|
+
return geom;
|
|
576
|
+
};
|
|
577
|
+
const collected = [];
|
|
578
|
+
if (meshUrl === "") {
|
|
579
|
+
if (this.collider) {
|
|
580
|
+
this.scene.remove(this.collider);
|
|
581
|
+
this.collider = null;
|
|
582
|
+
}
|
|
583
|
+
this.scene.traverse((c) => {
|
|
584
|
+
const mesh = c;
|
|
585
|
+
if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
|
|
586
|
+
try {
|
|
587
|
+
let geom = mesh.geometry.clone();
|
|
588
|
+
geom.applyMatrix4(mesh.matrixWorld);
|
|
589
|
+
if (geom.index) geom = geom.toNonIndexed();
|
|
590
|
+
const safe = ensureAttributesMinimal(geom);
|
|
591
|
+
if (safe) collected.push(safe);
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
if (!collected.length) return;
|
|
598
|
+
const attrMap = /* @__PURE__ */ new Map();
|
|
599
|
+
const attrConflict = /* @__PURE__ */ new Set();
|
|
600
|
+
for (const g of collected) {
|
|
601
|
+
for (const name of Object.keys(g.attributes)) {
|
|
602
|
+
const attr = g.attributes[name];
|
|
603
|
+
const ctor = attr.array.constructor;
|
|
604
|
+
const itemSize = attr.itemSize;
|
|
605
|
+
if (!attrMap.has(name)) {
|
|
606
|
+
attrMap.set(name, { itemSize, arrayCtor: ctor, examples: 1 });
|
|
607
|
+
} else {
|
|
608
|
+
const m = attrMap.get(name);
|
|
609
|
+
if (m.itemSize !== itemSize || m.arrayCtor !== ctor) {
|
|
610
|
+
attrConflict.add(name);
|
|
611
|
+
} else {
|
|
612
|
+
m.examples++;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (attrConflict.size) {
|
|
618
|
+
for (const g of collected) {
|
|
619
|
+
for (const name of Array.from(attrConflict)) {
|
|
620
|
+
if (g.attributes[name]) g.deleteAttribute(name);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
for (const name of attrConflict) attrMap.delete(name);
|
|
624
|
+
}
|
|
625
|
+
const attrNames = Array.from(attrMap.keys());
|
|
626
|
+
for (const g of collected) {
|
|
627
|
+
const count = g.attributes.position.count;
|
|
628
|
+
for (const name of attrNames) {
|
|
629
|
+
if (!g.attributes[name]) {
|
|
630
|
+
const meta = attrMap.get(name);
|
|
631
|
+
const len = count * meta.itemSize;
|
|
632
|
+
const array = new meta.arrayCtor(len);
|
|
633
|
+
g.setAttribute(name, new THREE.BufferAttribute(array, meta.itemSize));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
const gltf = await this.loader.loadAsync(meshUrl);
|
|
639
|
+
const mesh = gltf.scene.children[0];
|
|
640
|
+
mesh.name = "BVH\u52A0\u8F7D\u6A21\u578B";
|
|
641
|
+
let geom = mesh.geometry.clone();
|
|
642
|
+
geom.applyMatrix4(mesh.matrixWorld);
|
|
643
|
+
if (geom.index) geom = geom.toNonIndexed();
|
|
644
|
+
const safe = ensureAttributesMinimal(geom);
|
|
645
|
+
if (safe) collected.push(safe);
|
|
646
|
+
}
|
|
647
|
+
const merged = BufferGeometryUtils.mergeGeometries(collected, false);
|
|
648
|
+
if (!merged) {
|
|
649
|
+
console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
merged.boundsTree = new MeshBVH(merged, { maxDepth: 100 });
|
|
653
|
+
this.collider = new THREE.Mesh(
|
|
654
|
+
merged,
|
|
655
|
+
new THREE.MeshBasicMaterial({
|
|
656
|
+
opacity: 0.5,
|
|
657
|
+
transparent: true,
|
|
658
|
+
wireframe: true
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
if (this.displayCollider) this.scene.add(this.collider);
|
|
662
|
+
if (this.displayVisualizer) {
|
|
663
|
+
if (this.visualizer) this.scene.remove(this.visualizer);
|
|
664
|
+
this.visualizer = new MeshBVHHelper(this.collider, this.visualizeDepth);
|
|
665
|
+
this.scene.add(this.visualizer);
|
|
666
|
+
}
|
|
667
|
+
this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
|
|
515
668
|
}
|
|
516
|
-
|
|
669
|
+
/**
|
|
670
|
+
* 获取法线与Y轴的夹角
|
|
671
|
+
*/
|
|
517
672
|
getAngleWithYAxis(normal) {
|
|
518
673
|
const yAxis = { x: 0, y: 1, z: 0 };
|
|
519
674
|
const dotProduct = normal.x * yAxis.x + normal.y * yAxis.y + normal.z * yAxis.z;
|
|
520
675
|
const normalMagnitude = Math.sqrt(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z);
|
|
521
|
-
const
|
|
522
|
-
const cosTheta = dotProduct / (normalMagnitude * yAxisMagnitude);
|
|
676
|
+
const cosTheta = dotProduct / normalMagnitude;
|
|
523
677
|
return Math.acos(cosTheta);
|
|
524
678
|
}
|
|
525
|
-
//
|
|
679
|
+
// ==================== 循环更新 ====================
|
|
680
|
+
/**
|
|
681
|
+
* 每帧更新
|
|
682
|
+
*/
|
|
526
683
|
async update(delta = clock.getDelta()) {
|
|
527
684
|
if (!this.isupdate || !this.player || !this.collider) return;
|
|
528
685
|
delta = Math.min(delta, 1 / 30);
|
|
529
|
-
if (!this.isFlying)
|
|
686
|
+
if (!this.isFlying) {
|
|
687
|
+
this.player.position.addScaledVector(this.playerVelocity, delta);
|
|
688
|
+
}
|
|
530
689
|
this.updateMixers(delta);
|
|
531
690
|
this.camera.getWorldDirection(this.camDir);
|
|
532
691
|
let angle = Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2;
|
|
@@ -537,14 +696,9 @@ var PlayerController = class {
|
|
|
537
696
|
if (this.lftPressed) this.moveDir.add(this.DIR_LFT);
|
|
538
697
|
if (this.rgtPressed) this.moveDir.add(this.DIR_RGT);
|
|
539
698
|
if (this.isFlying) {
|
|
540
|
-
if (this.fwdPressed)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
this.moveDir.y = 0;
|
|
544
|
-
}
|
|
545
|
-
if (this.spacePressed) {
|
|
546
|
-
this.moveDir.add(this.DIR_UP);
|
|
547
|
-
}
|
|
699
|
+
if (this.fwdPressed) this.moveDir.y = this.camDir.y;
|
|
700
|
+
else this.moveDir.y = 0;
|
|
701
|
+
if (this.spacePressed) this.moveDir.add(this.DIR_UP);
|
|
548
702
|
}
|
|
549
703
|
if (this.isFlying && this.fwdPressed) {
|
|
550
704
|
this.playerSpeed = this.shiftPressed ? this.originPlayerSpeed * 12 : this.originPlayerSpeed * 7;
|
|
@@ -564,8 +718,7 @@ var PlayerController = class {
|
|
|
564
718
|
const maxH = this.playerHeight * this.playerModel.scale * 0.9;
|
|
565
719
|
const h = this.playerHeight * this.playerModel.scale * 0.75;
|
|
566
720
|
const minH = this.playerHeight * this.playerModel.scale * 0.7;
|
|
567
|
-
if (this.isFlying) {
|
|
568
|
-
} else {
|
|
721
|
+
if (!this.isFlying) {
|
|
569
722
|
if (playerDistanceFromGround > maxH) {
|
|
570
723
|
this.playerVelocity.y += delta * this.gravity;
|
|
571
724
|
this.player.position.addScaledVector(this.playerVelocity, delta);
|
|
@@ -628,7 +781,7 @@ var PlayerController = class {
|
|
|
628
781
|
this.moveDir.normalize();
|
|
629
782
|
this.moveDir.negate();
|
|
630
783
|
let lookTarget;
|
|
631
|
-
if (this.thirdMouseMode
|
|
784
|
+
if (this.thirdMouseMode === 0 || this.thirdMouseMode === 2) {
|
|
632
785
|
if (this.moveDir.lengthSq() > 0) {
|
|
633
786
|
lookTarget = this.player.position.clone().add(this.moveDir);
|
|
634
787
|
} else {
|
|
@@ -639,7 +792,7 @@ var PlayerController = class {
|
|
|
639
792
|
const alpha = Math.min(1, this.rotationSpeed * delta);
|
|
640
793
|
this.player.quaternion.slerp(this.targetQuat, alpha);
|
|
641
794
|
}
|
|
642
|
-
if ((this.thirdMouseMode
|
|
795
|
+
if ((this.thirdMouseMode === 1 || this.thirdMouseMode === 3) && this.moveDir.lengthSq() > 0) {
|
|
643
796
|
lookTarget = this.player.position.clone().add(this.moveDir);
|
|
644
797
|
this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
|
|
645
798
|
this.targetQuat.setFromRotationMatrix(this.targetMat);
|
|
@@ -703,206 +856,39 @@ var PlayerController = class {
|
|
|
703
856
|
}
|
|
704
857
|
}
|
|
705
858
|
}
|
|
706
|
-
|
|
859
|
+
/**
|
|
860
|
+
* 更新模型动画
|
|
861
|
+
*/
|
|
862
|
+
updateMixers(delta) {
|
|
863
|
+
if (this.personMixer) this.personMixer.update(delta);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* 重置玩家位置
|
|
867
|
+
*/
|
|
707
868
|
reset(position) {
|
|
708
869
|
if (!this.player) return;
|
|
709
870
|
this.playerVelocity.set(0, 0, 0);
|
|
710
|
-
this.player.position.copy(position
|
|
711
|
-
}
|
|
712
|
-
// 销毁
|
|
713
|
-
destroy() {
|
|
714
|
-
this.offAllEvent();
|
|
715
|
-
if (this.player) {
|
|
716
|
-
this.player.remove(this.camera);
|
|
717
|
-
this.scene.remove(this.player);
|
|
718
|
-
}
|
|
719
|
-
this.player = null;
|
|
720
|
-
if (this.person) {
|
|
721
|
-
this.scene.remove(this.person);
|
|
722
|
-
this.person = null;
|
|
723
|
-
}
|
|
724
|
-
this.resetControls();
|
|
725
|
-
if (this.visualizer) {
|
|
726
|
-
this.scene.remove(this.visualizer);
|
|
727
|
-
this.visualizer = null;
|
|
728
|
-
}
|
|
729
|
-
if (this.collider) {
|
|
730
|
-
this.scene.remove(this.collider);
|
|
731
|
-
this.collider = null;
|
|
732
|
-
}
|
|
733
|
-
this.destroyMobileControls();
|
|
734
|
-
controllerInstance = null;
|
|
735
|
-
}
|
|
736
|
-
// 事件绑定
|
|
737
|
-
onAllEvent() {
|
|
738
|
-
this.isupdate = true;
|
|
739
|
-
this.setPointerLock();
|
|
740
|
-
window.addEventListener("keydown", this._boundOnKeydown);
|
|
741
|
-
window.addEventListener("keyup", this._boundOnKeyup);
|
|
742
|
-
window.addEventListener("mousemove", this._mouseMove);
|
|
743
|
-
window.addEventListener("click", this._mouseClick);
|
|
744
|
-
}
|
|
745
|
-
// 事件解绑
|
|
746
|
-
offAllEvent() {
|
|
747
|
-
this.isupdate = false;
|
|
748
|
-
document.exitPointerLock();
|
|
749
|
-
window.removeEventListener("keydown", this._boundOnKeydown);
|
|
750
|
-
window.removeEventListener("keyup", this._boundOnKeyup);
|
|
751
|
-
window.removeEventListener("mousemove", this._mouseMove);
|
|
752
|
-
window.removeEventListener("click", this._mouseClick);
|
|
871
|
+
this.player.position.copy(position ?? this.initPos);
|
|
753
872
|
}
|
|
873
|
+
/**
|
|
874
|
+
* 获取玩家位置
|
|
875
|
+
*/
|
|
754
876
|
getPosition() {
|
|
755
877
|
return this.player.position;
|
|
756
878
|
}
|
|
757
|
-
//
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
// BVH构建
|
|
762
|
-
async createBVH(meshUrl = "") {
|
|
763
|
-
await this.initLoader();
|
|
764
|
-
const ensureAttributesMinimal = (geom) => {
|
|
765
|
-
if (!geom.attributes.position) {
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
if (!geom.attributes.normal) geom.computeVertexNormals();
|
|
769
|
-
if (!geom.attributes.uv) {
|
|
770
|
-
const count = geom.attributes.position.count;
|
|
771
|
-
const dummyUV = new Float32Array(count * 2);
|
|
772
|
-
geom.setAttribute("uv", new THREE.BufferAttribute(dummyUV, 2));
|
|
773
|
-
}
|
|
774
|
-
return geom;
|
|
775
|
-
};
|
|
776
|
-
const collected = [];
|
|
777
|
-
if (meshUrl == "") {
|
|
778
|
-
if (this.collider) {
|
|
779
|
-
this.scene.remove(this.collider);
|
|
780
|
-
this.collider = null;
|
|
781
|
-
}
|
|
782
|
-
this.scene.traverse((c) => {
|
|
783
|
-
const mesh = c;
|
|
784
|
-
if (mesh?.isMesh && mesh.geometry && c.name !== "capsule") {
|
|
785
|
-
try {
|
|
786
|
-
let geom = mesh.geometry.clone();
|
|
787
|
-
geom.applyMatrix4(mesh.matrixWorld);
|
|
788
|
-
if (geom.index) geom = geom.toNonIndexed();
|
|
789
|
-
const safe = ensureAttributesMinimal(geom);
|
|
790
|
-
if (safe) collected.push(safe);
|
|
791
|
-
} catch (e) {
|
|
792
|
-
console.warn("\u5904\u7406\u7F51\u683C\u65F6\u51FA\u9519\uFF1A", mesh, e);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
});
|
|
796
|
-
if (!collected.length) {
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
const attrMap = /* @__PURE__ */ new Map();
|
|
800
|
-
const attrConflict = /* @__PURE__ */ new Set();
|
|
801
|
-
for (const g of collected) {
|
|
802
|
-
for (const name of Object.keys(g.attributes)) {
|
|
803
|
-
const attr = g.attributes[name];
|
|
804
|
-
const ctor = attr.array.constructor;
|
|
805
|
-
const itemSize = attr.itemSize;
|
|
806
|
-
if (!attrMap.has(name)) {
|
|
807
|
-
attrMap.set(name, { itemSize, arrayCtor: ctor, examples: 1 });
|
|
808
|
-
} else {
|
|
809
|
-
const m = attrMap.get(name);
|
|
810
|
-
if (m.itemSize !== itemSize || m.arrayCtor !== ctor) attrConflict.add(name);
|
|
811
|
-
else m.examples++;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (attrConflict.size) {
|
|
816
|
-
for (const g of collected) {
|
|
817
|
-
for (const name of Array.from(attrConflict)) {
|
|
818
|
-
if (g.attributes[name]) g.deleteAttribute(name);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
for (const name of attrConflict) attrMap.delete(name);
|
|
822
|
-
}
|
|
823
|
-
const attrNames = Array.from(attrMap.keys());
|
|
824
|
-
for (const g of collected) {
|
|
825
|
-
const count = g.attributes.position.count;
|
|
826
|
-
for (const name of attrNames) {
|
|
827
|
-
if (!g.attributes[name]) {
|
|
828
|
-
const meta = attrMap.get(name);
|
|
829
|
-
const len = count * meta.itemSize;
|
|
830
|
-
const array = new meta.arrayCtor(len);
|
|
831
|
-
g.setAttribute(name, new THREE.BufferAttribute(array, meta.itemSize));
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
} else {
|
|
836
|
-
const gltf = await this.loader.loadAsync(meshUrl, (xhr) => {
|
|
837
|
-
});
|
|
838
|
-
const mesh = gltf.scene.children[0];
|
|
839
|
-
mesh.name = "BVH\u52A0\u8F7D\u6A21\u578B";
|
|
840
|
-
let geom = mesh.geometry.clone();
|
|
841
|
-
geom.applyMatrix4(mesh.matrixWorld);
|
|
842
|
-
if (geom.index) geom = geom.toNonIndexed();
|
|
843
|
-
const safe = ensureAttributesMinimal(geom);
|
|
844
|
-
if (safe) collected.push(safe);
|
|
845
|
-
}
|
|
846
|
-
const merged = BufferGeometryUtils.mergeGeometries(collected, false);
|
|
847
|
-
if (!merged) {
|
|
848
|
-
console.error("\u5408\u5E76\u51E0\u4F55\u5931\u8D25");
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
merged.boundsTree = new MeshBVH(merged, {
|
|
852
|
-
maxDepth: 100
|
|
853
|
-
});
|
|
854
|
-
this.collider = new THREE.Mesh(
|
|
855
|
-
merged,
|
|
856
|
-
new THREE.MeshBasicMaterial({
|
|
857
|
-
opacity: 0.5,
|
|
858
|
-
transparent: true,
|
|
859
|
-
wireframe: true
|
|
860
|
-
})
|
|
861
|
-
);
|
|
862
|
-
if (this.displayCollider) this.scene.add(this.collider);
|
|
863
|
-
if (this.displayVisualizer) {
|
|
864
|
-
if (this.visualizer) this.scene.remove(this.visualizer);
|
|
865
|
-
this.visualizer = new MeshBVHHelper(this.collider, this.visualizeDepth);
|
|
866
|
-
this.scene.add(this.visualizer);
|
|
867
|
-
}
|
|
868
|
-
this.boundingBoxMinY = this.collider.geometry.boundingBox.min.y;
|
|
869
|
-
}
|
|
870
|
-
// 设置朝向
|
|
871
|
-
setToward(dx, dy, speed) {
|
|
872
|
-
if (this.isFirstPerson) {
|
|
873
|
-
const yaw = -dx * speed * this.mouseSensity;
|
|
874
|
-
const pitch = -dy * speed * this.mouseSensity;
|
|
875
|
-
this.player.rotateY(yaw);
|
|
876
|
-
this.camera.rotation.x = THREE.MathUtils.clamp(this.camera.rotation.x + pitch, -1.1, 1.4);
|
|
877
|
-
} else {
|
|
878
|
-
const sensitivity = this.mouseSensity;
|
|
879
|
-
const deltaX = -dx * speed * sensitivity;
|
|
880
|
-
const deltaY = -dy * speed * sensitivity;
|
|
881
|
-
const target = this.player.position.clone();
|
|
882
|
-
const distance = this.camera.position.distanceTo(target);
|
|
883
|
-
const currentPosition = this.camera.position.clone().sub(target);
|
|
884
|
-
let theta = Math.atan2(currentPosition.x, currentPosition.z);
|
|
885
|
-
let phi = Math.acos(currentPosition.y / distance);
|
|
886
|
-
theta += deltaX;
|
|
887
|
-
phi += deltaY;
|
|
888
|
-
phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi));
|
|
889
|
-
const newX = distance * Math.sin(phi) * Math.sin(theta);
|
|
890
|
-
const newY = distance * Math.cos(phi);
|
|
891
|
-
const newZ = distance * Math.sin(phi) * Math.cos(theta);
|
|
892
|
-
this.camera.position.set(target.x + newX, target.y + newY, target.z + newZ);
|
|
893
|
-
this.camera.lookAt(target);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
// 设置输入
|
|
879
|
+
// ==================== 输入处理 ====================
|
|
880
|
+
/**
|
|
881
|
+
* 设置输入
|
|
882
|
+
*/
|
|
897
883
|
setInput(input) {
|
|
898
884
|
if (typeof input.moveX === "number") {
|
|
899
|
-
this.lftPressed = input.moveX
|
|
900
|
-
this.rgtPressed = input.moveX
|
|
885
|
+
this.lftPressed = input.moveX === -1;
|
|
886
|
+
this.rgtPressed = input.moveX === 1;
|
|
901
887
|
this.setAnimationByPressed();
|
|
902
888
|
}
|
|
903
889
|
if (typeof input.moveY === "number") {
|
|
904
|
-
this.fwdPressed = input.moveY
|
|
905
|
-
this.bkdPressed = input.moveY
|
|
890
|
+
this.fwdPressed = input.moveY === 1;
|
|
891
|
+
this.bkdPressed = input.moveY === -1;
|
|
906
892
|
this.setAnimationByPressed();
|
|
907
893
|
}
|
|
908
894
|
if (typeof input.lookDeltaX === "number" && typeof input.lookDeltaY === "number") {
|
|
@@ -928,10 +914,36 @@ var PlayerController = class {
|
|
|
928
914
|
if (input.toggleFly) {
|
|
929
915
|
this.isFlying = !this.isFlying;
|
|
930
916
|
this.setAnimationByPressed();
|
|
931
|
-
if (!this.isFlying && !this.playerIsOnGround)
|
|
917
|
+
if (!this.isFlying && !this.playerIsOnGround) {
|
|
918
|
+
this.playPersonAnimationByName("jumping");
|
|
919
|
+
}
|
|
932
920
|
}
|
|
933
921
|
}
|
|
934
|
-
|
|
922
|
+
/**
|
|
923
|
+
* 事件绑定
|
|
924
|
+
*/
|
|
925
|
+
onAllEvent() {
|
|
926
|
+
this.isupdate = true;
|
|
927
|
+
this.setPointerLock();
|
|
928
|
+
window.addEventListener("keydown", this._boundOnKeydown);
|
|
929
|
+
window.addEventListener("keyup", this._boundOnKeyup);
|
|
930
|
+
window.addEventListener("mousemove", this._mouseMove);
|
|
931
|
+
window.addEventListener("click", this._mouseClick);
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* 事件解绑
|
|
935
|
+
*/
|
|
936
|
+
offAllEvent() {
|
|
937
|
+
this.isupdate = false;
|
|
938
|
+
document.exitPointerLock();
|
|
939
|
+
window.removeEventListener("keydown", this._boundOnKeydown);
|
|
940
|
+
window.removeEventListener("keyup", this._boundOnKeyup);
|
|
941
|
+
window.removeEventListener("mousemove", this._mouseMove);
|
|
942
|
+
window.removeEventListener("click", this._mouseClick);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* 初始化移动端摇杆控制
|
|
946
|
+
*/
|
|
935
947
|
async initMobileControls() {
|
|
936
948
|
this.controls.maxPolarAngle = Math.PI * (300 / 360);
|
|
937
949
|
this.controls.touches = { ONE: null, TWO: null };
|
|
@@ -955,13 +967,9 @@ var PlayerController = class {
|
|
|
955
967
|
});
|
|
956
968
|
container.appendChild(this.joystickZoneEl);
|
|
957
969
|
["touchstart", "touchmove", "touchend", "touchcancel"].forEach((evtName) => {
|
|
958
|
-
this.joystickZoneEl?.addEventListener(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
e.preventDefault();
|
|
962
|
-
},
|
|
963
|
-
{ passive: false }
|
|
964
|
-
);
|
|
970
|
+
this.joystickZoneEl?.addEventListener(evtName, (e) => e.preventDefault(), {
|
|
971
|
+
passive: false
|
|
972
|
+
});
|
|
965
973
|
});
|
|
966
974
|
this.joystickManager = nipple.create({
|
|
967
975
|
zone: this.joystickZoneEl,
|
|
@@ -1013,26 +1021,14 @@ var PlayerController = class {
|
|
|
1013
1021
|
});
|
|
1014
1022
|
container.appendChild(this.lookAreaEl);
|
|
1015
1023
|
["touchstart", "touchmove", "touchend", "touchcancel"].forEach((evtName) => {
|
|
1016
|
-
this.lookAreaEl?.addEventListener(
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
e.preventDefault();
|
|
1020
|
-
},
|
|
1021
|
-
{ passive: false }
|
|
1022
|
-
);
|
|
1023
|
-
});
|
|
1024
|
-
this.lookAreaEl.addEventListener("pointerdown", this.onPointerDown, {
|
|
1025
|
-
passive: false
|
|
1026
|
-
});
|
|
1027
|
-
this.lookAreaEl.addEventListener("pointermove", this.onPointerMove, {
|
|
1028
|
-
passive: false
|
|
1029
|
-
});
|
|
1030
|
-
this.lookAreaEl.addEventListener("pointerup", this.onPointerUp, {
|
|
1031
|
-
passive: false
|
|
1032
|
-
});
|
|
1033
|
-
this.lookAreaEl.addEventListener("pointercancel", this.onPointerUp, {
|
|
1034
|
-
passive: false
|
|
1024
|
+
this.lookAreaEl?.addEventListener(evtName, (e) => e.preventDefault(), {
|
|
1025
|
+
passive: false
|
|
1026
|
+
});
|
|
1035
1027
|
});
|
|
1028
|
+
this.lookAreaEl.addEventListener("pointerdown", this.onPointerDown, { passive: false });
|
|
1029
|
+
this.lookAreaEl.addEventListener("pointermove", this.onPointerMove, { passive: false });
|
|
1030
|
+
this.lookAreaEl.addEventListener("pointerup", this.onPointerUp, { passive: false });
|
|
1031
|
+
this.lookAreaEl.addEventListener("pointercancel", this.onPointerUp, { passive: false });
|
|
1036
1032
|
const createBtn = (rightPx, bottomPx, bgUrl) => {
|
|
1037
1033
|
const btn = document.createElement("button");
|
|
1038
1034
|
const styles = {
|
|
@@ -1064,13 +1060,7 @@ var PlayerController = class {
|
|
|
1064
1060
|
Object.assign(btn.style, styles);
|
|
1065
1061
|
container.appendChild(btn);
|
|
1066
1062
|
["touchstart", "touchend", "touchcancel"].forEach((evtName) => {
|
|
1067
|
-
btn.addEventListener(
|
|
1068
|
-
evtName,
|
|
1069
|
-
(e) => {
|
|
1070
|
-
e.preventDefault();
|
|
1071
|
-
},
|
|
1072
|
-
{ passive: false }
|
|
1073
|
-
);
|
|
1063
|
+
btn.addEventListener(evtName, (e) => e.preventDefault(), { passive: false });
|
|
1074
1064
|
});
|
|
1075
1065
|
return btn;
|
|
1076
1066
|
};
|
|
@@ -1118,7 +1108,9 @@ var PlayerController = class {
|
|
|
1118
1108
|
{ passive: false }
|
|
1119
1109
|
);
|
|
1120
1110
|
}
|
|
1121
|
-
|
|
1111
|
+
/**
|
|
1112
|
+
* 销毁移动端摇杆控制
|
|
1113
|
+
*/
|
|
1122
1114
|
destroyMobileControls() {
|
|
1123
1115
|
try {
|
|
1124
1116
|
if (this.joystickManager && this.joystickManager.destroy) {
|
|
@@ -1153,6 +1145,33 @@ var PlayerController = class {
|
|
|
1153
1145
|
console.warn("\u9500\u6BC1\u79FB\u52A8\u7AEF\u6447\u6746\u63A7\u5236\u65F6\u51FA\u9519\uFF1A", e);
|
|
1154
1146
|
}
|
|
1155
1147
|
}
|
|
1148
|
+
// ==================== 销毁 ====================
|
|
1149
|
+
/**
|
|
1150
|
+
* 销毁人物控制器
|
|
1151
|
+
*/
|
|
1152
|
+
destroy() {
|
|
1153
|
+
this.offAllEvent();
|
|
1154
|
+
if (this.player) {
|
|
1155
|
+
this.player.remove(this.camera);
|
|
1156
|
+
this.scene.remove(this.player);
|
|
1157
|
+
}
|
|
1158
|
+
this.player = null;
|
|
1159
|
+
if (this.person) {
|
|
1160
|
+
this.scene.remove(this.person);
|
|
1161
|
+
this.person = null;
|
|
1162
|
+
}
|
|
1163
|
+
this.resetControls();
|
|
1164
|
+
if (this.visualizer) {
|
|
1165
|
+
this.scene.remove(this.visualizer);
|
|
1166
|
+
this.visualizer = null;
|
|
1167
|
+
}
|
|
1168
|
+
if (this.collider) {
|
|
1169
|
+
this.scene.remove(this.collider);
|
|
1170
|
+
this.collider = null;
|
|
1171
|
+
}
|
|
1172
|
+
this.destroyMobileControls();
|
|
1173
|
+
controllerInstance = null;
|
|
1174
|
+
}
|
|
1156
1175
|
};
|
|
1157
1176
|
function playerController() {
|
|
1158
1177
|
if (!controllerInstance) controllerInstance = new PlayerController();
|