@gg-web-engine/core 0.0.39 → 0.0.40

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.
@@ -1,5 +1,5 @@
1
1
  import { IVisualSceneComponent, Point2, RendererOptions } from '../../../base';
2
2
  import { VisualTypeDocRepo2D } from '../../gg-2d-world';
3
3
  export interface IVisualScene2dComponent<TypeDoc extends VisualTypeDocRepo2D = VisualTypeDocRepo2D> extends IVisualSceneComponent<Point2, number, TypeDoc> {
4
- createRenderer(canvas?: HTMLCanvasElement, rendererOptions?: Partial<RendererOptions>): TypeDoc['renderer'];
4
+ createRenderer(canvas?: HTMLCanvasElement, rendererOptions?: Partial<RendererOptions & TypeDoc['rendererExtraOpts']>): TypeDoc['renderer'];
5
5
  }
@@ -1,4 +1,6 @@
1
1
  import { IRenderableEntity, Point2 } from '../../base';
2
- import { PhysicsTypeDocRepo2D, VisualTypeDocRepo2D } from '../gg-2d-world';
2
+ import { Gg2dWorld, PhysicsTypeDocRepo2D, VisualTypeDocRepo2D } from '../gg-2d-world';
3
3
  export declare abstract class IRenderable2dEntity<TypeDoc extends VisualTypeDocRepo2D = VisualTypeDocRepo2D, PTypeDoc extends PhysicsTypeDocRepo2D = PhysicsTypeDocRepo2D> extends IRenderableEntity<Point2, number, TypeDoc, PTypeDoc> {
4
+ protected _world: Gg2dWorld<TypeDoc, PTypeDoc> | null;
5
+ get world(): Gg2dWorld<TypeDoc, PTypeDoc> | null;
4
6
  }
@@ -1,3 +1,10 @@
1
1
  import { IRenderableEntity } from '../../base';
2
2
  export class IRenderable2dEntity extends IRenderableEntity {
3
+ constructor() {
4
+ super(...arguments);
5
+ this._world = null;
6
+ }
7
+ get world() {
8
+ return this._world;
9
+ }
3
10
  }
@@ -13,6 +13,7 @@ export declare type VisualTypeDocRepo2D = {
13
13
  factory: IDisplayObject2dComponentFactory;
14
14
  displayObject: IDisplayObject2dComponent;
15
15
  renderer: IRenderer2dComponent;
16
+ rendererExtraOpts: {};
16
17
  texture: unknown;
17
18
  };
18
19
  export declare type PhysicsTypeDocRepo2D = {
@@ -2,5 +2,5 @@ import { IVisualSceneComponent, Point3, Point4, RendererOptions } from '../../..
2
2
  import { VisualTypeDocRepo3D } from '../../gg-3d-world';
3
3
  export interface IVisualScene3dComponent<TypeDoc extends VisualTypeDocRepo3D = VisualTypeDocRepo3D> extends IVisualSceneComponent<Point3, Point4, TypeDoc> {
4
4
  readonly loader: TypeDoc['loader'];
5
- createRenderer(camera: TypeDoc['camera'], canvas?: HTMLCanvasElement, rendererOptions?: Partial<RendererOptions>): TypeDoc['renderer'];
5
+ createRenderer(camera: TypeDoc['camera'], canvas?: HTMLCanvasElement, rendererOptions?: Partial<RendererOptions & TypeDoc['rendererExtraOpts']>): TypeDoc['renderer'];
6
6
  }
@@ -1,4 +1,6 @@
1
1
  import { IRenderableEntity, Point3, Point4 } from '../../base';
2
- import { PhysicsTypeDocRepo3D, VisualTypeDocRepo3D } from '../gg-3d-world';
2
+ import { Gg3dWorld, PhysicsTypeDocRepo3D, VisualTypeDocRepo3D } from '../gg-3d-world';
3
3
  export declare abstract class IRenderable3dEntity<TypeDoc extends VisualTypeDocRepo3D = VisualTypeDocRepo3D, PTypeDoc extends PhysicsTypeDocRepo3D = PhysicsTypeDocRepo3D> extends IRenderableEntity<Point3, Point4, TypeDoc, PTypeDoc> {
4
+ protected _world: Gg3dWorld<TypeDoc, PTypeDoc> | null;
5
+ get world(): Gg3dWorld<TypeDoc, PTypeDoc> | null;
4
6
  }
@@ -1,3 +1,10 @@
1
1
  import { IRenderableEntity } from '../../base';
2
2
  export class IRenderable3dEntity extends IRenderableEntity {
3
+ constructor() {
4
+ super(...arguments);
5
+ this._world = null;
6
+ }
7
+ get world() {
8
+ return this._world;
9
+ }
3
10
  }
@@ -1,5 +1,5 @@
1
1
  import { BehaviorSubject, Observable, Subject } from 'rxjs';
2
- import { Graph, IEntity, Point3, Point4, TickOrder } from '../../base';
2
+ import { Graph, IEntity, PausableClock, Point3, Point4, TickOrder } from '../../base';
3
3
  import { Gg3dWorld, PhysicsTypeDocRepo3D, VisualTypeDocRepo3D } from '../gg-3d-world';
4
4
  import { Entity3d } from './entity-3d';
5
5
  import { LoadOptions, LoadResultWithProps } from '../loader';
@@ -60,10 +60,9 @@ export declare class MapGraph3dEntity<VTypeDoc extends VisualTypeDocRepo3D = Vis
60
60
  rotation: Point4;
61
61
  }
62
62
  ]>;
63
- get world(): Gg3dWorld<VTypeDoc, PTypeDoc> | null;
64
- protected _world: Gg3dWorld<VTypeDoc, PTypeDoc> | null;
65
63
  protected readonly mapGraphNodes: MapGraph[];
66
64
  protected readonly options: Gg3dMapGraphEntityOptions;
65
+ protected loadClock: PausableClock | null;
67
66
  constructor(mapGraph: MapGraph, options?: Partial<Gg3dMapGraphEntityOptions>);
68
67
  onSpawned(world: Gg3dWorld<VTypeDoc, PTypeDoc>): void;
69
68
  onRemoved(): void;
@@ -8,7 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { BehaviorSubject, NEVER, startWith, Subject, switchMap, takeUntil } from 'rxjs';
11
- import { distinctUntilChanged, map, tap, throttleTime } from 'rxjs/operators';
11
+ import { distinctUntilChanged, map, tap } from 'rxjs/operators';
12
12
  import { Graph, Qtrn, TickOrder } from '../../base';
13
13
  import { IRenderable3dEntity } from './i-renderable-3d.entity';
14
14
  export class MapGraph extends Graph {
@@ -90,7 +90,7 @@ export class MapGraph3dEntity extends IRenderable3dEntity {
90
90
  this._initialLoadComplete$ = new BehaviorSubject(false);
91
91
  this._nearestDummy$ = new BehaviorSubject(null);
92
92
  this._chunkLoaded$ = new Subject();
93
- this._world = null;
93
+ this.loadClock = null;
94
94
  this.options = Object.assign(Object.assign({}, defaultOptions), options);
95
95
  this.mapGraphNodes = mapGraph.nodes();
96
96
  }
@@ -103,15 +103,14 @@ export class MapGraph3dEntity extends IRenderable3dEntity {
103
103
  get chunkLoaded$() {
104
104
  return this._chunkLoaded$.asObservable();
105
105
  }
106
- get world() {
107
- return this._world;
108
- }
109
106
  onSpawned(world) {
110
107
  super.onSpawned(world);
108
+ this.loadClock = world.createClock(true);
109
+ this.loadClock.tickRateLimit = 1;
111
110
  this.loaderCursorEntity$
112
111
  .pipe(takeUntil(this._onRemoved$), switchMap(entity => entity
113
- ? this.tick$.pipe(startWith(null), // map will perform initial loading even if world not started yet. Handy to preload map
114
- takeUntil(entity.onRemoved$), throttleTime(1000), map(() => entity.position))
112
+ ? this.loadClock.tick$.pipe(startWith(null), // map will perform initial loading even if world not started yet. Handy to preload map
113
+ takeUntil(entity.onRemoved$), map(() => entity.position))
115
114
  : NEVER), map(pos => this.mapGraph.getNearestDummy(this.mapGraphNodes, pos)), tap(node => this._nearestDummy$.next(node)), distinctUntilChanged())
116
115
  .subscribe(currentChunk => {
117
116
  let haveToBeLoaded = new Set();
@@ -143,6 +142,10 @@ export class MapGraph3dEntity extends IRenderable3dEntity {
143
142
  }
144
143
  onRemoved() {
145
144
  super.onRemoved();
145
+ if (this.loadClock) {
146
+ this.loadClock.stop();
147
+ this.loadClock = null;
148
+ }
146
149
  this.loaderCursorEntity$.next(null);
147
150
  }
148
151
  loadChunk(node) {
@@ -51,6 +51,8 @@ export declare class RaycastVehicle3dEntity<VTypeDoc extends VisualTypeDocRepo3D
51
51
  protected readonly wheelLocalRotation: (Point4 | null)[];
52
52
  protected readonly frontWheelsIndices: number[];
53
53
  protected readonly rearWheelsIndices: number[];
54
+ get name(): string;
55
+ set name(value: string);
54
56
  getSpeed(): number;
55
57
  readonly tractionWheelRadius: number;
56
58
  private _steeringAngle;
@@ -93,11 +93,25 @@ export class RaycastVehicle3dEntity extends Entity3d {
93
93
  localRotation = { x: 0, y: 0.707107 * (flip ? 1 : -1), z: 0, w: 0.707107 };
94
94
  }
95
95
  this.wheelLocalRotation.push(localRotation);
96
- this.wheels.push(new Entity3d(entity));
96
+ const wheelEntity = new Entity3d(entity);
97
+ wheelEntity.name = this.name + '__wheel_' + (options.isFront ? 'f' : 'r') + (options.isLeft ? 'l' : 'r');
98
+ this.wheels.push(wheelEntity);
97
99
  }
98
100
  this.addChildren(...this.wheels.filter(x => !!x));
99
101
  this.vehicleComponent.entity = this;
100
102
  }
103
+ get name() {
104
+ return super.name;
105
+ }
106
+ set name(value) {
107
+ const oldName = super.name;
108
+ super.name = value;
109
+ for (const w of this.wheels || []) {
110
+ if (w) {
111
+ w.name = w.name.replace(oldName, value);
112
+ }
113
+ }
114
+ }
101
115
  // m/s
102
116
  getSpeed() {
103
117
  return this.vehicleComponent.wheelSpeed;
@@ -18,6 +18,7 @@ export declare type VisualTypeDocRepo3D = {
18
18
  loader: IDisplayObject3dComponentLoader;
19
19
  displayObject: IDisplayObject3dComponent;
20
20
  renderer: IRenderer3dComponent;
21
+ rendererExtraOpts: {};
21
22
  camera: ICameraComponent;
22
23
  texture: unknown;
23
24
  };
@@ -6,6 +6,7 @@ import { IClock } from './i-clock';
6
6
  export declare class PausableClock implements IClock {
7
7
  protected readonly parentClock: IClock;
8
8
  private tickSub;
9
+ private readonly _internalTick$;
9
10
  private readonly _tick$;
10
11
  /**
11
12
  * Observable stream of ticks, emitting an array containing the current time and the tick delta.
@@ -35,12 +36,17 @@ export declare class PausableClock implements IClock {
35
36
  * Gets the elapsed time since the clock started.
36
37
  */
37
38
  get elapsedTime(): number;
39
+ /**
40
+ * Tick rate limiter. If set to 0 - tick rate is unlimited, 15 means "allow at most 15 ticks per second"
41
+ */
42
+ tickRateLimit: number;
38
43
  private startedAt;
39
44
  private oldRelativeTime;
40
45
  private pausedAt;
41
46
  private lastStopElapsed;
42
47
  private _timeScale;
43
48
  private pausedByTimescale;
49
+ private lastFiredTickElapsed;
44
50
  /**
45
51
  * Constructs a new PausableClock instance.
46
52
  * @param autoStart Indicates whether the clock should start automatically upon creation.
@@ -1,4 +1,4 @@
1
- import { Subject } from 'rxjs';
1
+ import { filter, Subject } from 'rxjs';
2
2
  import { map, tap } from 'rxjs/operators';
3
3
  import { GgGlobalClock } from './global-clock';
4
4
  /**
@@ -13,7 +13,12 @@ export class PausableClock {
13
13
  constructor(autoStart = false, parentClock = GgGlobalClock.instance) {
14
14
  this.parentClock = parentClock;
15
15
  this.tickSub = null;
16
+ this._internalTick$ = new Subject();
16
17
  this._tick$ = new Subject();
18
+ /**
19
+ * Tick rate limiter. If set to 0 - tick rate is unlimited, 15 means "allow at most 15 ticks per second"
20
+ */
21
+ this.tickRateLimit = 0;
17
22
  // State variables
18
23
  this.startedAt = -1;
19
24
  this.oldRelativeTime = 0; // "elapsed", emitted on last tick
@@ -21,15 +26,21 @@ export class PausableClock {
21
26
  this.lastStopElapsed = 0;
22
27
  this._timeScale = 1;
23
28
  this.pausedByTimescale = false;
29
+ this.lastFiredTickElapsed = 0;
24
30
  if (autoStart) {
25
31
  this.start();
26
32
  }
33
+ this._internalTick$
34
+ .pipe(map(([_, newTime]) => [newTime, newTime - this.lastFiredTickElapsed]), filter(([elapsed]) => !this.tickRateLimit ||
35
+ Math.floor((this.lastFiredTickElapsed * this.tickRateLimit) / 1000) <
36
+ Math.floor((elapsed * this.tickRateLimit) / 1000)), tap(([elapsed]) => (this.lastFiredTickElapsed = elapsed)))
37
+ .subscribe(this._tick$);
27
38
  }
28
39
  /**
29
40
  * Observable stream of ticks, emitting an array containing the current time and the tick delta.
30
41
  */
31
42
  get tick$() {
32
- return this._tick$.pipe(map(([oldTime, newTime]) => [newTime, newTime - oldTime]));
43
+ return this._tick$.asObservable();
33
44
  }
34
45
  /**
35
46
  * Checks if the clock is currently running.
@@ -145,7 +156,7 @@ export class PausableClock {
145
156
  }
146
157
  this.tickSub = this.parentClock.tick$
147
158
  .pipe(map(([_, d]) => [this.oldRelativeTime, this.oldRelativeTime + d * this.timeScale]), tap(([_, cur]) => (this.oldRelativeTime = cur)))
148
- .subscribe(this._tick$);
159
+ .subscribe(this._internalTick$);
149
160
  }
150
161
  /**
151
162
  * Stops listening for ticks from the parent clock.
@@ -25,12 +25,12 @@ export declare abstract class IRendererComponent<D, R, VTypeDoc extends VisualTy
25
25
  readonly canvas?: HTMLCanvasElement | undefined;
26
26
  entity: IEntity | null;
27
27
  /** Specifies the options for the renderer. */
28
- readonly rendererOptions: RendererOptions;
28
+ readonly rendererOptions: RendererOptions & Partial<VTypeDoc['rendererExtraOpts']>;
29
29
  /** get flag whether renderer shows physics debugger view */
30
30
  abstract get physicsDebugViewActive(): boolean;
31
31
  /** turns on/off physics debugger view for this renderer */
32
32
  abstract set physicsDebugViewActive(value: boolean);
33
- protected constructor(scene: IVisualSceneComponent<D, R, VTypeDoc>, canvas?: HTMLCanvasElement | undefined, options?: Partial<RendererOptions>);
33
+ protected constructor(scene: IVisualSceneComponent<D, R, VTypeDoc>, canvas?: HTMLCanvasElement | undefined, options?: Partial<RendererOptions & VTypeDoc['rendererExtraOpts']>);
34
34
  /**
35
35
  * Renders the scene.
36
36
  */
@@ -1,3 +1,3 @@
1
1
  import { GgWorld } from '../../gg-world';
2
2
  import { Observable } from 'rxjs';
3
- export declare function createInlineTickController(world: GgWorld<any, any>, tickOrder?: number): Observable<[number, number]>;
3
+ export declare function createInlineTickController(world: GgWorld<any, any>, tickOrder?: number, name?: string): Observable<[number, number]>;
@@ -6,8 +6,11 @@ class InlineTickController extends IEntity {
6
6
  this.tickOrder = tickOrder;
7
7
  }
8
8
  }
9
- export function createInlineTickController(world, tickOrder = TickOrder.CONTROLLERS) {
9
+ export function createInlineTickController(world, tickOrder = TickOrder.CONTROLLERS, name) {
10
10
  const controller = new InlineTickController(tickOrder);
11
+ if (name) {
12
+ controller.name = name;
13
+ }
11
14
  world.addEntity(controller);
12
15
  return controller.tick$.pipe(finalize(() => {
13
16
  world.removeEntity(controller, true);
@@ -1,8 +1,10 @@
1
1
  import { IDisplayObjectComponent, IEntity, IPhysicsWorldComponent, IPositionable, IRenderableEntity, IRendererComponent, IRigidBodyComponent, ITriggerComponent, IVisualSceneComponent, KeyboardInput, PausableClock } from '../base';
2
+ import { Subject } from 'rxjs';
2
3
  export declare type VisualTypeDocRepo<D, R> = {
3
4
  factory: unknown;
4
5
  displayObject: IDisplayObjectComponent<D, R>;
5
6
  renderer: IRendererComponent<D, R>;
7
+ rendererExtraOpts: {};
6
8
  };
7
9
  export declare type PhysicsTypeDocRepo<D, R> = {
8
10
  factory: unknown;
@@ -12,14 +14,17 @@ export declare type PhysicsTypeDocRepo<D, R> = {
12
14
  export declare abstract class GgWorld<D, R, VTypeDoc extends VisualTypeDocRepo<D, R> = VisualTypeDocRepo<D, R>, PTypeDoc extends PhysicsTypeDocRepo<D, R> = PhysicsTypeDocRepo<D, R>, VS extends IVisualSceneComponent<D, R, VTypeDoc> = IVisualSceneComponent<D, R, VTypeDoc>, PW extends IPhysicsWorldComponent<D, R, PTypeDoc> = IPhysicsWorldComponent<D, R, PTypeDoc>> {
13
15
  readonly visualScene: VS;
14
16
  readonly physicsWorld: PW;
17
+ private static default_name_counter;
18
+ private static _documentWorlds;
19
+ static get documentWorlds(): GgWorld<any, any>[];
15
20
  readonly worldClock: PausableClock;
16
21
  readonly keyboardInput: KeyboardInput;
17
- private static default_name_counter;
18
22
  name: string;
19
23
  readonly children: IEntity[];
20
24
  protected readonly tickListeners: IEntity[];
21
- private static _documentWorlds;
22
- static get documentWorlds(): GgWorld<any, any>[];
25
+ readonly tickStarted$: Subject<void>;
26
+ readonly tickForwardTo$: Subject<IEntity | 'PHYSICS_WORLD'>;
27
+ readonly tickForwardedTo$: Subject<IEntity | 'PHYSICS_WORLD'>;
23
28
  protected constructor(visualScene: VS, physicsWorld: PW);
24
29
  init(): Promise<void>;
25
30
  start(): void;
@@ -8,6 +8,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { GgGlobalClock, IRendererEntity, KeyboardInput, TickOrder, } from '../base';
11
+ import { lastValueFrom, Subject, take } from 'rxjs';
12
+ import { PerformanceMeterEntity } from '../dev';
11
13
  export class GgWorld {
12
14
  constructor(visualScene, physicsWorld) {
13
15
  this.visualScene = visualScene;
@@ -18,17 +20,13 @@ export class GgWorld {
18
20
  this.children = [];
19
21
  // the same as children, but sorted by tick order
20
22
  this.tickListeners = [];
23
+ // events
24
+ this.tickStarted$ = new Subject();
25
+ this.tickForwardTo$ = new Subject();
26
+ this.tickForwardedTo$ = new Subject();
21
27
  GgWorld._documentWorlds.push(this);
22
28
  this.keyboardInput.start();
23
29
  if (window.ggstatic) {
24
- window.ggstatic.registerConsoleCommand(this, 'show_debugger', (...args) => __awaiter(this, void 0, void 0, function* () {
25
- window.ggstatic.showDebugControls = ['1', 'true', '+'].includes(args[0]);
26
- return '' + window.ggstatic.showDebugControls;
27
- }), 'args: [0 or 1]; turn on/off debug panel. Default value is 0');
28
- window.ggstatic.registerConsoleCommand(this, 'show_stats', (...args) => __awaiter(this, void 0, void 0, function* () {
29
- window.ggstatic.showStats = ['1', 'true', '+'].includes(args[0]);
30
- return '' + window.ggstatic.showStats;
31
- }), 'args: [0 or 1]; turn on/off stats. Default value is 0');
32
30
  window.ggstatic.registerConsoleCommand(this, 'ph_timescale', (...args) => __awaiter(this, void 0, void 0, function* () {
33
31
  this.worldClock.timeScale = +args[0];
34
32
  return JSON.stringify(this.worldClock.timeScale);
@@ -56,6 +54,40 @@ export class GgWorld {
56
54
  return 'false';
57
55
  }), 'args: [0 or 1] or [0 or 1, string]; turn on/off physics debug view. Second argument expects renderer ' +
58
56
  'name, if not provided first renderer will be picked. Look up for renderer names using command "ls_renderers"');
57
+ window.ggstatic.registerConsoleCommand(this, 'performance_report', (...args) => __awaiter(this, void 0, void 0, function* () {
58
+ let mode = 'avg';
59
+ let samples = 20;
60
+ for (let i = 0; i < 2; i++) {
61
+ if (['avg', 'peak'].includes(args[i])) {
62
+ mode = args[i];
63
+ }
64
+ else if (!isNaN(+args[i])) {
65
+ samples = +args[i];
66
+ }
67
+ }
68
+ const meter = new PerformanceMeterEntity(samples, 250);
69
+ this.addEntity(meter);
70
+ yield lastValueFrom(this.worldClock.tick$.pipe(take(samples)));
71
+ const report = mode === 'avg' ? meter.avgReport : meter.peakReport;
72
+ this.removeEntity(meter);
73
+ const renderItems = report.entries.map(([name, value]) => `<span style='color:lightgray;'>${name}:</span>` +
74
+ new Array(Math.max(0, 26 - name.length)).join('&nbsp;') +
75
+ `${value.toFixed(2)} ms` +
76
+ (mode === 'avg' ? ` (${((value * 100) / report.totalTime).toFixed(2)}%)` : ''));
77
+ let totalColor = 'lightgreen';
78
+ if (report.totalTime > 12) {
79
+ totalColor = report.totalTime < 16 ? 'yellow' : 'red';
80
+ }
81
+ const title = `${mode === 'avg' ? 'Average' : 'Peak'} Frame time`;
82
+ renderItems.unshift(title +
83
+ ':' +
84
+ new Array(Math.max(0, 26 - title.length)).join('&nbsp;') +
85
+ `<span style='color:${totalColor};'>${report.totalTime.toFixed(2)} ms</span>`);
86
+ renderItems.unshift(`Performance report (${samples} samples)`);
87
+ return renderItems.join('\n');
88
+ }), 'args: [int, avg|peak]; [avg|peak, int]; [avg|peak]; [int] or []; measure how much time was spent per ' +
89
+ 'entity in world. Arguments are samples amount (20 by default) and "peak" or "avg" choice, both arguments are ' +
90
+ 'optional. "avg" report sorts entities by average time consumed, "peak" records highest value for each entity');
59
91
  }
60
92
  }
61
93
  static get documentWorlds() {
@@ -64,24 +96,30 @@ export class GgWorld {
64
96
  init() {
65
97
  return __awaiter(this, void 0, void 0, function* () {
66
98
  yield Promise.all([this.physicsWorld.init(), this.visualScene.init()]);
99
+ const forwardTick = (listener, elapsed, delta) => {
100
+ if (listener.active) {
101
+ this.tickForwardTo$.next(listener);
102
+ listener.tick$.next([elapsed, delta]);
103
+ this.tickForwardedTo$.next(listener);
104
+ }
105
+ };
67
106
  this.worldClock.tick$.subscribe(([elapsed, delta]) => {
107
+ this.tickStarted$.next();
68
108
  let i = 0;
69
109
  // emit tick to all entities with tick order < GGTickOrder.PHYSICS_SIMULATION
70
110
  for (i; i < this.tickListeners.length; i++) {
71
111
  if (this.tickListeners[i].tickOrder >= TickOrder.PHYSICS_SIMULATION) {
72
112
  break;
73
113
  }
74
- if (this.tickListeners[i].active) {
75
- this.tickListeners[i].tick$.next([elapsed, delta]);
76
- }
114
+ forwardTick(this.tickListeners[i], elapsed, delta);
77
115
  }
78
116
  // run physics simulation
117
+ this.tickForwardTo$.next('PHYSICS_WORLD');
79
118
  this.physicsWorld.simulate(delta);
119
+ this.tickForwardedTo$.next('PHYSICS_WORLD');
80
120
  // emit tick to all remained entities
81
121
  for (i; i < this.tickListeners.length; i++) {
82
- if (this.tickListeners[i].active) {
83
- this.tickListeners[i].tick$.next([elapsed, delta]);
84
- }
122
+ forwardTick(this.tickListeners[i], elapsed, delta);
85
123
  }
86
124
  });
87
125
  });
@@ -113,6 +151,9 @@ export class GgWorld {
113
151
  }
114
152
  this.worldClock.stop();
115
153
  this.keyboardInput.stop();
154
+ this.tickStarted$.complete();
155
+ this.tickForwardTo$.complete();
156
+ this.tickForwardedTo$.complete();
116
157
  for (let i = 0; i < this.children.length; i++) {
117
158
  this.children[i].onRemoved();
118
159
  this.children[i].dispose();
@@ -162,11 +162,16 @@ export class MouseInput extends IInput {
162
162
  .pipe(takeUntil(this.stopped$))
163
163
  .subscribe((event) => {
164
164
  if (pointers.length === 0) {
165
- if (this.options.canvas) {
166
- this.options.canvas.setPointerCapture(event.pointerId);
165
+ try {
166
+ if (this.options.canvas) {
167
+ this.options.canvas.setPointerCapture(event.pointerId);
168
+ }
169
+ this._element.addEventListener('pointerup', onPointerUp);
170
+ this._element.addEventListener('pointercancel', onPointerUp);
171
+ }
172
+ catch (err) {
173
+ console.error(err);
167
174
  }
168
- this._element.addEventListener('pointerup', onPointerUp);
169
- this._element.addEventListener('pointercancel', onPointerUp);
170
175
  }
171
176
  pointers.push(event);
172
177
  if (event.pointerType === 'touch') {
@@ -10,6 +10,11 @@ export declare class GgDebuggerUI {
10
10
  get showDebugControls(): boolean;
11
11
  setShowDebugControls(selectedWorld: GgWorld<any, any>, value: boolean): void;
12
12
  private snapshot;
13
+ perfStatsMode: 'AVG' | 'PEAK';
14
+ private performanceStatsSnapshot;
13
15
  private makeSnapshot;
16
+ private makePerformanceStatsSnapshot;
17
+ css: string;
14
18
  private renderControls;
19
+ private renderPerformanceStats;
15
20
  }
@@ -2,9 +2,12 @@ import { createInlineTickController, IRendererEntity } from '../base';
2
2
  import { animationFrameScheduler, fromEvent, of, Subject, takeUntil } from 'rxjs';
3
3
  import Stats from 'stats.js';
4
4
  import { repeat } from 'rxjs/operators';
5
+ import { PerformanceMeterEntity } from './performance-meter.entity';
5
6
  const snapshotEqual = (a, b) => {
6
7
  if (a.timeScale !== b.timeScale)
7
8
  return false;
9
+ if (a.performanceStatsEnabled !== b.performanceStatsEnabled)
10
+ return false;
8
11
  if (a.renderers.length !== b.renderers.length)
9
12
  return false;
10
13
  for (let i = 0; i < a.renderers.length; i++) {
@@ -17,6 +20,23 @@ const snapshotEqual = (a, b) => {
17
20
  }
18
21
  return true;
19
22
  };
23
+ const performanceStatsSnapshotEqual = (a, b) => {
24
+ if (!!a !== !!b)
25
+ return false;
26
+ if (a && b) {
27
+ if (a.totalTime !== b.totalTime)
28
+ return false;
29
+ if (a.entries.length !== b.entries.length)
30
+ return false;
31
+ for (let i = 0; i < a.entries.length; i++) {
32
+ if (a.entries[i][0] !== b.entries[i][0])
33
+ return false;
34
+ if (a.entries[i][1] !== b.entries[i][1])
35
+ return false;
36
+ }
37
+ }
38
+ return true;
39
+ };
20
40
  export class GgDebuggerUI {
21
41
  constructor() {
22
42
  this.ui = {
@@ -30,7 +50,11 @@ export class GgDebuggerUI {
30
50
  this.snapshot = {
31
51
  timeScale: 1,
32
52
  renderers: [],
53
+ performanceStatsEnabled: false,
33
54
  };
55
+ this.perfStatsMode = 'AVG';
56
+ this.performanceStatsSnapshot = null;
57
+ this.css = "style='display:flex;align-items:center;margin:0.25rem;'";
34
58
  }
35
59
  get showStats() {
36
60
  return !!this.ui.stats;
@@ -52,12 +76,12 @@ export class GgDebuggerUI {
52
76
  stats.dom.style.right = '0';
53
77
  stats.showPanel(0); // 0: fps, 1: ms, 2: mb
54
78
  document.body.appendChild(stats.dom);
55
- createInlineTickController(selectedWorld, -1)
79
+ createInlineTickController(selectedWorld, -1, 'fps_meter_init')
56
80
  .pipe(takeUntil(this.statsRemoved$))
57
81
  .subscribe(() => {
58
82
  stats === null || stats === void 0 ? void 0 : stats.begin();
59
83
  });
60
- createInlineTickController(selectedWorld, 10000)
84
+ createInlineTickController(selectedWorld, 10000, 'fps_meter')
61
85
  .pipe(takeUntil(this.statsRemoved$))
62
86
  .subscribe(() => {
63
87
  stats === null || stats === void 0 ? void 0 : stats.end();
@@ -90,7 +114,9 @@ export class GgDebuggerUI {
90
114
  debugControlsContainer.style.cssText =
91
115
  'position:fixed;top:48px;right:0;opacity:0.9;z-index:9999;background-color:#333;color:white;display:flex;flex-direction:column';
92
116
  this.snapshot = this.makeSnapshot();
117
+ this.performanceStatsSnapshot = this.makePerformanceStatsSnapshot();
93
118
  this.renderControls(debugControlsContainer);
119
+ this.renderPerformanceStats();
94
120
  of(undefined, animationFrameScheduler)
95
121
  .pipe(repeat(), takeUntil(this.debugControlsRemoved$))
96
122
  .subscribe(() => {
@@ -99,6 +125,11 @@ export class GgDebuggerUI {
99
125
  this.snapshot = newSnapshot;
100
126
  this.renderControls(debugControlsContainer);
101
127
  }
128
+ const newPerformanceStats = this.makePerformanceStatsSnapshot();
129
+ if (!performanceStatsSnapshotEqual(this.performanceStatsSnapshot, newPerformanceStats)) {
130
+ this.performanceStatsSnapshot = newPerformanceStats;
131
+ this.renderPerformanceStats();
132
+ }
102
133
  });
103
134
  }
104
135
  else {
@@ -108,33 +139,74 @@ export class GgDebuggerUI {
108
139
  }
109
140
  }
110
141
  makeSnapshot() {
142
+ const renderers = [];
143
+ let performanceMeter = null;
144
+ for (const e of this.currentWorld.children) {
145
+ if (e instanceof IRendererEntity) {
146
+ renderers.push(e);
147
+ }
148
+ else if (e instanceof PerformanceMeterEntity) {
149
+ performanceMeter = e;
150
+ }
151
+ }
111
152
  return {
112
153
  timeScale: this.currentWorld.worldClock.timeScale,
113
- renderers: this.currentWorld.children
114
- .filter(x => x instanceof IRendererEntity)
115
- .map(r => ({
154
+ renderers: renderers.map(r => ({
116
155
  name: r.name,
117
156
  entity: r,
118
157
  physicsDebugViewActive: r.physicsDebugViewActive,
119
158
  })),
159
+ performanceStatsEnabled: !!performanceMeter,
120
160
  };
121
161
  }
162
+ makePerformanceStatsSnapshot() {
163
+ if (!this.snapshot.performanceStatsEnabled)
164
+ return null;
165
+ let performanceMeter = this.currentWorld.children.find(e => e instanceof PerformanceMeterEntity);
166
+ if (performanceMeter) {
167
+ if (this.perfStatsMode === 'AVG') {
168
+ return performanceMeter.avgReport;
169
+ }
170
+ else if (this.perfStatsMode === 'PEAK') {
171
+ return performanceMeter.peakReport;
172
+ }
173
+ }
174
+ return null;
175
+ }
122
176
  renderControls(debugControlsContainer) {
123
- const debugLabelCss = "style='display:flex;align-items:center;margin:0.25rem;'";
124
177
  let html = '';
125
178
  for (const { entity, physicsDebugViewActive } of this.snapshot.renderers) {
126
179
  html += `
127
- <div ${debugLabelCss}>
180
+ <div ${this.css}>
128
181
  <input type='checkbox' name='checkbox' id='physics_debugger_checkbox_id_${entity.name}' value='1'${physicsDebugViewActive ? ' checked' : ''}>
129
182
  <label for='physics_debugger_checkbox_id_${entity.name}' style='user-select: none;'>Physics debugger${this.snapshot.renderers.length > 1 ? ' (' + entity.name + ')' : ''}</label>
130
183
  </div>`;
131
184
  }
132
185
  html += `
133
- <div ${debugLabelCss}>
186
+ <div ${this.css}>
134
187
  <input id='time_scale_slider' type='range' min='0' max='5' step='0.01' style='flex-grow:1' value='${this.snapshot.timeScale}'/>
135
188
  <label for='time_scale_slider' style='user-select: none;'>Time scale</label>
136
189
  </div>`;
190
+ html +=
191
+ `
192
+ <div ${this.css}>
193
+ <input type='checkbox' name='checkbox' id='perf_stats_checkbox_id' value='1'${this.snapshot.performanceStatsEnabled ? ' checked' : ''}>
194
+ <label for='perf_stats_checkbox_id' style='user-select: none;'>Entities performance distribution</label>
195
+ </div>
196
+ ` +
197
+ (this.snapshot.performanceStatsEnabled
198
+ ? `<div ${this.css}>
199
+ <label for='per_mode'>Mode:</label>
200
+ <select id='per_mode'>
201
+ <option value='AVG' ${this.perfStatsMode === 'AVG' ? 'selected' : ''}>Average</option>
202
+ <option value='PEAK' ${this.perfStatsMode === 'PEAK' ? 'selected' : ''}>Peak</option>
203
+ </select>
204
+ </div>`
205
+ : '') +
206
+ `
207
+ <div style='display: contents' id='perf_stats_container'></div>`;
137
208
  debugControlsContainer.innerHTML = html;
209
+ debugControlsContainer.style.minWidth = '25rem';
138
210
  this.viewUpdated$.next();
139
211
  for (const { entity } of this.snapshot.renderers) {
140
212
  fromEvent(document.getElementById('physics_debugger_checkbox_id_' + entity.name), 'change')
@@ -158,5 +230,54 @@ export class GgDebuggerUI {
158
230
  console.error(err);
159
231
  }
160
232
  });
233
+ fromEvent(document.getElementById('perf_stats_checkbox_id'), 'change')
234
+ .pipe(takeUntil(this.debugControlsRemoved$), takeUntil(this.viewUpdated$))
235
+ .subscribe(e => {
236
+ const entity = this.currentWorld.children.find(x => x instanceof PerformanceMeterEntity);
237
+ if (e.target.checked == !entity) {
238
+ if (entity) {
239
+ this.currentWorld.removeEntity(entity);
240
+ }
241
+ else {
242
+ this.currentWorld.addEntity(new PerformanceMeterEntity());
243
+ }
244
+ }
245
+ });
246
+ if (this.snapshot.performanceStatsEnabled) {
247
+ fromEvent(document.getElementById('per_mode'), 'change')
248
+ .pipe(takeUntil(this.debugControlsRemoved$), takeUntil(this.viewUpdated$))
249
+ .subscribe(e => {
250
+ this.perfStatsMode = e.target.value;
251
+ });
252
+ }
253
+ }
254
+ renderPerformanceStats() {
255
+ const pCss = "style='display:flex;align-items:center;margin:0.25rem;max-width:25rem'";
256
+ const em = document.getElementById('perf_stats_container');
257
+ if (!em)
258
+ return;
259
+ let html = '';
260
+ if (this.performanceStatsSnapshot) {
261
+ const total = this.performanceStatsSnapshot.totalTime;
262
+ const labelCss = `style='user-select:none;width:3.5rem;text-align:left;flex-shrink:0;'`;
263
+ const progressCss = `style='width:11.5rem;margin:4px;flex-shrink:0;'`;
264
+ const spanCss = `style='user-select:none;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;width:calc(9.5rem - 8px);'`;
265
+ html += `
266
+ <div ${pCss}>
267
+ <label for='stat_progress' ${labelCss}>${total.toFixed(2)}ms</label>
268
+ <progress id="stat_progress" ${progressCss} max="16" value="${total}"></progress>
269
+ <span ${spanCss}>Total frame time</span>
270
+ </div>`;
271
+ for (const [i, [name, value]] of this.performanceStatsSnapshot.entries.entries()) {
272
+ const strVal = this.perfStatsMode == 'AVG' ? ((value * 100) / total).toFixed(2) + '%' : value.toFixed(2) + 'ms';
273
+ html += `
274
+ <div ${pCss}>
275
+ <label for='stat_progress_${i}' ${labelCss}>${strVal}</label>
276
+ <progress id='stat_progress_${i}' ${progressCss} max='${total}' value='${value}'></progress>
277
+ <span ${spanCss}>${name}</span>
278
+ </div>`;
279
+ }
280
+ }
281
+ em.innerHTML = html;
161
282
  }
162
283
  }
@@ -30,13 +30,13 @@ export class GgStatic {
30
30
  this.consoleCommands = new Map();
31
31
  this.registerConsoleCommand(null, 'ls_commands', () => __awaiter(this, void 0, void 0, function* () {
32
32
  return this.availableCommands
33
- .map(([key, value]) => `<span style="color:yellow">${key}</span>${value.doc ? '\t<span style="color:#aaa">// ' + value.doc + '</span>' : ''}`)
33
+ .map(([key, value]) => `<span style='color:yellow'>${key}</span>${value.doc ? '\t<span style="color:#aaa">// ' + value.doc + '</span>' : ''}`)
34
34
  .sort()
35
35
  .join('\n\n');
36
36
  }), 'no args; print all available commands');
37
37
  this.registerConsoleCommand(null, 'ls_worlds', () => __awaiter(this, void 0, void 0, function* () {
38
38
  return GgWorld.documentWorlds
39
- .map(w => (w === this.selectedWorld ? `<span style="color:green;">* ${w.name}</span>` : ` ${w.name}`))
39
+ .map(w => (w === this.selectedWorld ? `<span style='color:lightgreen;'>* ${w.name}</span>` : ` ${w.name}`))
40
40
  .join('\n');
41
41
  }), 'no args; print all available worlds');
42
42
  this.registerConsoleCommand(null, 'select_world', (...args) => __awaiter(this, void 0, void 0, function* () {
@@ -49,6 +49,14 @@ export class GgStatic {
49
49
  }
50
50
  return ((_a = this.selectedWorld) === null || _a === void 0 ? void 0 : _a.name) || 'null';
51
51
  }), 'args: [string]; select world by name');
52
+ this.registerConsoleCommand(null, 'show_debugger', (...args) => __awaiter(this, void 0, void 0, function* () {
53
+ this.showDebugControls = ['1', 'true', '+'].includes(args[0]);
54
+ return '' + this.showDebugControls;
55
+ }), 'args: [0 or 1]; turn on/off debug panel. Default value is 0');
56
+ this.registerConsoleCommand(null, 'show_stats', (...args) => __awaiter(this, void 0, void 0, function* () {
57
+ this.showStats = ['1', 'true', '+'].includes(args[0]);
58
+ return '' + this.showStats;
59
+ }), 'args: [0 or 1]; turn on/off stats. Default value is 0');
52
60
  }
53
61
  static get instance() {
54
62
  if (!window.ggstatic) {
@@ -126,14 +134,14 @@ export class GgStatic {
126
134
  if (!action) {
127
135
  action = (this.consoleCommands.get(this.selectedWorld) || {})[command];
128
136
  if (!action) {
129
- return `<span style="color:red">Unrecognized command: ${command}</span>`;
137
+ return `<span style='color:red'>Unrecognized command: ${command}</span>`;
130
138
  }
131
139
  }
132
140
  try {
133
141
  return yield action.handler(...args);
134
142
  }
135
143
  catch (err) {
136
- return `<span style="color:red">${err}</span>`;
144
+ return `<span style='color:red'>${err}</span>`;
137
145
  }
138
146
  });
139
147
  }
@@ -1,3 +1,4 @@
1
1
  export * from './gg-console.ui';
2
2
  export * from './gg-debugger.ui';
3
3
  export * from './gg-static';
4
+ export * from './performance-meter.entity';
package/dist/dev/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './gg-console.ui';
2
2
  export * from './gg-debugger.ui';
3
3
  export * from './gg-static';
4
+ export * from './performance-meter.entity';
@@ -0,0 +1,21 @@
1
+ import { GgWorld, IEntity, TickOrder } from '../base';
2
+ export declare class PerformanceMeterEntity extends IEntity {
3
+ private readonly maxSamples;
4
+ private readonly maxRows;
5
+ readonly tickOrder: TickOrder | number;
6
+ private sampleIndex;
7
+ private collectedData;
8
+ private _avgReport;
9
+ get avgReport(): {
10
+ totalTime: number;
11
+ entries: [string, number][];
12
+ };
13
+ private _peakReport;
14
+ get peakReport(): {
15
+ totalTime: number;
16
+ entries: [string, number][];
17
+ };
18
+ constructor(maxSamples?: number, maxRows?: number);
19
+ onSpawned(world: GgWorld<unknown, unknown>): void;
20
+ onRemoved(): void;
21
+ }
@@ -0,0 +1,120 @@
1
+ import { IEntity } from '../base';
2
+ import { takeUntil } from 'rxjs';
3
+ export class PerformanceMeterEntity extends IEntity {
4
+ constructor(maxSamples = 60, maxRows = 15) {
5
+ super();
6
+ this.maxSamples = maxSamples;
7
+ this.maxRows = maxRows;
8
+ this.tickOrder = Number.MAX_SAFE_INTEGER;
9
+ this.sampleIndex = 0;
10
+ this.collectedData = new Map();
11
+ this._avgReport = { totalTime: 0, entries: [] };
12
+ this._peakReport = { totalTime: 0, entries: [] };
13
+ }
14
+ get avgReport() {
15
+ return this._avgReport;
16
+ }
17
+ get peakReport() {
18
+ return this._peakReport;
19
+ }
20
+ onSpawned(world) {
21
+ super.onSpawned(world);
22
+ this.sampleIndex = 0;
23
+ const now = () => (typeof performance === 'undefined' ? Date : performance).now();
24
+ this.world.tickForwardTo$.pipe(takeUntil(this.onRemoved$)).subscribe(e => {
25
+ if (e === this)
26
+ return;
27
+ if (!this.collectedData.has(e)) {
28
+ this.collectedData.set(e, [[this.sampleIndex, now(), 0]]);
29
+ }
30
+ else {
31
+ this.collectedData.get(e).push([this.sampleIndex, now(), 0]);
32
+ }
33
+ });
34
+ this.world.tickForwardedTo$.pipe(takeUntil(this.onRemoved$)).subscribe(e => {
35
+ if (e === this)
36
+ return;
37
+ const report = this.collectedData.get(e);
38
+ report[report.length - 1][2] = now();
39
+ });
40
+ this.tick$.pipe(takeUntil(this.onRemoved$)).subscribe(() => {
41
+ this.sampleIndex++;
42
+ // remove oldest samples
43
+ const entries = Array.from(this.collectedData.entries());
44
+ for (const [k, v] of entries) {
45
+ while (v.length > 0 && v[0][0] < this.sampleIndex - this.maxSamples) {
46
+ v.shift();
47
+ }
48
+ if (v.length === 0) {
49
+ this.collectedData.delete(k);
50
+ }
51
+ }
52
+ // helper functions
53
+ const label = (e) => {
54
+ if (e instanceof IEntity) {
55
+ if (e.name === '') {
56
+ return e.constructor.name;
57
+ }
58
+ else {
59
+ return e.name;
60
+ }
61
+ }
62
+ else if (e === 'PHYSICS_WORLD') {
63
+ return 'Physics simulation';
64
+ }
65
+ else {
66
+ return e;
67
+ }
68
+ };
69
+ const truncateReport = (report) => {
70
+ let restSum = 0;
71
+ let restAmount = 0;
72
+ for (restAmount; restAmount < report.length - 2; restAmount++) {
73
+ if (this.maxRows > report.length - restAmount + 1) {
74
+ restAmount--;
75
+ break;
76
+ }
77
+ restSum += report[report.length - restAmount - 1][1];
78
+ }
79
+ if (restAmount > 1) {
80
+ report = report.slice(0, report.length - restAmount);
81
+ report.push([`Rest (${restAmount})`, restSum]);
82
+ }
83
+ return report;
84
+ };
85
+ // build avg report
86
+ let totalTimeAvg = 0;
87
+ let avgReport = Array.from(this.collectedData.entries())
88
+ .map(([e, samples]) => {
89
+ const timeSpentAvg = samples.reduce((prev, [_, start, finish]) => prev + finish - start, 0) / this.maxSamples;
90
+ totalTimeAvg += timeSpentAvg;
91
+ return [label(e), timeSpentAvg];
92
+ })
93
+ .sort((a, b) => b[1] - a[1]);
94
+ avgReport = truncateReport(avgReport);
95
+ this._avgReport = { totalTime: totalTimeAvg, entries: avgReport };
96
+ // build peak report
97
+ let totalTimePeaks = new Map();
98
+ let peakReport = Array.from(this.collectedData.entries())
99
+ .map(([e, samples]) => {
100
+ let peakTime = 0;
101
+ for (const [sample, start, finish] of samples) {
102
+ const time = finish - start;
103
+ peakTime = Math.max(peakTime, time);
104
+ totalTimePeaks.set(sample, time + (totalTimePeaks.get(sample) || 0));
105
+ }
106
+ return [label(e), peakTime];
107
+ })
108
+ .sort((a, b) => b[1] - a[1]);
109
+ peakReport = truncateReport(peakReport);
110
+ let totalPeak = Array.from(totalTimePeaks.values()).reduce((prev, cur) => Math.max(prev, cur), 0);
111
+ this._peakReport = { totalTime: totalPeak, entries: peakReport };
112
+ });
113
+ }
114
+ onRemoved() {
115
+ super.onRemoved();
116
+ this.collectedData = new Map();
117
+ this._avgReport = { totalTime: 0, entries: [] };
118
+ this._peakReport = { totalTime: 0, entries: [] };
119
+ }
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gg-web-engine/core",
3
- "version": "0.0.39",
3
+ "version": "0.0.40",
4
4
  "description": "An attempt to create open source game engine for browser",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",