Needle Engine

Changes between version 3.9.1-alpha and 3.10.0-alpha
Files changed (4) hide show
  1. src/engine/engine_mainloop_utils.ts +8 -3
  2. src/engine/engine_networking_auto.ts +48 -22
  3. src/engine/engine_types.ts +6 -3
  4. src/engine-components/webxr/WebXR.ts +6 -0
src/engine/engine_mainloop_utils.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import { IComponent, IContext } from './engine_types';
6
6
  import { isActiveSelf } from './engine_gameobject';
7
7
  import { ContextRegistry } from "./engine_context_registry";
8
+ import { showBalloonWarning, isDevEnvironment, showBalloonMessage, LogType } from "./debug";
8
9
 
9
10
  const debug = getParam("debugnewscripts");
10
11
  const debugHierarchy = getParam("debughierarchy");
@@ -171,8 +172,7 @@
171
172
  // keep them in queue until script has started
172
173
  // call awake if the script was inactive before
173
174
  utils.safeInvoke(script.__internalAwake.bind(script));
174
- if(script.enabled)
175
- {
175
+ if (script.enabled) {
176
176
  utils.safeInvoke(script.__internalEnable.bind(script));
177
177
  // now call start
178
178
  utils.safeInvoke(script.__internalStart.bind(script));
@@ -232,8 +232,13 @@
232
232
  const activeSelf = isActiveSelf(obj);
233
233
  const wasSuccessful = updateIsActiveInHierarchyRecursiveRuntime(obj, activeSelf, true);
234
234
  if (!wasSuccessful) {
235
- console.error("Failed to update active state in hierarchy of \"" + obj.name + "\"", obj);
235
+ if (debug || isDevEnvironment()) {
236
+ console.error("Error updating hierarchy\nDo you have circular references in your project? <a target=\"_blank\" href=\"https://docs.needle.tools/circular-reference\"> Click here for more information.", obj)
237
+ }
238
+ else
239
+ console.error("Failed to update active state in hierarchy of \"" + obj.name + "\"", obj);
236
240
  console.warn(" ↑ this error might be caused by circular references. Please make sure you don't have files with circular references (e.g. one GLB 1 is loading GLB 2 which is then loading GLB 1 again).")
241
+
237
242
  }
238
243
  }
239
244
 
src/engine/engine_networking_auto.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getParam } from "./engine_utils";
2
- import { Component } from "../engine-components/Component";
3
- import { RoomEvents } from "./engine_networking";
2
+ import { isDevEnvironment } from "./debug";
3
+ import { IComponent } from "./engine_types";
4
4
 
5
5
  const debug = getParam("debugautosync");
6
6
 
@@ -8,7 +8,7 @@
8
8
  class ComponentsSyncerManager {
9
9
  private _syncers: { [key: string]: ComponentPropertiesSyncer } = {};
10
10
 
11
- getOrCreateSyncer(comp: Component): ComponentPropertiesSyncer | null {
11
+ getOrCreateSyncer(comp: IComponent): ComponentPropertiesSyncer | null {
12
12
  if (!comp.guid) return null;
13
13
  if (this._syncers[comp.guid]) return this._syncers[comp.guid];
14
14
  const syncer = new ComponentPropertiesSyncer(comp);
@@ -28,9 +28,9 @@
28
28
  */
29
29
  class ComponentPropertiesSyncer {
30
30
 
31
- comp: Component;
31
+ comp: IComponent;
32
32
 
33
- constructor(comp: Component) {
33
+ constructor(comp: IComponent) {
34
34
  // console.log("CREATE NEW SYNC", comp.name, comp.guid);
35
35
  this.comp = comp;
36
36
  }
@@ -115,8 +115,7 @@
115
115
  if (!this.comp) return;
116
116
  const guid = val.guid;
117
117
  if (guid && guid !== this.comp.guid) return;
118
- if (debug)
119
- console.log("RECEIVED", this.comp.name, this.comp.guid, val);
118
+ if (debug) console.log("RECEIVED", this.comp.name, this.comp.guid, val);
120
119
  try {
121
120
  this._isReceiving = true;
122
121
  for (const key in val) {
@@ -124,6 +123,7 @@
124
123
  // TODO: maybe use serializable here?!
125
124
  const value = val[key];
126
125
  this.comp[key] = value;
126
+ if(debug) console.log("SET", key, value);
127
127
  }
128
128
  }
129
129
  catch (err) {
@@ -164,13 +164,16 @@
164
164
  // }
165
165
  }
166
166
  else if (typeof newValue === "object" && typeof previousValue === "object") {
167
- // do we want to traverse / recursively check if anything changed???
168
- for (const key of Object.keys(newValue)) {
169
- if (newValue[key] !== previousValue[key]) {
170
- valueChanged = true;
171
- break;
172
- }
173
- }
167
+ valueChanged = true;
168
+ // The following code doesnt work because the object is a reference type
169
+ // To properly detect changes we would have to detect assignments for each property #
170
+ // OR keep a copy of the previous object
171
+ // for (const key of Object.keys(newValue)) {
172
+ // if (newValue[key] !== previousValue[key]) {
173
+ // valueChanged = true;
174
+ // break;
175
+ // }
176
+ // }
174
177
  }
175
178
  }
176
179
  return valueChanged;
@@ -203,14 +206,17 @@
203
206
  export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean;
204
207
 
205
208
  /**
206
- * Decorate a field to be automatically networked synced
209
+ * **Decorate a field to be automatically networked synced**
210
+ * *Primitive* values are all automatically synced (like string, boolean, number).
211
+ * For *arrays or objects* make sure to re-assign them (e.g. `this.mySyncField = this.mySyncField`) to trigger an update
212
+ *
207
213
  * @param onFieldChanged name of a callback function that will be called when the field is changed.
208
214
  * You can also pass in a function like so: syncField(myClass.prototype.myFunctionToBeCalled)
209
215
  * This function may return false to prevent notifyChanged from being called
210
216
  * (for example a networked color is sent as a number and may be converted to a color in the receiver again)
211
217
  * Parameters: (newValue, previousValue)
212
218
  */
213
- export const syncField = function(onFieldChanged?: string | FieldChangedCallbackFn) {
219
+ export const syncField = function (onFieldChanged?: string | FieldChangedCallbackFn) {
214
220
 
215
221
  return function (target: any, propertyKey: string) {
216
222
 
@@ -225,6 +231,11 @@
225
231
 
226
232
  const t = target;
227
233
  const internalAwake = t.__internalAwake;
234
+ if (typeof internalAwake !== "function") {
235
+ if (debug || isDevEnvironment())
236
+ console.error("@syncField can currently only used on Needle Engine Components, custom object of type \"" + target?.constructor?.name + "\" is not supported", target);
237
+ return;
238
+ }
228
239
  if (debug)
229
240
  console.log(propertyKey);
230
241
  const backingFieldName = Symbol(propertyKey);
@@ -234,20 +245,35 @@
234
245
  return;
235
246
  }
236
247
  this[backingFieldName] = this[propertyKey];
237
- internalAwake.call(this);
238
248
 
239
249
  syncer = syncerHandler.getOrCreateSyncer(this);
240
250
 
241
251
  const desc = Object.getOwnPropertyDescriptor(this, propertyKey);
242
252
  if (desc?.set === undefined) {
253
+ let invokingCallback = false;
243
254
  Object.defineProperty(this, propertyKey, {
244
255
  set: function (value) {
245
256
  const oldValue = this[backingFieldName];
246
257
  this[backingFieldName] = value;
247
- if (testValueChanged(value, oldValue)) {
248
- if (fn?.call(this, value, oldValue) !== false)
249
- getSyncer(this)?.notifyChanged(propertyKey, value);
258
+ // Prevent recursive calls when object is assigned in callback
259
+ if (invokingCallback) {
260
+ if (isDevEnvironment())
261
+ console.warn("Recursive call detected", propertyKey);
262
+ return;
250
263
  }
264
+ invokingCallback = true;
265
+ try {
266
+ const valueChanged = testValueChanged(value, oldValue);
267
+ if (debug) console.log("SyncField assignment", propertyKey, "changed?", valueChanged, value);
268
+ if (valueChanged) {
269
+ if (fn?.call(this, value, oldValue) !== false) {
270
+ getSyncer(this)?.notifyChanged(propertyKey, value);
271
+ }
272
+ }
273
+ }
274
+ finally {
275
+ invokingCallback = false;
276
+ }
251
277
  },
252
278
  get: function () {
253
279
  return this[backingFieldName];
@@ -258,7 +284,7 @@
258
284
  }
259
285
 
260
286
  syncer?.init(this);
261
-
287
+ internalAwake.call(this);
262
288
  }
263
289
 
264
290
  const internalDestroy = t.__internalDestroy;
@@ -281,7 +307,7 @@
281
307
 
282
308
  return function <T>(target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
283
309
  // override awake
284
- const comp = target as Component;
310
+ const comp = target as IComponent;
285
311
  let syncer: ComponentPropertiesSyncer | null;
286
312
  const internalAwake = comp.__internalAwake.bind(comp);
287
313
  comp.__internalAwake = function () {
src/engine/engine_types.ts CHANGED
@@ -123,12 +123,15 @@
123
123
  getComponentsInParent<T>(type: Constructor<T>, arr?: T[]): Array<T>;
124
124
  }
125
125
 
126
- export interface IComponent {
126
+ export interface IHasGuid {
127
+ guid: string;
128
+ }
129
+
130
+ export interface IComponent extends IHasGuid {
127
131
  get isComponent(): boolean;
128
132
 
129
-
130
133
  gameObject: IGameObject;
131
- guid: string;
134
+ // guid: string;
132
135
  enabled: boolean;
133
136
  sourceId?: SourceIdentifier;
134
137
 
src/engine-components/webxr/WebXR.ts CHANGED
@@ -604,6 +604,12 @@
604
604
 
605
605
  if (!this.sessionRoot || this.sessionRoot.destroyed || !this.sessionRoot.activeAndEnabled)
606
606
  this.sessionRoot = GameObject.findObjectOfType(WebARSessionRoot, context);
607
+ if (!this.sessionRoot) {
608
+ // TODO: adding it on the scene directly doesnt work (probably because then everything in the scene is disabled including this component). See code a bit furhter below where we add this component to a temporary object inside the scene
609
+ const obj = this.webxr.gameObject;
610
+ this.sessionRoot = GameObject.addNewComponent(obj, WebARSessionRoot);
611
+ console.warn("WebAR: No ARSessionRoot found, creating one automatically on the WebXR object");
612
+ }
607
613
 
608
614
  this.previousBackground = context.scene.background;
609
615
  this.previousEnvironment = context.scene.environment;