@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.
- package/CHANGELOG.md +10 -0
- package/dist/needle-engine.d.ts +188 -131
- package/dist/needle-engine.js +345 -345
- package/dist/needle-engine.js.map +4 -4
- package/dist/needle-engine.min.js +19 -19
- package/dist/needle-engine.min.js.map +4 -4
- package/lib/engine/engine_input.d.ts +13 -1
- package/lib/engine/engine_input.js +47 -16
- package/lib/engine/engine_input.js.map +1 -1
- package/lib/engine/engine_physics.d.ts +1 -0
- package/lib/engine/engine_physics.js +2 -1
- package/lib/engine/engine_physics.js.map +1 -1
- package/lib/engine/engine_playerview.d.ts +26 -0
- package/lib/engine/engine_playerview.js +65 -0
- package/lib/engine/engine_playerview.js.map +1 -0
- package/lib/engine/engine_serialization_core.js +5 -0
- package/lib/engine/engine_serialization_core.js.map +1 -1
- package/lib/engine/engine_setup.d.ts +3 -0
- package/lib/engine/engine_setup.js +17 -0
- package/lib/engine/engine_setup.js.map +1 -1
- package/lib/engine/extensions/NEEDLE_lighting_settings.js +6 -2
- package/lib/engine/extensions/NEEDLE_lighting_settings.js.map +1 -1
- package/lib/engine-components/Component.d.ts +1 -1
- package/lib/engine-components/Component.js.map +1 -1
- package/lib/engine-components/Light.js +1 -0
- package/lib/engine-components/Light.js.map +1 -1
- package/lib/engine-components/OrbitControls.js +1 -2
- package/lib/engine-components/OrbitControls.js.map +1 -1
- package/lib/engine-components/ParticleSystem.d.ts +0 -1
- package/lib/engine-components/ParticleSystem.js +24 -27
- package/lib/engine-components/ParticleSystem.js.map +1 -1
- package/lib/engine-components/PlayerColor.js +1 -2
- package/lib/engine-components/PlayerColor.js.map +1 -1
- package/lib/engine-components/SpectatorCamera.d.ts +24 -17
- package/lib/engine-components/SpectatorCamera.js +410 -181
- package/lib/engine-components/SpectatorCamera.js.map +1 -1
- package/lib/engine-components/SyncedCamera.d.ts +8 -4
- package/lib/engine-components/SyncedCamera.js +15 -18
- package/lib/engine-components/SyncedCamera.js.map +1 -1
- package/lib/engine-components/WebXR.js +1 -0
- package/lib/engine-components/WebXR.js.map +1 -1
- package/lib/engine-components/WebXRAvatar.d.ts +3 -0
- package/lib/engine-components/WebXRAvatar.js +16 -0
- package/lib/engine-components/WebXRAvatar.js.map +1 -1
- package/lib/engine-components/WebXRController.js +1 -1
- package/lib/engine-components/WebXRController.js.map +1 -1
- package/lib/engine-components/WebXRSync.js +3 -3
- package/lib/engine-components/WebXRSync.js.map +1 -1
- package/lib/engine-components/XRFlag.d.ts +2 -1
- package/lib/engine-components/XRFlag.js +1 -0
- package/lib/engine-components/XRFlag.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/engine_components.js +16 -0
- package/src/engine/engine_input.ts +62 -20
- package/src/engine/engine_physics.ts +2 -1
- package/src/engine/engine_playerview.ts +80 -0
- package/src/engine/engine_serialization_core.ts +8 -0
- package/src/engine/engine_setup.ts +18 -0
- package/src/engine/extensions/NEEDLE_lighting_settings.ts +4 -2
- package/src/engine-components/Component.ts +1 -1
- package/src/engine-components/Light.ts +3 -0
- package/src/engine-components/OrbitControls.ts +1 -2
- package/src/engine-components/ParticleSystem.ts +25 -26
- package/src/engine-components/PlayerColor.ts +1 -1
- package/src/engine-components/SpectatorCamera.ts +466 -194
- package/src/engine-components/SyncedCamera.ts +23 -22
- package/src/engine-components/WebXR.ts +1 -0
- package/src/engine-components/WebXRAvatar.ts +22 -2
- package/src/engine-components/WebXRController.ts +1 -1
- package/src/engine-components/WebXRSync.ts +3 -3
- 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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
if (this.
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
36
|
-
this.
|
|
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
|
-
|
|
42
|
-
this.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
121
|
+
this._debug = new SpectatorSelectionController(this.context, this);
|
|
122
|
+
this._networking = new SpectatorCamNetworking(this.context, this);
|
|
123
|
+
this._networking.awake();
|
|
74
124
|
|
|
75
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
199
|
-
this.updateUI();
|
|
200
|
-
|
|
172
|
+
if (!this.isSupportedPlatform()) return;
|
|
201
173
|
if (this.context.mainCamera) {
|
|
202
|
-
|
|
203
|
-
this.setupFollowMode(cam);
|
|
174
|
+
this.followSelf();
|
|
204
175
|
}
|
|
205
176
|
}
|
|
206
177
|
|
|
207
178
|
private onXRSessionEnded(_evt) {
|
|
208
|
-
this.
|
|
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
|
-
|
|
215
|
-
private
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
+
}
|