three-player-controller 0.1.6 → 0.1.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 CHANGED
@@ -1,49 +1,57 @@
1
1
  # three-player-controller
2
2
 
3
- 轻量的第三人称 / 第一人称玩家控制器,开箱即用,基于 three.js three-mesh-bvh 实现人物胶囊体碰撞、BVH 碰撞检测、人物动画、第一/三人称切换与相机避障。k此仓库包含库源码、example 演示。
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.
4
4
 
5
- # 安装
5
+ # Installation
6
6
 
7
7
  npm install three-player-controller
8
8
 
9
- # 示例
9
+ # Demo
10
10
 
11
11
  [Player Controller](https://hh-hang.github.io/three-player-controller/)
12
12
 
13
- ### 控制
14
- ![控制演示](example/public/gif/1.gif)
13
+ ### Controls
15
14
 
16
- ### 第三人称相机避障
17
- ![第三人称相机避障](example/public/gif/2.gif)
15
+ ![Controls](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/1.gif)
18
16
 
19
- # 使用
17
+ ### fly
18
+
19
+ ![fly](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/3.gif)
20
+
21
+ ### Third-person camera obstacle avoidance
22
+
23
+ ![Third-person camera obstacle avoidance](https://github.com/hh-hang/three-player-controller/blob/master/example/public/gif/2.gif)
24
+
25
+ # Usage
20
26
 
21
27
  ```js
22
28
  import * as THREE from "three";
23
29
  import { playerController } from "three-player-controller";
24
30
 
25
- // 初始化玩家控制器
31
+ const player = playerController();
32
+
33
+ // Initialize the player controller
26
34
  player.init({
27
- scene, // three.js 场景
28
- camera, // three.js 相机
29
- controls, // three.js 控制器
30
- playerModel: {
31
- url: "./glb/person.glb", // 模型路径
32
- scale: 0.001, // 模型缩放
33
- idleAnim: "Idle_2", // 默认 Idle 动画名字
34
- walkAnim: "Walking_11", // 默认 Walk 动画名字
35
- runAnim: "Running_9", // 默认 Run 动画名字
36
- jumpAnim: "Jump_3", // 默认 Jump 动画名字
37
- },
38
- initPos: pos, // 初始位置
35
+ scene, // three.js scene
36
+ camera, // three.js camera
37
+ controls, // three.js controls
38
+ playerModel: {
39
+ url: "./glb/person.glb", // model path
40
+ scale: 0.001, // model scale
41
+ idleAnim: "Idle_2", // idle animation name
42
+ walkAnim: "Walking_11", // walk animation name
43
+ runAnim: "Running_9", // run animation name
44
+ jumpAnim: "Jump_3", // jump animation name
45
+ },
46
+ initPos: pos, // initial position
39
47
  });
40
48
 
41
- // 渲染循环调用
49
+ // Call in the render loop
42
50
  player.update();
43
-
44
51
  ```
45
- # 感谢
52
+
53
+ # Thanks
46
54
 
47
55
  [three-mesh-bvh](https://github.com/gkjohnson/three-mesh-bvh)
48
56
 
49
- [three](https://github.com/mrdoob/three.js)
57
+ [three](https://github.com/mrdoob/three.js)
package/dist/index.d.mts CHANGED
@@ -15,10 +15,11 @@ 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;
package/dist/index.d.ts CHANGED
@@ -15,10 +15,11 @@ 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;
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
  // 平滑系数
@@ -102,6 +101,7 @@ var PlayerController = class {
102
101
  this.DIR_BKD = new THREE.Vector3(0, 0, 1);
103
102
  this.DIR_LFT = new THREE.Vector3(-1, 0, 0);
104
103
  this.DIR_RGT = new THREE.Vector3(1, 0, 0);
104
+ this.DIR_Y = new THREE.Vector3(0, 1, 0);
105
105
  this._personToCam = new THREE.Vector3();
106
106
  this._originTmp = new THREE.Vector3();
107
107
  this._raycaster = new THREE.Raycaster(new THREE.Vector3(), new THREE.Vector3(0, -1, 0));
@@ -133,12 +133,11 @@ var PlayerController = class {
133
133
  this.setAnimationByPressed();
134
134
  break;
135
135
  case "Space":
136
- if (!this.spacePressed) this.spacePressed = true;
137
- if (!this.spaceLongPressTimer) {
138
- this.spaceLongPressTimer = setTimeout(() => {
139
- this.sustainSpacePressed = true;
140
- }, 2e3);
141
- }
136
+ if (!this.playerIsOnGround || this.isFlying) return;
137
+ this.spacePressed = true;
138
+ this.playPersonAnimationByName("jumping");
139
+ this.playerVelocity.y = this.jumpHeight;
140
+ this.playerIsOnGround = false;
142
141
  break;
143
142
  case "ControlLeft":
144
143
  this.ctPressed = true;
@@ -146,6 +145,10 @@ var PlayerController = class {
146
145
  case "KeyV":
147
146
  this.changeView();
148
147
  break;
148
+ case "KeyF":
149
+ this.isFlying = !this.isFlying;
150
+ this.setAnimationByPressed();
151
+ break;
149
152
  }
150
153
  };
151
154
  // 键盘抬起事件
@@ -172,12 +175,6 @@ var PlayerController = class {
172
175
  this.setAnimationByPressed();
173
176
  break;
174
177
  case "Space":
175
- if (this.spaceLongPressTimer) {
176
- clearTimeout(this.spaceLongPressTimer);
177
- this.spaceLongPressTimer = null;
178
- }
179
- this.spacePressed = false;
180
- this.sustainSpacePressed = false;
181
178
  break;
182
179
  case "ControlLeft":
183
180
  this.ctPressed = false;
@@ -186,6 +183,16 @@ var PlayerController = class {
186
183
  };
187
184
  // 根据按键设置人物动画
188
185
  this.setAnimationByPressed = () => {
186
+ this._maxCamDistance = 440 * this.playerModel.scale;
187
+ if (this.isFlying) {
188
+ if (!this.fwdPressed) {
189
+ this.playPersonAnimationByName("flyidle");
190
+ return;
191
+ }
192
+ this.playPersonAnimationByName("flying");
193
+ this._maxCamDistance = 700 * this.playerModel.scale;
194
+ return;
195
+ }
189
196
  if (this.playerIsOnGround) {
190
197
  if (!this.fwdPressed && !this.bkdPressed && !this.lftPressed && !this.rgtPressed) {
191
198
  this.playPersonAnimationByName("idle");
@@ -219,6 +226,8 @@ var PlayerController = class {
219
226
  this.playPersonAnimationByName("walking_backward");
220
227
  return;
221
228
  }
229
+ } else {
230
+ this.playPersonAnimationByName("jumping");
222
231
  }
223
232
  };
224
233
  // 鼠标移动事件
@@ -265,8 +274,7 @@ var PlayerController = class {
265
274
  const s = this.playerModel.scale;
266
275
  this.visualizeDepth = 0 * s;
267
276
  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;
277
+ this.jumpHeight = opts.playerModel.jumpHeight ? opts.playerModel.jumpHeight * s : 800 * s;
270
278
  this.playerSpeed = opts.playerModel.speed ? opts.playerModel.speed * s : 400 * s;
271
279
  this._camCollisionLerp = 0.18;
272
280
  this._camEpsilon = 35 * s;
@@ -318,7 +326,7 @@ var PlayerController = class {
318
326
  // 设置控制器
319
327
  setControls() {
320
328
  this.controls.enabled = false;
321
- this.controls.maxPolarAngle = Math.PI * (230 / 360);
329
+ this.controls.maxPolarAngle = Math.PI * (300 / 360);
322
330
  }
323
331
  // 重置控制器
324
332
  resetControls() {
@@ -358,6 +366,7 @@ var PlayerController = class {
358
366
  this.reset();
359
367
  this.personMixer = new THREE.AnimationMixer(this.person);
360
368
  const animations = gltf.animations ?? [];
369
+ console.log("animations", animations);
361
370
  this.personActions = /* @__PURE__ */ new Map();
362
371
  const findClip = (name) => animations.find((a) => a.name === name);
363
372
  const regs = [
@@ -367,7 +376,9 @@ var PlayerController = class {
367
376
  [this.playerModel.rightWalkAnim || this.playerModel.walkAnim, "right_walking"],
368
377
  [this.playerModel.backwardAnim || this.playerModel.walkAnim, "walking_backward"],
369
378
  [this.playerModel.jumpAnim, "jumping"],
370
- [this.playerModel.runAnim, "running"]
379
+ [this.playerModel.runAnim, "running"],
380
+ [this.playerModel.flyIdleAnim || this.playerModel.idleAnim, "flyidle"],
381
+ [this.playerModel.flyAnim || this.playerModel.idleAnim, "flying"]
371
382
  ];
372
383
  for (const [key, clipName] of regs) {
373
384
  const clip = findClip(key);
@@ -393,6 +404,8 @@ var PlayerController = class {
393
404
  this.backwardAction = this.personActions.get("walking_backward");
394
405
  this.jumpAction = this.personActions.get("jumping");
395
406
  this.runAction = this.personActions.get("running");
407
+ this.flyidleAction = this.personActions.get("flyidle");
408
+ this.flyAction = this.personActions.get("flying");
396
409
  this.idleAction.setEffectiveWeight(1);
397
410
  this.idleAction.play();
398
411
  this.actionState = this.idleAction;
@@ -447,6 +460,7 @@ var PlayerController = class {
447
460
  material.transparent = true;
448
461
  material.opacity = this.displayPlayer ? 0.5 : 0;
449
462
  material.wireframe = true;
463
+ material.depthWrite = false;
450
464
  const r = this.playerRadius * this.playerModel.scale;
451
465
  const h = this.playerHeight * this.playerModel.scale;
452
466
  this.player = new THREE.Mesh(new import_RoundedBoxGeometry.RoundedBoxGeometry(r * 2, h, r * 2, 1, 75), material);
@@ -469,10 +483,10 @@ var PlayerController = class {
469
483
  }
470
484
  // 每帧更新
471
485
  async update(delta = clock.getDelta()) {
472
- if (!this.isupdate || !this.player) return;
473
- delta = Math.min(delta, 1 / 30);
486
+ if (!this.isupdate || !this.player || !this.collider) return;
487
+ delta = Math.min(delta, 1 / 60);
488
+ if (!this.isFlying) this.player.position.addScaledVector(this.playerVelocity, delta);
474
489
  this.updateMixers(delta);
475
- if (!this.collider) return;
476
490
  this.camera.getWorldDirection(this.camDir);
477
491
  let angle = Math.atan2(this.camDir.z, this.camDir.x) + Math.PI / 2;
478
492
  angle = 2 * Math.PI - angle;
@@ -481,31 +495,16 @@ var PlayerController = class {
481
495
  if (this.bkdPressed) this.moveDir.add(this.DIR_BKD);
482
496
  if (this.lftPressed) this.moveDir.add(this.DIR_LFT);
483
497
  if (this.rgtPressed) this.moveDir.add(this.DIR_RGT);
484
- if (this.spacePressed && this.playerIsOnGround) {
485
- this.playPersonAnimationByName("jumping");
486
- setTimeout(() => {
487
- this.playerVelocity.y = this.jumpHeight;
488
- this.playerIsOnGround = false;
489
- this.spacePressed = false;
490
- this.player.position.addScaledVector(this.playerVelocity, delta);
491
- this.player.updateMatrixWorld();
492
- }, 200);
493
- }
494
- if (this.sustainSpacePressed && this.playerIsOnGround) {
495
- this.playPersonAnimationByName("jumping");
496
- setTimeout(() => {
497
- this.playerVelocity.y = this.highJumpHeight;
498
- this.playerIsOnGround = false;
499
- this.spacePressed = false;
500
- this.player.position.addScaledVector(this.playerVelocity, delta);
501
- this.player.updateMatrixWorld();
502
- }, 200);
498
+ if (this.isFlying && this.fwdPressed) {
499
+ this.moveDir.y = this.camDir.y;
503
500
  }
504
- this.playerSpeed = this.shiftPressed ? 900 * this.playerModel.scale : 400 * this.playerModel.scale;
505
- if (this.moveDir.lengthSq() > 1e-6) {
506
- this.moveDir.normalize().applyAxisAngle(this.upVector, angle);
507
- this.player.position.addScaledVector(this.moveDir, this.playerSpeed * delta);
501
+ if (this.isFlying && this.fwdPressed) {
502
+ this.playerSpeed = this.shiftPressed ? 4e3 * this.playerModel.scale : 3e3 * this.playerModel.scale;
503
+ } else {
504
+ this.playerSpeed = this.shiftPressed ? 900 * this.playerModel.scale : 400 * this.playerModel.scale;
508
505
  }
506
+ this.moveDir.normalize().applyAxisAngle(this.upVector, angle);
507
+ this.player.position.addScaledVector(this.moveDir, this.playerSpeed * delta);
509
508
  let playerDistanceFromGround = Infinity;
510
509
  this._originTmp.set(this.player.position.x, this.player.position.y, this.player.position.z);
511
510
  this._raycaster.ray.origin.copy(this._originTmp);
@@ -517,27 +516,29 @@ var PlayerController = class {
517
516
  const maxH = this.playerHeight * this.playerModel.scale * 0.9;
518
517
  const h = this.playerHeight * this.playerModel.scale * 0.75;
519
518
  const minH = this.playerHeight * this.playerModel.scale * 0.7;
520
- if (playerDistanceFromGround > maxH) {
521
- this.playerVelocity.y += delta * this.gravity;
522
- this.player.position.addScaledVector(this.playerVelocity, delta);
523
- this.playerIsOnGround = false;
524
- } else if (playerDistanceFromGround > h && playerDistanceFromGround < maxH) {
525
- if (angle2 >= 0 && angle2 < 5) {
526
- console.log("\u5E73\u5730");
519
+ if (this.isFlying) {
520
+ } else {
521
+ if (playerDistanceFromGround > maxH) {
527
522
  this.playerVelocity.y += delta * this.gravity;
528
523
  this.player.position.addScaledVector(this.playerVelocity, delta);
529
524
  this.playerIsOnGround = false;
530
- } else {
525
+ } else if (playerDistanceFromGround > h && playerDistanceFromGround < maxH) {
526
+ if (angle2 >= 0 && angle2 < 5) {
527
+ this.playerVelocity.y += delta * this.gravity;
528
+ this.player.position.addScaledVector(this.playerVelocity, delta);
529
+ this.playerIsOnGround = true;
530
+ } else {
531
+ this.playerVelocity.set(0, 0, 0);
532
+ this.playerIsOnGround = true;
533
+ }
534
+ } else if (playerDistanceFromGround > minH && playerDistanceFromGround < h) {
531
535
  this.playerVelocity.set(0, 0, 0);
532
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;
533
541
  }
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
542
  }
542
543
  }
543
544
  this.player.updateMatrixWorld();
@@ -569,12 +570,9 @@ var PlayerController = class {
569
570
  });
570
571
  const newPosition = this.tempVector.copy(this.tempSegment.start).applyMatrix4(this.collider.matrixWorld);
571
572
  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
- }
573
+ const offset = Math.max(0, deltaVector.length() - 1e-5);
574
+ deltaVector.normalize().multiplyScalar(offset);
575
+ this.player.position.add(deltaVector);
578
576
  if (!this.isFirstPerson && this.moveDir.lengthSq() > 0) {
579
577
  this.camDir.y = 0;
580
578
  this.camDir.normalize();
@@ -607,18 +605,15 @@ var PlayerController = class {
607
605
  const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
608
606
  this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
609
607
  } else {
610
- const dis = this.player.position.distanceTo(this.camera.position);
611
608
  this._raycasterPersonToCam.far = this._maxCamDistance;
612
609
  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);
610
+ let safeDist = this._maxCamDistance;
611
+ if (intersectsMaxDis.length) {
612
+ const hitMax = intersectsMaxDis[0];
613
+ safeDist = hitMax.distance - this._camEpsilon;
621
614
  }
615
+ const targetCamPos = origin.clone().add(direction.clone().multiplyScalar(safeDist));
616
+ this.camera.position.lerp(targetCamPos, this._camCollisionLerp);
622
617
  }
623
618
  }
624
619
  if (this.player.position.y < this.boundingBoxMinY - 1) {
@@ -684,7 +679,6 @@ var PlayerController = class {
684
679
  // 更新模型动画
685
680
  updateMixers(delta) {
686
681
  if (this.personMixer) this.personMixer.update(delta);
687
- if (this.droneMixer) this.droneMixer.update(delta);
688
682
  }
689
683
  // BVH构建
690
684
  async createBVH(meshUrl = "") {
@@ -779,12 +773,6 @@ var PlayerController = class {
779
773
  merged.boundsTree = new import_three_mesh_bvh.MeshBVH(merged);
780
774
  this.collider = new THREE.Mesh(
781
775
  merged,
782
- // new THREE.MeshBasicMaterial({
783
- // color: "red",
784
- // opacity: 0.2,
785
- // transparent: true,
786
- // wireframe: false,
787
- // })
788
776
  new THREE.MeshBasicMaterial({
789
777
  opacity: 0.5,
790
778
  transparent: true,