@needle-tools/engine 2.28.0-pre → 2.29.0-pre

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/needle-engine.d.ts +188 -131
  3. package/dist/needle-engine.js +345 -345
  4. package/dist/needle-engine.js.map +4 -4
  5. package/dist/needle-engine.min.js +19 -19
  6. package/dist/needle-engine.min.js.map +4 -4
  7. package/lib/engine/engine_input.d.ts +13 -1
  8. package/lib/engine/engine_input.js +47 -16
  9. package/lib/engine/engine_input.js.map +1 -1
  10. package/lib/engine/engine_physics.d.ts +1 -0
  11. package/lib/engine/engine_physics.js +2 -1
  12. package/lib/engine/engine_physics.js.map +1 -1
  13. package/lib/engine/engine_playerview.d.ts +26 -0
  14. package/lib/engine/engine_playerview.js +65 -0
  15. package/lib/engine/engine_playerview.js.map +1 -0
  16. package/lib/engine/engine_serialization_core.js +5 -0
  17. package/lib/engine/engine_serialization_core.js.map +1 -1
  18. package/lib/engine/engine_setup.d.ts +3 -0
  19. package/lib/engine/engine_setup.js +17 -0
  20. package/lib/engine/engine_setup.js.map +1 -1
  21. package/lib/engine/extensions/NEEDLE_lighting_settings.js +6 -2
  22. package/lib/engine/extensions/NEEDLE_lighting_settings.js.map +1 -1
  23. package/lib/engine-components/Component.d.ts +1 -1
  24. package/lib/engine-components/Component.js.map +1 -1
  25. package/lib/engine-components/Light.js +1 -0
  26. package/lib/engine-components/Light.js.map +1 -1
  27. package/lib/engine-components/OrbitControls.js +1 -2
  28. package/lib/engine-components/OrbitControls.js.map +1 -1
  29. package/lib/engine-components/ParticleSystem.d.ts +0 -1
  30. package/lib/engine-components/ParticleSystem.js +24 -27
  31. package/lib/engine-components/ParticleSystem.js.map +1 -1
  32. package/lib/engine-components/PlayerColor.js +1 -2
  33. package/lib/engine-components/PlayerColor.js.map +1 -1
  34. package/lib/engine-components/SpectatorCamera.d.ts +24 -17
  35. package/lib/engine-components/SpectatorCamera.js +410 -181
  36. package/lib/engine-components/SpectatorCamera.js.map +1 -1
  37. package/lib/engine-components/SyncedCamera.d.ts +8 -4
  38. package/lib/engine-components/SyncedCamera.js +15 -18
  39. package/lib/engine-components/SyncedCamera.js.map +1 -1
  40. package/lib/engine-components/WebXR.js +1 -0
  41. package/lib/engine-components/WebXR.js.map +1 -1
  42. package/lib/engine-components/WebXRAvatar.d.ts +3 -0
  43. package/lib/engine-components/WebXRAvatar.js +16 -0
  44. package/lib/engine-components/WebXRAvatar.js.map +1 -1
  45. package/lib/engine-components/WebXRController.js +1 -1
  46. package/lib/engine-components/WebXRController.js.map +1 -1
  47. package/lib/engine-components/WebXRSync.js +3 -3
  48. package/lib/engine-components/WebXRSync.js.map +1 -1
  49. package/lib/engine-components/XRFlag.d.ts +2 -1
  50. package/lib/engine-components/XRFlag.js +1 -0
  51. package/lib/engine-components/XRFlag.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/engine/engine_components.js +16 -0
  54. package/src/engine/engine_input.ts +62 -20
  55. package/src/engine/engine_physics.ts +2 -1
  56. package/src/engine/engine_playerview.ts +80 -0
  57. package/src/engine/engine_serialization_core.ts +8 -0
  58. package/src/engine/engine_setup.ts +18 -0
  59. package/src/engine/extensions/NEEDLE_lighting_settings.ts +4 -2
  60. package/src/engine-components/Component.ts +1 -1
  61. package/src/engine-components/Light.ts +3 -0
  62. package/src/engine-components/OrbitControls.ts +1 -2
  63. package/src/engine-components/ParticleSystem.ts +25 -26
  64. package/src/engine-components/PlayerColor.ts +1 -1
  65. package/src/engine-components/SpectatorCamera.ts +466 -194
  66. package/src/engine-components/SyncedCamera.ts +23 -22
  67. package/src/engine-components/WebXR.ts +1 -0
  68. package/src/engine-components/WebXRAvatar.ts +22 -2
  69. package/src/engine-components/WebXRController.ts +1 -1
  70. package/src/engine-components/WebXRSync.ts +3 -3
  71. package/src/engine-components/XRFlag.ts +1 -0
@@ -1,4 +1,4 @@
1
- import { Behaviour, GameObject } from "./Component";
1
+ import { Behaviour, Component, GameObject } from "./Component";
2
2
  import { Camera } from "./Camera";
3
3
  import * as THREE from "three";
4
4
  import { OrbitControls } from "./OrbitControls";
@@ -6,54 +6,102 @@ import { WebXR, WebXREvent } from "./WebXR";
6
6
  import { AvatarMarker } from "./WebXRAvatar";
7
7
  import { XRStateFlag } from "./XRFlag";
8
8
  import { SmoothFollow } from "./SmoothFollow";
9
- import { setWorldPosition, setWorldQuaternion, getWorldPosition, getWorldQuaternion, lookAtInverse } from "../engine/engine_three_utils";
10
- import { ArrayCamera } from "three";
11
- import { KeyCode } from "../engine/engine_input";
9
+ import { Object3D } from "three";
10
+ import { InputEvents, KeyCode } from "../engine/engine_input";
11
+ import { Context } from "../engine/engine_setup";
12
+ import { getParam } from "../engine/engine_utils";
13
+ import { PlayerView, ViewDevice } from "../engine/engine_playerview";
14
+ import { RaycastOptions } from "../engine/engine_physics";
15
+ import { IModel, RoomEvents } from "../engine/engine_networking";
16
+
17
+
18
+ export enum SpectatorMode {
19
+ FirstPerson = 0,
20
+ ThirdPerson = 1,
21
+ }
22
+
23
+ const debug = getParam("debugspectator");
12
24
 
13
25
  export class SpectatorCamera extends Behaviour {
14
26
 
15
27
  cam: Camera | null = null;
16
28
 
17
- private _firstPersonMode: boolean | undefined = true;
18
- get firstPersonMode(): boolean {
19
- return this._firstPersonMode ?? false;
29
+ private _mode: SpectatorMode = SpectatorMode.FirstPerson;
30
+
31
+ get mode() { return this._mode; }
32
+ set mode(val: SpectatorMode) {
33
+ this._mode = val;
34
+ }
35
+
36
+ /** if this user is currently spectating someone else */
37
+ get isSpectating(): boolean {
38
+ return this._handler?.currentTarget !== undefined;
39
+ }
40
+
41
+ isSpectatingUser(userId: string): boolean {
42
+ return this.target?.userId === userId;
43
+ }
44
+
45
+ isFollowedBy(userId: string): boolean {
46
+ return this.followers?.includes(userId);
20
47
  }
21
- set firstPersonMode(val: boolean) {
22
- this._firstPersonMode = val;
23
- // if (this._firstPersonMode) this.enableFirstPersonMode();
24
- // else this.enableThirdPersonMode();
48
+
49
+ /** list of other users that are following me */
50
+ get followers(): string[] {
51
+ return this._networking.followers;
52
+ }
53
+
54
+ stopSpectating() {
55
+ if (this.context.isInXR) {
56
+ this.followSelf();
57
+ return;
58
+ }
59
+ this.target = undefined;
25
60
  }
26
61
 
27
- enableFirstPersonMode() {
28
- this._firstPersonMode = true;
29
- this.updateUI();
30
- if (this.cam) {
31
- if (this.firstPersonFollow) {
32
- this.firstPersonFollow.enabled = true;
33
- this.firstPersonFollow.updateNow(true);
62
+ /** player view to follow */
63
+ set target(target: PlayerView | undefined) {
64
+ if (this._handler) {
65
+
66
+ // if (this.target?.userId) {
67
+ // const isFollowedByThisUser = this.followers.includes(this.target.userId);
68
+ // if (isFollowedByThisUser) {
69
+ // console.warn("Can not follow follower");
70
+ // target = undefined;
71
+ // }
72
+ // }
73
+
74
+ const prev = this._handler.currentTarget?.userId;
75
+ const self = this.context.players.getPlayerView(this.context.connection.connectionId);
76
+
77
+ // if user is in XR and sets target to self disable it
78
+ if (target === undefined || (this.context.isInXR === false && self?.currentObject === target.currentObject)) {
79
+ if (this._handler.currentTarget !== undefined) {
80
+ this._handler.disable();
81
+ GameObject.setActive(this.gameObject, false);
82
+ if (this.orbit) this.orbit.enabled = true;
83
+ this._networking.onSpectatedObjectChanged(target, prev);
84
+ }
34
85
  }
35
- if (this.orbit) {
36
- this.orbit.enabled = false;
86
+ else if (this._handler.currentTarget !== target) {
87
+ this._handler.set(target);
88
+ GameObject.setActive(this.gameObject, true);
89
+ if (this.orbit) this.orbit.enabled = false;
90
+ this._networking.onSpectatedObjectChanged(target, prev);
37
91
  }
38
92
  }
39
93
  }
40
94
 
41
- enableThirdPersonMode() {
42
- this._firstPersonMode = false;
43
- this.updateUI();
44
- if (this.firstPersonFollow) {
45
- this.firstPersonFollow.enabled = false;
46
- }
47
- if (!this.cam) return;
48
- setWorldPosition(this.cam.cam, this._orbitStartPos);
49
- setWorldQuaternion(this.cam.cam, this._orbitStartRot);
50
- setWorldPosition(this.cam.gameObject, this._orbitStartPos2);
51
- setWorldQuaternion(this.cam.gameObject, this._orbitStartRot2);
52
-
53
- if (this.orbit) {
54
- this.orbit.enabled = true;
55
- this.orbit.setFromTargetPosition();
56
- }
95
+ get target(): PlayerView | undefined {
96
+ return this._handler?.currentTarget;
97
+ }
98
+
99
+ requestAllFollowMe() {
100
+ this._networking.onRequestFollowMe();
101
+ }
102
+
103
+ private get isSpectatingSelf() {
104
+ return this.isSpectating && this.target?.currentObject === this.context.players.getPlayerView(this.context.connection.connectionId)?.currentObject;
57
105
  }
58
106
 
59
107
  // private currentViewport : THREE.Vector4 = new THREE.Vector4();
@@ -61,53 +109,31 @@ export class SpectatorCamera extends Behaviour {
61
109
  // private currentScissorTest : boolean = false;
62
110
 
63
111
  private orbit: OrbitControls | null = null;
64
- private firstPersonFollow: SmoothFollow | null = null;
65
- private spectatorUIDomElement: HTMLElement | null = null;
66
-
112
+ private _handler?: ISpectatorHandler;
67
113
  private eventSub_WebXRRequestStartEvent: Function | null = null;
68
114
  private eventSub_WebXRStartEvent: Function | null = null;
69
115
  private eventSub_WebXREndEvent: Function | null = null;
116
+ private _debug?: SpectatorSelectionController;
117
+ private _networking!: SpectatorCamNetworking;
70
118
 
71
119
  awake(): void {
72
120
 
73
- GameObject.setActive(this.gameObject, false);
121
+ this._debug = new SpectatorSelectionController(this.context, this);
122
+ this._networking = new SpectatorCamNetworking(this.context, this);
123
+ this._networking.awake();
74
124
 
75
- if (!this.isSupportedPlatform()) {
76
- console.log("Disable spectator cam", window.navigator.userAgent, this);
77
- return;
78
- }
125
+ GameObject.setActive(this.gameObject, false);
79
126
 
80
127
  this.cam = GameObject.getComponent(this.gameObject, Camera);
81
128
  if (!this.cam) {
82
- // TODO: adding camera component does currently not work properly
83
129
  console.error("Spectator camera needs camera component", this);
84
130
  return;
85
- // this.cam = GameObject.addNewComponent(this.gameObject, Camera) as Camera;
86
131
  }
87
132
 
88
- const uiQuery = "#spectator-camera-ui";
89
- this.spectatorUIDomElement = this.context.domElement.querySelector(uiQuery);
90
- if (!this.spectatorUIDomElement) {
91
- console.warn("Could not find spectator camera UI element", uiQuery);
92
- // this.spectatorUIDomElement = document.createElement("div");
93
- // this.spectatorUIDomElement.id = "spectator-camera-ui";
94
- // this.spectatorUIDomElement.classList.add("desktop");
95
- // this.context.domElement.appendChild(this.spectatorUIDomElement);
96
-
97
- // const toggle = document.createElement("button");
98
- // toggle.id = "toggle-spectator-view";
99
- // this.spectatorUIDomElement.appendChild(toggle);
100
- }
101
- this.spectatorUIDomElement?.classList.add("hidden");
102
133
 
134
+ if (!this._handler && this.cam)
135
+ this._handler = new SpectatorHandler(this.context, this.cam, this);
103
136
 
104
- if (this.cam) {
105
- this.cam.enabled = true;
106
- this._orbitStartPos.copy(getWorldPosition(this.cam.cam));
107
- this._orbitStartRot.copy(getWorldQuaternion(this.cam.cam));
108
- this._orbitStartPos2.copy(getWorldPosition(this.cam.gameObject));
109
- this._orbitStartRot2.copy(getWorldQuaternion(this.cam.gameObject));
110
- }
111
137
 
112
138
  this.eventSub_WebXRRequestStartEvent = this.onXRSessionRequestStart.bind(this);
113
139
  this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
@@ -117,42 +143,16 @@ export class SpectatorCamera extends Behaviour {
117
143
  WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
118
144
  WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
119
145
 
120
- this.context.domElement.querySelector("button#toggle-spectator-view")?.addEventListener("click", this.toggleView.bind(this));
121
- this.updateUI();
146
+ this.orbit = GameObject.getComponent(this.context.mainCamera, OrbitControls);
122
147
  }
123
148
 
124
149
  onDestroy(): void {
150
+ this.stopSpectating();
125
151
  WebXR.removeEventListener(WebXREvent.RequestVRSession, this.eventSub_WebXRStartEvent);
126
152
  WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
127
153
  WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
128
- }
129
-
130
- private toggleView() {
131
- this.firstPersonMode = !this.firstPersonMode;
132
- }
133
-
134
- private updateUI() {
135
- const el: any = this.context.domElement.querySelector("button#toggle-spectator-view");
136
- if (!el) return;
137
- el.disabled = true;// !this._sessionHasStarted;
138
- if (el.firstChild) {
139
- if (!this._sessionHasStarted) {
140
- el.firstChild.textContent = "Waiting for VR session";
141
- }
142
- else
143
- el.firstChild.textContent = this.firstPersonMode ? "👁️" : "📷";
144
- }
145
-
146
- if (el.children?.length > 1) {
147
- const tooltipElement = el.children[1];
148
- tooltipElement.style.display = !this._sessionHasStarted ? "none" : "block";
149
- if (this.firstPersonMode) {
150
- tooltipElement.textContent = "First-Person View. See what the person in VR sees";
151
- }
152
- else {
153
- tooltipElement.textContent = "Third-Person View. Freely move the camera around"
154
- }
155
- }
154
+ this._handler?.destroy();
155
+ this._networking.destroy();
156
156
  }
157
157
 
158
158
  private isSupportedPlatform() {
@@ -163,60 +163,32 @@ export class SpectatorCamera extends Behaviour {
163
163
  }
164
164
 
165
165
  private onXRSessionRequestStart(_evt) {
166
- this._sessionHasStarted = false;
167
- this.spectatorUIDomElement?.classList.remove("hidden");
168
-
166
+ if (!this.isSupportedPlatform()) return;
169
167
  GameObject.setActive(this.gameObject, true);
170
-
171
- // if (!this.orbit && this.cam) {
172
- // const lookAt = GameObject.addNewComponent(this.gameObject, LookAtConstraint);
173
- // lookAt.constraintActive = true;
174
- // const center = new THREE.Object3D();
175
- // this.context.scene.add(center);
176
- // lookAt.sources = [center];
177
- // this.orbit = GameObject.addNewComponent(this.cam?.gameObject, OrbitControls);
178
- // this.orbit.lookAtConstraint = lookAt;
179
- // this.orbit.debugLog = true;
180
- // this.orbit.enabled = !this.firstPersonMode;
181
- // }
182
-
183
- if (this.cam && this.cam.cam) {
184
- setWorldPosition(this.cam.cam, this._orbitStartPos);
185
- setWorldQuaternion(this.cam.cam, this._orbitStartRot);
186
- setWorldPosition(this.cam.gameObject, this._orbitStartPos2);
187
- setWorldQuaternion(this.cam.gameObject, this._orbitStartRot2);
188
- }
189
-
190
- if (this.firstPersonMode) {
191
- this.enableFirstPersonMode();
192
- }
193
- else this.enableThirdPersonMode();
194
168
  }
195
169
 
196
170
 
197
171
  private onXRSessionStart(_evt) {
198
- this._sessionHasStarted = true;
199
- this.updateUI();
200
-
172
+ if (!this.isSupportedPlatform()) return;
201
173
  if (this.context.mainCamera) {
202
- const cam = this.context.renderer.xr.getCamera(this.context.mainCamera) as ArrayCamera;
203
- this.setupFollowMode(cam);
174
+ this.followSelf();
204
175
  }
205
176
  }
206
177
 
207
178
  private onXRSessionEnded(_evt) {
208
- this._sessionHasStarted = false;
209
- this._firstPersonIsSetup = false;
210
- this.spectatorUIDomElement?.classList.add("hidden");
179
+ this.context.removeCamera(this.cam);
211
180
  GameObject.setActive(this.gameObject, false);
181
+ if (this.orbit) this.orbit.enabled = true;
182
+ this._handler?.set(undefined);
183
+ this._handler?.disable();
184
+ if (this.isSpectatingSelf)
185
+ this.stopSpectating();
212
186
  }
213
187
 
214
- private _sessionHasStarted: boolean = false;
215
- private _firstPersonIsSetup: boolean = false;
216
- private _orbitStartPos: THREE.Vector3 = new THREE.Vector3();
217
- private _orbitStartRot: THREE.Quaternion = new THREE.Quaternion();
218
- private _orbitStartPos2: THREE.Vector3 = new THREE.Vector3();
219
- private _orbitStartRot2: THREE.Quaternion = new THREE.Quaternion();
188
+
189
+ private followSelf() {
190
+ this.target = this.context.players.getPlayerView(this.context.connection.connectionId);
191
+ }
220
192
 
221
193
  // TODO: only show Spectator cam for DesktopVR;
222
194
  // don't show for AR, don't show on Quest
@@ -224,19 +196,12 @@ export class SpectatorCamera extends Behaviour {
224
196
  onAfterRender(): void {
225
197
  if (!this.cam) return;
226
198
 
227
- if(this.context.input.isKeyDown(KeyCode.KEY_S))
228
- this.firstPersonMode = !this.firstPersonMode;
229
-
230
- this.updateFollowSettings();
231
-
232
199
  const renderer = this.context.renderer;
233
200
  const xrWasEnabled = renderer.xr.enabled;
234
- // these should not be needed if we don't override viewport/scissor
235
- // renderer.getViewport(this.currentViewport);
236
- // renderer.getScissor(this.currentScissor);
237
- // this.currentScissorTest = renderer.getScissorTest();
238
201
 
239
- if (!renderer.xr.isPresenting) return;
202
+ if (!renderer.xr.isPresenting && !this._handler?.currentTarget) return;
203
+
204
+ this._handler?.update(this._mode);
240
205
 
241
206
  // remember XR render target so we can restore later
242
207
  const previousRenderTarget = renderer.getRenderTarget();
@@ -254,6 +219,10 @@ export class SpectatorCamera extends Behaviour {
254
219
 
255
220
  this.setAvatarFlagsBeforeRender();
256
221
 
222
+ // these should not be needed if we don't override viewport/scissor
223
+ // renderer.getViewport(this.currentViewport);
224
+ // renderer.getScissor(this.currentScissor);
225
+ // this.currentScissorTest = renderer.getScissorTest();
257
226
  // for scissor rendering (e.g. just a part of the screen / viewport, multiplayer split view)
258
227
  // let left = 0;
259
228
  // let bottom = 100;
@@ -287,55 +256,14 @@ export class SpectatorCamera extends Behaviour {
287
256
  this.resetAvatarFlags();
288
257
  }
289
258
 
290
- private setupFollowMode(object: THREE.Object3D) {
291
- if (!object) return;
292
- if (!this.cam) return;
293
- if (this._firstPersonIsSetup) return;
294
- this._firstPersonIsSetup = true;
295
-
296
-
297
- this.firstPersonFollow = GameObject.addNewComponent(this.cam.gameObject, SmoothFollow);
298
-
299
- const target = new THREE.Object3D();
300
- object.add(target);
301
- target.add(new THREE.AxesHelper(.2))
302
- this.firstPersonFollow.target = target;
303
-
304
- this.updateFollowSettings();
305
-
306
- const perspectiveCamera = this.context.mainCamera as THREE.PerspectiveCamera;
307
- if (perspectiveCamera) {
308
- this.cam.cam.near = perspectiveCamera.near;
309
- this.cam.cam.far = perspectiveCamera.far;
310
- this.cam.cam.updateProjectionMatrix();
311
- }
312
- if (this.orbit) this.orbit.enabled = false;
313
- }
314
-
315
- private updateFollowSettings() {
316
- const target = this.firstPersonFollow?.target;
317
- if (!target || !this.firstPersonFollow) return;
318
- if (this.firstPersonMode === false) {
319
- this.firstPersonFollow.followFactor = 3;
320
- this.firstPersonFollow.rotateFactor = 2;
321
- this.firstPersonFollow.flipForward = false;
322
- target.position.set(0, .5, 1.5);
323
- target.quaternion.identity();
324
- // lookAtInverse(target, new THREE.Vector3(0, 0, 0));
325
- }
326
- else {
327
- target.position.set(0, 0, 0);
328
- target.quaternion.identity();
329
- this.firstPersonFollow.followFactor = 12;
330
- this.firstPersonFollow.rotateFactor = 5;
331
- this.firstPersonFollow.flipForward = false;
332
- }
333
- }
334
-
335
259
  private setAvatarFlagsBeforeRender() {
260
+ const isFirstPersonMode = this._mode === SpectatorMode.FirstPerson;
261
+
336
262
  for (const av of AvatarMarker.instances) {
337
263
  if (av.avatar && "isLocalAvatar" in av.avatar) {
338
- const mask = this.firstPersonMode && this._firstPersonIsSetup && av.avatar.isLocalAvatar ? XRStateFlag.FirstPerson : XRStateFlag.ThirdPerson;
264
+ let mask = XRStateFlag.All;
265
+ if (this.isSpectatingSelf)
266
+ mask = isFirstPersonMode && av.avatar.isLocalAvatar ? XRStateFlag.FirstPerson : XRStateFlag.ThirdPerson;
339
267
  const flags = av.avatar.flags;
340
268
  if (!flags) continue;
341
269
  for (const flag of flags) {
@@ -362,3 +290,347 @@ export class SpectatorCamera extends Behaviour {
362
290
  }
363
291
  }
364
292
  }
293
+
294
+ interface ISpectatorHandler {
295
+ context: Context;
296
+ get currentTarget(): PlayerView | undefined;
297
+ set(target?: PlayerView): void;
298
+ update(mode: SpectatorMode);
299
+ disable();
300
+ destroy();
301
+ }
302
+
303
+ class SpectatorHandler implements ISpectatorHandler {
304
+
305
+ readonly context: Context;
306
+ readonly cam: Camera;
307
+ readonly spectator: SpectatorCamera;
308
+
309
+ private follow?: SmoothFollow;
310
+ private target?: THREE.Object3D;
311
+ private view?: PlayerView;
312
+ private currentObject: Object3D | undefined;
313
+
314
+ get currentTarget(): PlayerView | undefined {
315
+ return this.view;
316
+ }
317
+
318
+ constructor(context: Context, cam: Camera, spectator: SpectatorCamera) {
319
+ this.context = context;
320
+ this.cam = cam;
321
+ this.spectator = spectator;
322
+ }
323
+
324
+ set(view?: PlayerView): void {
325
+ const followObject = view?.currentObject;
326
+ if (!followObject) {
327
+ this.spectator.stopSpectating();
328
+ return;
329
+ }
330
+ if (followObject === this.currentObject) return;
331
+ this.currentObject = followObject;
332
+ this.view = view;
333
+ if (!this.follow)
334
+ this.follow = GameObject.addNewComponent(this.cam.gameObject, SmoothFollow);
335
+ if (!this.target)
336
+ this.target = new THREE.Object3D();
337
+ followObject.add(this.target);
338
+
339
+ this.follow.enabled = true;
340
+ this.follow.target = this.target;
341
+ // this.context.setCurrentCamera(this.cam);
342
+ if (debug) console.log("FOLLOW", followObject);
343
+ if (!this.context.isInXR) {
344
+ this.context.setCurrentCamera(this.cam);
345
+ }
346
+ else this.context.removeCamera(this.cam);
347
+ }
348
+
349
+ disable() {
350
+ if (debug) console.log("STOP FOLLOW", this.currentObject);
351
+ this.view = undefined;
352
+ this.currentObject = undefined;
353
+ this.context.removeCamera(this.cam);
354
+ if (this.follow)
355
+ this.follow.enabled = false;
356
+ }
357
+
358
+ destroy() {
359
+ this.target?.removeFromParent();
360
+ if (this.follow)
361
+ GameObject.destroy(this.follow);
362
+ }
363
+
364
+ update(mode: SpectatorMode) {
365
+ if (this.currentTarget?.isConnected === false || this.currentTarget?.removed === true) {
366
+ if (debug) console.log("Target disconnected or timeout", this.currentTarget);
367
+ this.spectator.stopSpectating();
368
+ return;
369
+ }
370
+ if (this.currentTarget && this.currentTarget?.currentObject !== this.currentObject) {
371
+ if (debug) console.log("Target changed", this.currentObject, "to", this.currentTarget.currentObject);
372
+ this.set(this.currentTarget);
373
+ }
374
+ const perspectiveCamera = this.context.mainCamera as THREE.PerspectiveCamera;
375
+ if (perspectiveCamera) {
376
+ if (this.cam.cam.near !== perspectiveCamera.near || this.cam.cam.far !== perspectiveCamera.far) {
377
+ this.cam.cam.near = perspectiveCamera.near;
378
+ this.cam.cam.far = perspectiveCamera.far;
379
+ this.cam.cam.updateProjectionMatrix();
380
+ }
381
+ }
382
+
383
+ const target = this.follow?.target;
384
+ if (!target || !this.follow) return;
385
+ switch (mode) {
386
+ case SpectatorMode.FirstPerson:
387
+ this.follow.followFactor = 20;
388
+ this.follow.rotateFactor = 20;
389
+ target.position.set(0, 0, 0);
390
+ break;
391
+ case SpectatorMode.ThirdPerson:
392
+ this.follow.followFactor = 3;
393
+ this.follow.rotateFactor = 2;
394
+ target.position.set(0, .5, 1.5);
395
+ break;
396
+ }
397
+ this.follow.flipForward = false;
398
+ // console.log(this.view);
399
+ if (this.view?.viewDevice !== ViewDevice.Browser)
400
+ target.quaternion.copy(_inverseYQuat);
401
+ else target.quaternion.identity();
402
+ }
403
+
404
+
405
+ }
406
+
407
+ const _inverseYQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
408
+
409
+
410
+ class SpectatorSelectionController {
411
+
412
+ private readonly context: Context;
413
+ private readonly spectator: SpectatorCamera;
414
+
415
+ constructor(context: Context, spectator: SpectatorCamera) {
416
+ this.context = context;
417
+ this.spectator = spectator;
418
+ console.log("Click other avatars or cameras to follow them. Press ESC to exit spectator mode.");
419
+ window.addEventListener("keydown", (evt) => {
420
+ const key = evt.key;
421
+ if (key === "Escape") {
422
+ this.spectator.stopSpectating();
423
+ }
424
+ });
425
+ let downTime: number = 0;
426
+ this.context.input.addEventListener(InputEvents.PointerDown, _ => {
427
+ downTime = this.context.time.time;
428
+ });
429
+ this.context.input.addEventListener(InputEvents.PointerUp, _ => {
430
+ if (this.context.time.time - downTime > 1) {
431
+ this.spectator.stopSpectating();
432
+ }
433
+ else
434
+ this.trySelectObject();
435
+ });
436
+ }
437
+
438
+ private trySelectObject() {
439
+ const opts = new RaycastOptions();
440
+ opts.setMask(0xffffff);
441
+ // opts.cam = this.spectator.cam?.cam;
442
+ const hits = this.context.physics.raycast(opts);
443
+ if (debug) console.log(...hits);
444
+ if (hits?.length) {
445
+ for (const hit of hits) {
446
+ if (hit.distance < .2) continue;
447
+ const obj = hit.object;
448
+ const avatar = GameObject.getComponentInParent(obj, AvatarMarker);
449
+ const id = avatar?.connectionId;
450
+ if (id) {
451
+ const view = this.context.players.getPlayerView(id);
452
+ this.spectator.target = view;
453
+ if (debug) console.log("spectate", id, avatar);
454
+ break;
455
+ }
456
+ }
457
+ }
458
+ }
459
+ }
460
+
461
+
462
+
463
+
464
+
465
+ class SpectatorFollowerChangedEventModel implements IModel {
466
+ /** the user that is following */
467
+ guid: string;
468
+ readonly dontSave: boolean = true;
469
+
470
+ /** the user being followed */
471
+ targetUserId: string | undefined;
472
+ stoppedFollowing: boolean;
473
+
474
+ constructor(connectionId: string, userId: string | undefined, stoppedFollowing: boolean) {
475
+ this.guid = connectionId;
476
+ this.targetUserId = userId;
477
+ this.stoppedFollowing = stoppedFollowing;
478
+ }
479
+ }
480
+
481
+ class SpectatorFollowEventModel implements IModel {
482
+ guid: string;
483
+ userId: string | undefined;
484
+
485
+ constructor(comp: Component, userId: string | undefined) {
486
+ this.guid = comp.guid;
487
+ this.userId = userId;
488
+ }
489
+ }
490
+
491
+ class SpectatorCamNetworking {
492
+
493
+ readonly followers: string[] = [];
494
+
495
+
496
+ private readonly context: Context;
497
+ private readonly spectator: SpectatorCamera;
498
+ private _followerEventMethod: Function;
499
+ private _requestFollowMethod: Function;
500
+ private _joinedRoomMethod: Function;
501
+
502
+ constructor(context: Context, spectator: SpectatorCamera) {
503
+ this.context = context;
504
+ this.spectator = spectator;
505
+ this._followerEventMethod = this.onFollowerEvent.bind(this);
506
+ this._requestFollowMethod = this.onRequestFollowEvent.bind(this);
507
+ this._joinedRoomMethod = this.onUserJoinedRoom.bind(this);
508
+ }
509
+
510
+ awake() {
511
+ this.context.connection.beginListen("spectator-follower-changed", this._followerEventMethod);
512
+ this.context.connection.beginListen("spectator-request-follow", this._requestFollowMethod);
513
+ this.context.connection.beginListen(RoomEvents.JoinedRoom, this._joinedRoomMethod);
514
+ document.addEventListener("keydown", evt => {
515
+ if (evt.key === "f") {
516
+ this.onRequestFollowMe();
517
+ }
518
+ else if (evt.key === "Escape") {
519
+ this.onRequestFollowMe(true);
520
+ }
521
+ });
522
+ }
523
+
524
+ destroy() {
525
+ this.context.connection.stopListening("spectator-follower-changed", this._followerEventMethod);
526
+ this.context.connection.stopListening("spectator-request-follow", this._requestFollowMethod);
527
+ this.context.connection.stopListening(RoomEvents.JoinedRoom, this._joinedRoomMethod);
528
+ }
529
+
530
+ onSpectatedObjectChanged(target: PlayerView | undefined, _prevId?: string) {
531
+ if (debug)
532
+ console.log(this.context.connection.connectionId, "onSpectatedObjectChanged", target, _prevId);
533
+ if (this.context.connection.connectionId) {
534
+ const stopped = target?.userId === undefined;
535
+ const userId = stopped ? _prevId : target?.userId;
536
+ const evt = new SpectatorFollowerChangedEventModel(this.context.connection.connectionId, userId, stopped);
537
+ this.context.connection.send("spectator-follower-changed", evt)
538
+ }
539
+ }
540
+
541
+ onRequestFollowMe(stop: boolean = false) {
542
+ if (debug)
543
+ console.log("Request follow", this.context.connection.connectionId);
544
+ if (this.context.connection.connectionId) {
545
+ this.spectator.stopSpectating();
546
+ const id = stop ? undefined : this.context.connection.connectionId;
547
+ const model = new SpectatorFollowEventModel(this.spectator, id);
548
+ this.context.connection.send("spectator-request-follow", model);
549
+ }
550
+ }
551
+
552
+ private onUserJoinedRoom() {
553
+ if (getParam("followme")) {
554
+ this.onRequestFollowMe();
555
+ }
556
+ }
557
+
558
+ private onFollowerEvent(evt: SpectatorFollowerChangedEventModel) {
559
+ const userBeingFollowed = evt.targetUserId;
560
+ const userThatIsFollowing = evt.guid;
561
+
562
+ if (debug)
563
+ console.log(evt);
564
+
565
+ if (userBeingFollowed === this.context.connection.connectionId) {
566
+ if (evt.stoppedFollowing) {
567
+ const index = this.followers.indexOf(userThatIsFollowing);
568
+ if (index !== -1) {
569
+ this.followers.splice(index, 1);
570
+ this.removeDisconnectedFollowers();
571
+ console.log(userThatIsFollowing, "unfollows you", this.followers.length);
572
+ }
573
+ }
574
+ else {
575
+ if (!this.followers.includes(userThatIsFollowing)) {
576
+ this.followers.push(userThatIsFollowing);
577
+ this.removeDisconnectedFollowers();
578
+ console.log(userThatIsFollowing, "follows you", this.followers.length);
579
+ }
580
+ }
581
+ }
582
+ }
583
+
584
+ private removeDisconnectedFollowers() {
585
+ for (let i = this.followers.length - 1; i >= 0; i--) {
586
+ const id = this.followers[i];
587
+ if (this.context.connection.userIsInRoom(id) === false) {
588
+ this.followers.splice(i, 1);
589
+ }
590
+ }
591
+ }
592
+
593
+ private _lastRequestFollowUser: SpectatorFollowEventModel | undefined;
594
+
595
+ private onRequestFollowEvent(evt: SpectatorFollowEventModel) {
596
+ this._lastRequestFollowUser = evt;
597
+
598
+ if (evt.userId === this.context.connection.connectionId) {
599
+ this.spectator.stopSpectating();
600
+ }
601
+ else if (evt.userId === undefined) {
602
+ // this will currently also stop spectating if the user is not following you
603
+ this.spectator.stopSpectating();
604
+ }
605
+ else {
606
+ const view = this.context.players.getPlayerView(evt.userId);
607
+ if (view) {
608
+ this.spectator.target = view;
609
+ }
610
+ else {
611
+ if (debug)
612
+ console.warn("Could not find view", evt.userId);
613
+ this.enforceFollow();
614
+ return false;
615
+ }
616
+ }
617
+ return true;
618
+ }
619
+
620
+ private _enforceFollowInterval: any;
621
+ private enforceFollow() {
622
+ if (this._enforceFollowInterval) return;
623
+ this._enforceFollowInterval = setInterval(() => {
624
+ if (this._lastRequestFollowUser === undefined || this._lastRequestFollowUser.userId && this.spectator.isFollowedBy(this._lastRequestFollowUser.userId)) {
625
+ clearInterval(this._enforceFollowInterval);
626
+ this._enforceFollowInterval = undefined;
627
+ }
628
+ else {
629
+ if (debug)
630
+ console.log("REQUEST FOLLOW AGAIN", this._lastRequestFollowUser.userId);
631
+ this.onRequestFollowEvent(this._lastRequestFollowUser);
632
+ }
633
+
634
+ }, 1000);
635
+ }
636
+ }