three-player-controller 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,50 +1,57 @@
1
1
  # three-player-controller
2
2
 
3
- A lightweight third-person / first-person player controller, ready to use out of the box, based on three.js and three-mesh-bvh. It implements capsule-based character collision, BVH collision detection, character animations, first/third-person switching, and camera obstacle avoidance. This repository contains the library source code and example demos.
3
+ 轻量的第三人称 / 第一人称玩家控制器,开箱即用,基于 three.js three-mesh-bvh 实现人物胶囊体碰撞、BVH 碰撞检测、人物动画、第一/三人称切换与相机避障。此仓库包含库源码、example 演示。
4
4
 
5
- # Installation
5
+ # 安装
6
6
 
7
7
  npm install three-player-controller
8
8
 
9
- # Demo
9
+ # 示例
10
10
 
11
11
  [Player Controller](https://hh-hang.github.io/three-player-controller/)
12
12
 
13
- ### Controls
13
+ ### 控制
14
14
 
15
- ![Controls](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/1.gif)
15
+ ![控制演示](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/1.gif)
16
16
 
17
- ### Third-person camera obstacle avoidance
17
+ ### 飞行
18
18
 
19
- ![Third-person camera obstacle avoidance](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/2.gif)
19
+ ![飞行](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/3.gif)
20
20
 
21
- # Usage
21
+ ### 3DTiles 漫游
22
+
23
+ ![3DTiles 漫游](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/4.gif)
24
+
25
+
26
+ # 使用
22
27
 
23
28
  ```js
24
29
  import * as THREE from "three";
25
30
  import { playerController } from "three-player-controller";
26
31
 
27
- // Initialize the player controller
32
+ const player = playerController();
33
+
34
+ // 初始化玩家控制器
28
35
  player.init({
29
- scene, // three.js scene
30
- camera, // three.js camera
31
- controls, // three.js controls
36
+ scene, // three.js 场景
37
+ camera, // three.js 相机
38
+ controls, // three.js 控制器
32
39
  playerModel: {
33
- url: "./glb/person.glb", // model path
34
- scale: 0.001, // model scale
35
- idleAnim: "Idle_2", // idle animation name
36
- walkAnim: "Walking_11", // walk animation name
37
- runAnim: "Running_9", // run animation name
38
- jumpAnim: "Jump_3", // jump animation name
40
+ url: "./glb/person.glb", // 模型路径
41
+ scale: 0.001, // 模型缩放
42
+ idleAnim: "Idle_2", // 默认 Idle 动画名字
43
+ walkAnim: "Walking_11", // 默认 Walk 动画名字
44
+ runAnim: "Running_9", // 默认 Run 动画名字
45
+ jumpAnim: "Jump_3", // 默认 Jump 动画名字
39
46
  },
40
- initPos: pos, // initial position
47
+ initPos: pos, // 初始位置
41
48
  });
42
49
 
43
- // Call in the render loop
50
+ // 渲染循环调用
44
51
  player.update();
45
52
  ```
46
53
 
47
- # Thanks
54
+ # 感谢
48
55
 
49
56
  [three-mesh-bvh](https://github.com/gkjohnson/three-mesh-bvh)
50
57
 
package/dist/index.d.mts CHANGED
@@ -15,14 +15,17 @@ declare function playerController(): {
15
15
  leftWalkAnim?: string;
16
16
  rightWalkAnim?: string;
17
17
  backwardAnim?: string;
18
+ flyAnim?: string;
19
+ flyIdleAnim?: string;
18
20
  scale: number;
19
21
  gravity?: number;
20
22
  jumpHeight?: number;
21
- highJumpHeight?: number;
22
23
  speed?: number;
23
24
  };
24
25
  initPos?: THREE.Vector3;
25
26
  mouseSensity?: number;
27
+ minCamDistance?: number;
28
+ maxCamDistance?: number;
26
29
  }, callback?: () => void) => Promise<void>;
27
30
  changeView: () => void;
28
31
  createBVH: (url?: string) => Promise<void>;
package/dist/index.d.ts CHANGED
@@ -15,14 +15,17 @@ declare function playerController(): {
15
15
  leftWalkAnim?: string;
16
16
  rightWalkAnim?: string;
17
17
  backwardAnim?: string;
18
+ flyAnim?: string;
19
+ flyIdleAnim?: string;
18
20
  scale: number;
19
21
  gravity?: number;
20
22
  jumpHeight?: number;
21
- highJumpHeight?: number;
22
23
  speed?: number;
23
24
  };
24
25
  initPos?: THREE.Vector3;
25
26
  mouseSensity?: number;
27
+ minCamDistance?: number;
28
+ maxCamDistance?: number;
26
29
  }, callback?: () => void) => Promise<void>;
27
30
  changeView: () => void;
28
31
  createBVH: (url?: string) => Promise<void>;
package/dist/index.js CHANGED
@@ -63,6 +63,7 @@ var PlayerController = class {
63
63
  // 状态开关
64
64
  this.playerIsOnGround = false;
65
65
  this.isupdate = true;
66
+ this.isFlying = false;
66
67
  // 输入状态
67
68
  this.fwdPressed = false;
68
69
  this.bkdPressed = false;
@@ -71,8 +72,6 @@ var PlayerController = class {
71
72
  this.spacePressed = false;
72
73
  this.ctPressed = false;
73
74
  this.shiftPressed = false;
74
- this.sustainSpacePressed = false;
75
- this.spaceLongPressTimer = null;
76
75
  // 第三人称
77
76
  this._camCollisionLerp = 0.18;
78
77
  // 平滑系数
@@ -82,6 +81,7 @@ var PlayerController = class {
82
81
  // 摄像机最小距离
83
82
  this._maxCamDistance = 4.4;
84
83
  // 摄像机最大距离
84
+ this.orginMaxCamDistance = 4.4;
85
85
  // 物理/运动
86
86
  this.playerVelocity = new THREE.Vector3();
87
87
  // 玩家速度向量
@@ -102,6 +102,7 @@ var PlayerController = class {
102
102
  this.DIR_BKD = new THREE.Vector3(0, 0, 1);
103
103
  this.DIR_LFT = new THREE.Vector3(-1, 0, 0);
104
104
  this.DIR_RGT = new THREE.Vector3(1, 0, 0);
105
+ this.DIR_UP = new THREE.Vector3(0, 1, 0);
105
106
  this._personToCam = new THREE.Vector3();
106
107
  this._originTmp = new THREE.Vector3();
107
108
  this._raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0));
@@ -133,12 +134,11 @@ var PlayerController = class {
133
134
  this.setAnimationByPressed();
134
135
  break;
135
136
  case "Space":
136
- if (!this.spacePressed) this.spacePressed = true;
137
- if (!this.spaceLongPressTimer) {
138
- this.spaceLongPressTimer = setTimeout(() => {
139
- this.sustainSpacePressed = true;
140
- }, 2e3);
141
- }
137
+ this.spacePressed = true;
138
+ if (!this.playerIsOnGround || this.isFlying) return;
139
+ this.playPersonAnimationByName("jumping");
140
+ this.playerVelocity.y = this.jumpHeight;
141
+ this.playerIsOnGround = false;
142
142
  break;
143
143
  case "ControlLeft":
144
144
  this.ctPressed = true;
@@ -146,6 +146,15 @@ var PlayerController = class {
146
146
  case "KeyV":
147
147
  this.changeView();
148
148
  break;
149
+ case "KeyF":
150
+ this.isFlying = !this.isFlying;
151
+ this._maxCamDistance = this.isFlying ? this.orginMaxCamDistance * 2 : this.orginMaxCamDistance;
152
+ if (!this.isFlying && !this.playerIsOnGround) {
153
+ this.playPersonAnimationByName("jumping");
154
+ } else {
155
+ this.setAnimationByPressed();
156
+ }
157
+ break;
149
158
  }
150
159
  };
151
160
  // 键盘抬起事件
@@ -172,12 +181,7 @@ var PlayerController = class {
172
181
  this.setAnimationByPressed();
173
182
  break;
174
183
  case "Space":
175
- if (this.spaceLongPressTimer) {
176
- clearTimeout(this.spaceLongPressTimer);
177
- this.spaceLongPressTimer = null;
178
- }
179
184
  this.spacePressed = false;
180
- this.sustainSpacePressed = false;
181
185
  break;
182
186
  case "ControlLeft":
183
187
  this.ctPressed = false;
@@ -186,6 +190,16 @@ var PlayerController = class {
186
190
  };
187
191
  // 根据按键设置人物动画
188
192
  this.setAnimationByPressed = () => {
193
+ this._maxCamDistance = this.orginMaxCamDistance;
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
+ }
189
203
  if (this.playerIsOnGround) {
190
204
  if (!this.fwdPressed && !this.bkdPressed && !this.lftPressed && !this.rgtPressed) {
191
205
  this.playPersonAnimationByName("idle");
@@ -265,13 +279,13 @@ var PlayerController = class {
265
279
  const s = this.playerModel.scale;
266
280
  this.visualizeDepth = 0 * s;
267
281
  this.gravity = opts.playerModel.gravity ? opts.playerModel.gravity * s : -2400 * s;
268
- this.jumpHeight = opts.playerModel.jumpHeight ? opts.playerModel.jumpHeight * s : 300 * s;
269
- this.highJumpHeight = opts.playerModel.highJumpHeight ? opts.playerModel.highJumpHeight * s : 1e3 * s;
282
+ this.jumpHeight = opts.playerModel.jumpHeight ? opts.playerModel.jumpHeight * s : 800 * s;
270
283
  this.playerSpeed = opts.playerModel.speed ? opts.playerModel.speed * s : 400 * s;
271
284
  this._camCollisionLerp = 0.18;
272
285
  this._camEpsilon = 35 * s;
273
- this._minCamDistance = 100 * s;
274
- this._maxCamDistance = 440 * s;
286
+ this._minCamDistance = opts.minCamDistance ? opts.minCamDistance * s : 100 * s;
287
+ this._maxCamDistance = opts.maxCamDistance ? opts.maxCamDistance * s : 440 * s;
288
+ this.orginMaxCamDistance = this._maxCamDistance;
275
289
  await this.createBVH();
276
290
  this.createPlayer();
277
291
  await this.loadPersonGLB();
@@ -318,10 +332,11 @@ var PlayerController = class {
318
332
  // 设置控制器
319
333
  setControls() {
320
334
  this.controls.enabled = false;
321
- this.controls.maxPolarAngle = Math.PI * (230 / 360);
335
+ this.controls.maxPolarAngle = Math.PI * (300 / 360);
322
336
  }
323
337
  // 重置控制器
324
338
  resetControls() {
339
+ if (!this.controls) return;
325
340
  this.controls.enabled = true;
326
341
  this.controls.enablePan = true;
327
342
  this.controls.maxPolarAngle = Math.PI / 2;
@@ -352,6 +367,7 @@ var PlayerController = class {
352
367
  this.person.traverse((child) => {
353
368
  if (child.isMesh) {
354
369
  child.castShadow = true;
370
+ child.receiveShadow = true;
355
371
  }
356
372
  });
357
373
  this.player.add(this.person);
@@ -367,7 +383,9 @@ var PlayerController = class {
367
383
  [this.playerModel.rightWalkAnim || this.playerModel.walkAnim, "right_walking"],
368
384
  [this.playerModel.backwardAnim || this.playerModel.walkAnim, "walking_backward"],
369
385
  [this.playerModel.jumpAnim, "jumping"],
370
- [this.playerModel.runAnim, "running"]
386
+ [this.playerModel.runAnim, "running"],
387
+ [this.playerModel.flyIdleAnim || this.playerModel.idleAnim, "flyidle"],
388
+ [this.playerModel.flyAnim || this.playerModel.idleAnim, "flying"]
371
389
  ];
372
390
  for (const [key, clipName] of regs) {
373
391
  const clip = findClip(key);
@@ -393,6 +411,8 @@ var PlayerController = class {
393
411
  this.backwardAction = this.personActions.get("walking_backward");
394
412
  this.jumpAction = this.personActions.get("jumping");
395
413
  this.runAction = this.personActions.get("running");
414
+ this.flyidleAction = this.personActions.get("flyidle");
415
+ this.flyAction = this.personActions.get("flying");
396
416
  this.idleAction.setEffectiveWeight(1);
397
417
  this.idleAction.play();
398
418
  this.actionState = this.idleAction;
@@ -460,6 +480,7 @@ var PlayerController = class {
460
480
  this.scene.add(this.player);
461
481
  this.reset();
462
482
  }
483
+ // 获取法线与Y轴的夹角
463
484
  getAngleWithYAxis(normal) {
464
485
  const yAxis = { x: 0, y: 1, z: 0 };
465
486
  const dotProduct = normal.x * yAxis.x + normal.y * yAxis.y + normal.z * yAxis.z;
@@ -470,10 +491,10 @@ var PlayerController = class {
470
491
  }
471
492
  // 每帧更新
472
493
  async update(delta = clock.getDelta()) {
473
- if (!this.isupdate || !this.player) return;
494
+ if (!this.isupdate || !this.player || !this.collider) return;
474
495
  delta = Math.min(delta, 1 / 30);
496
+ if (!this.isFlying) this.player.position.addScaledVector(this.playerVelocity, delta);
475
497
  this.updateMixers(delta);
476
- if (!this.collider) return;
477
498
  this.camera.getWorldDirection(this.camDir);
478
499
  let angle = Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2;
479
500
  angle = 2 * Math.PI - angle;
@@ -482,31 +503,23 @@ var PlayerController = class {
482
503
  if (this.bkdPressed) this.moveDir.add(this.DIR_BKD);
483
504
  if (this.lftPressed) this.moveDir.add(this.DIR_LFT);
484
505
  if (this.rgtPressed) this.moveDir.add(this.DIR_RGT);
485
- if (this.spacePressed && this.playerIsOnGround) {
486
- this.playPersonAnimationByName("jumping");
487
- setTimeout(() => {
488
- this.playerVelocity.y = this.jumpHeight;
489
- this.playerIsOnGround = false;
490
- this.spacePressed = false;
491
- this.player.position.addScaledVector(this.playerVelocity, delta);
492
- this.player.updateMatrixWorld();
493
- }, 200);
494
- }
495
- if (this.sustainSpacePressed && this.playerIsOnGround) {
496
- this.playPersonAnimationByName("jumping");
497
- setTimeout(() => {
498
- this.playerVelocity.y = this.highJumpHeight;
499
- this.playerIsOnGround = false;
500
- this.spacePressed = false;
501
- this.player.position.addScaledVector(this.playerVelocity, delta);
502
- this.player.updateMatrixWorld();
503
- }, 200);
506
+ if (this.isFlying) {
507
+ if (this.fwdPressed) {
508
+ this.moveDir.y = this.camDir.y;
509
+ } else {
510
+ this.moveDir.y = 0;
511
+ }
512
+ if (this.spacePressed) {
513
+ this.moveDir.add(this.DIR_UP);
514
+ }
504
515
  }
505
- this.playerSpeed = this.shiftPressed ? 900 * this.playerModel.scale : 400 * this.playerModel.scale;
506
- if (this.moveDir.lengthSq() > 1e-6) {
507
- this.moveDir.normalize().applyAxisAngle(this.upVector, angle);
508
- this.player.position.addScaledVector(this.moveDir, this.playerSpeed * delta);
516
+ if (this.isFlying && this.fwdPressed) {
517
+ this.playerSpeed = this.shiftPressed ? 4e3 * this.playerModel.scale : 3e3 * this.playerModel.scale;
518
+ } else {
519
+ this.playerSpeed = this.shiftPressed ? 900 * this.playerModel.scale : 400 * this.playerModel.scale;
509
520
  }
521
+ this.moveDir.normalize().applyAxisAngle(this.upVector, angle);
522
+ this.player.position.addScaledVector(this.moveDir, this.playerSpeed * delta);
510
523
  let playerDistanceFromGround = Infinity;
511
524
  this._originTmp.set(this.player.position.x, this.player.position.y, this.player.position.z);
512
525
  this._raycaster.ray.origin.copy(this._originTmp);
@@ -518,26 +531,30 @@ var PlayerController = class {
518
531
  const maxH = this.playerHeight * this.playerModel.scale * 0.9;
519
532
  const h = this.playerHeight * this.playerModel.scale * 0.75;
520
533
  const minH = this.playerHeight * this.playerModel.scale * 0.7;
521
- if (playerDistanceFromGround > maxH) {
522
- this.playerVelocity.y += delta * this.gravity;
523
- this.player.position.addScaledVector(this.playerVelocity, delta);
524
- this.playerIsOnGround = false;
525
- } else if (playerDistanceFromGround > h && playerDistanceFromGround < maxH) {
526
- if (angle2 >= 0 && angle2 < 5) {
527
- this.player.position.y = intersects[0].point.y + h;
534
+ console.log(angle2, playerDistanceFromGround, maxH, h, minH);
535
+ if (this.isFlying) {
536
+ } else {
537
+ if (playerDistanceFromGround > maxH) {
538
+ this.playerVelocity.y += delta * this.gravity;
539
+ this.player.position.addScaledVector(this.playerVelocity, delta);
540
+ this.playerIsOnGround = false;
541
+ } else if (playerDistanceFromGround > h && playerDistanceFromGround < maxH) {
542
+ if (angle2 >= 0 && angle2 < 5) {
543
+ this.playerVelocity.y += delta * this.gravity;
544
+ this.player.position.addScaledVector(this.playerVelocity, delta);
545
+ this.playerIsOnGround = true;
546
+ } else {
547
+ this.playerVelocity.set(0, 0, 0);
548
+ this.playerIsOnGround = true;
549
+ }
550
+ } else if (playerDistanceFromGround > minH && playerDistanceFromGround < h) {
528
551
  this.playerVelocity.set(0, 0, 0);
529
552
  this.playerIsOnGround = true;
530
- } else {
553
+ } else if (playerDistanceFromGround < minH) {
531
554
  this.playerVelocity.set(0, 0, 0);
555
+ this.player.position.set(this.player.position.x, intersects[0].point.y + h, this.player.position.z);
532
556
  this.playerIsOnGround = true;
533
557
  }
534
- } else if (playerDistanceFromGround > minH && playerDistanceFromGround < h) {
535
- this.playerVelocity.set(0, 0, 0);
536
- this.playerIsOnGround = true;
537
- } else if (playerDistanceFromGround < minH) {
538
- this.playerVelocity.set(0, 0, 0);
539
- this.player.position.set(this.player.position.x, intersects[0].point.y + h, this.player.position.z);
540
- this.playerIsOnGround = true;
541
558
  }
542
559
  }
543
560
  this.player.updateMatrixWorld();
@@ -569,13 +586,10 @@ var PlayerController = class {
569
586
  });
570
587
  const newPosition = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
571
588
  const deltaVector = this.tempVector2.subVectors(newPosition, this.player.position);
572
- const len = deltaVector.length();
573
- const offset = Math.max(0, len - 1e-5);
574
- if (offset > 0 && len > 0) {
575
- const n = deltaVector.multiplyScalar(1 / len);
576
- this.player.position.addScaledVector(n, offset);
577
- }
578
- if (!this.isFirstPerson && this.moveDir.lengthSq() > 0) {
589
+ const offset = Math.max(0, deltaVector.length() - 1e-5);
590
+ deltaVector.normalize().multiplyScalar(offset);
591
+ this.player.position.add(deltaVector);
592
+ if (!this.isFirstPerson && this.moveDir.lengthSq() > 0 && !this.isFlying) {
579
593
  this.camDir.y = 0;
580
594
  this.camDir.normalize();
581
595
  this.camDir.negate();
@@ -587,6 +601,18 @@ var PlayerController = class {
587
601
  const alpha = Math.min(1, this.rotationSpeed * delta);
588
602
  this.player.quaternion.slerp(this.targetQuat, alpha);
589
603
  }
604
+ if (this.isFlying) {
605
+ this.camDir.y = 0;
606
+ this.camDir.normalize();
607
+ this.camDir.negate();
608
+ this.moveDir.normalize();
609
+ this.moveDir.negate();
610
+ const lookTarget = this.player.position.clone().add(this.fwdPressed ? this.moveDir : this.camDir);
611
+ this.targetMat.lookAt(this.player.position, lookTarget, this.player.up);
612
+ this.targetQuat.setFromRotationMatrix(this.targetMat);
613
+ const alpha = Math.min(1, this.rotationSpeed * delta);
614
+ this.player.quaternion.slerp(this.targetQuat, alpha);
615
+ }
590
616
  if (!this.isFirstPerson) {
591
617
  const lookTarget = this.player.position.clone();
592
618
  lookTarget.y += 30 * this.playerModel.scale;
@@ -607,18 +633,15 @@ var PlayerController = class {
607
633
  const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
608
634
  this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
609
635
  } else {
610
- const dis = this.player.position.distanceTo(this.camera.position);
611
636
  this._raycasterPersonToCam.far = this._maxCamDistance;
612
637
  const intersectsMaxDis = this._raycasterPersonToCam.intersectObject(this.collider, false);
613
- if (dis < this._maxCamDistance) {
614
- let safeDist = this._maxCamDistance;
615
- if (intersectsMaxDis.length) {
616
- const hitMax = intersectsMaxDis[0];
617
- safeDist = hitMax.distance - this._camEpsilon;
618
- }
619
- const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
620
- this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
638
+ let safeDist = this._maxCamDistance;
639
+ if (intersectsMaxDis.length) {
640
+ const hitMax = intersectsMaxDis[0];
641
+ safeDist = hitMax.distance - this._camEpsilon;
621
642
  }
643
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
644
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
622
645
  }
623
646
  }
624
647
  if (this.player.position.y < this.boundingBoxMinY - 1) {
@@ -684,7 +707,6 @@ var PlayerController = class {
684
707
  // 更新模型动画
685
708
  updateMixers(delta) {
686
709
  if (this.personMixer) this.personMixer.update(delta);
687
- if (this.droneMixer) this.droneMixer.update(delta);
688
710
  }
689
711
  // BVH构建
690
712
  async createBVH(meshUrl = "") {