Skip to content

Commit 3c4ee25

Browse files
committed
refactor(engine): optimize SceneManager, Actor and ActorTree
1 parent dd4d9c6 commit 3c4ee25

File tree

10 files changed

+185
-57
lines changed

10 files changed

+185
-57
lines changed

.changeset/puny-papers-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@jolly-pixel/engine": minor
3+
---
4+
5+
Optimize SceneManager, Actor and ActorTree

packages/engine/src/actor/Actor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export class Actor<
9595
else {
9696
this.world.sceneManager.tree.add(this);
9797
}
98+
99+
this.world.sceneManager.registerActor(this);
98100
}
99101

100102
#initializeComponent(
@@ -220,6 +222,8 @@ export class Actor<
220222
}
221223

222224
destroy() {
225+
this.world.sceneManager.unregisterActor(this);
226+
223227
for (let i = this.components.length - 1; i >= 0; i--) {
224228
this.components[i].destroy?.();
225229
}

packages/engine/src/actor/ActorTree.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,17 @@ export class ActorTree<
9393
const currentPattern = patternParts[patternIndex];
9494
const isLastPattern = patternIndex === patternParts.length - 1;
9595

96+
const matchers = new Map<string, (name: string) => boolean>();
9697
// eslint-disable-next-line func-style
97-
const matchSinglePattern = (name: string, pattern: string) => pm(pattern)(name);
98+
const matchSinglePattern = (name: string, pattern: string) => {
99+
let matcher = matchers.get(pattern);
100+
if (!matcher) {
101+
matcher = pm(pattern);
102+
matchers.set(pattern, matcher);
103+
}
104+
105+
return matcher(name);
106+
};
98107

99108
if (currentPattern === "**") {
100109
if (isLastPattern) {
@@ -176,7 +185,7 @@ export class ActorTree<
176185
break;
177186
}
178187

179-
currentNode = [...currentNode.children].find(
188+
currentNode = currentNode.children.find(
180189
(child) => child.name === parts[i]
181190
) ?? null;
182191
}

packages/engine/src/systems/SceneManager.ts

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,26 @@ import {
88
ActorComponent,
99
ActorTree
1010
} from "../actor/index.ts";
11+
import type { WorldDefaultContext } from "./World.ts";
1112
import type { Component } from "../components/types.ts";
1213

1314
export type SceneEvents = {
1415
awake: [];
1516
};
1617

17-
export interface SceneContract {
18-
readonly tree: ActorTree<any>;
19-
20-
componentsToBeStarted: Component[];
21-
componentsToBeDestroyed: Component[];
22-
23-
getSource(): THREE.Scene;
24-
awake(): void;
25-
beginFrame(): void;
26-
update(deltaTime: number): void;
27-
fixedUpdate(deltaTime: number): void;
28-
endFrame(): void;
29-
destroyActor(actor: Actor<any>): void;
30-
}
31-
32-
export class SceneManager extends EventEmitter<
33-
SceneEvents
34-
> implements SceneContract {
18+
export class SceneManager<
19+
TContext = WorldDefaultContext
20+
> extends EventEmitter<SceneEvents> {
3521
default: THREE.Scene;
3622

3723
componentsToBeStarted: Component[] = [];
3824
componentsToBeDestroyed: Component[] = [];
3925

40-
#cachedActors: Actor<any>[] = [];
26+
#registeredActors: Set<Actor<TContext>> = new Set();
27+
#actorsByName: Map<string, Actor<TContext>[]> = new Map();
28+
#cachedActors: Actor<TContext>[] = [];
4129

42-
readonly tree = new ActorTree<any>({
30+
readonly tree = new ActorTree<TContext>({
4331
addCallback: (actor) => this.default.add(actor.object3D),
4432
removeCallback: (actor) => this.default.remove(actor.object3D)
4533
});
@@ -66,20 +54,15 @@ export class SceneManager extends EventEmitter<
6654
}
6755

6856
beginFrame() {
69-
this.#cachedActors.length = 0;
70-
for (const { actor } of this.tree.walk()) {
71-
this.#cachedActors.push(actor);
72-
}
73-
74-
const cachedActors = this.#cachedActors;
57+
this.#cachedActors = Array.from(this.#registeredActors);
7558

7659
let i = 0;
7760
while (i < this.componentsToBeStarted.length) {
7861
const component = this.componentsToBeStarted[i];
7962

8063
// If the component to be started is part of an actor
8164
// which will not be updated, skip it until next loop
82-
if (cachedActors.indexOf(component.actor) === -1) {
65+
if (!this.#registeredActors.has(component.actor)) {
8366
i++;
8467
continue;
8568
}
@@ -111,7 +94,7 @@ export class SceneManager extends EventEmitter<
11194
});
11295
this.componentsToBeDestroyed.length = 0;
11396

114-
const actorToBeDestroyed: Actor<any>[] = [];
97+
const actorToBeDestroyed: Actor<TContext>[] = [];
11598
this.#cachedActors.forEach((actor) => {
11699
if (actor.pendingForDestruction || actor.isDestroyed()) {
117100
actorToBeDestroyed.push(actor);
@@ -124,26 +107,71 @@ export class SceneManager extends EventEmitter<
124107
}
125108

126109
destroyActor(
127-
actor: Actor<any>
110+
actor: Actor<TContext>
128111
) {
129112
const childrenToDestroy = [...actor.children];
130113

131114
childrenToDestroy.forEach((child) => {
132115
this.destroyActor(child);
133116
});
134117

135-
const cachedIndex = this.#cachedActors.indexOf(actor);
136-
if (cachedIndex !== -1) {
137-
this.#cachedActors.splice(cachedIndex, 1);
138-
}
118+
this.unregisterActor(actor);
139119

140120
// NOTE: make sure to remove deeply into the tree
141121
this.tree.remove(actor);
142122
actor.destroy();
143123
}
144124

125+
registerActor(
126+
actor: Actor<TContext>
127+
) {
128+
this.#registeredActors.add(actor);
129+
130+
const actors = this.#actorsByName.get(actor.name);
131+
if (actors) {
132+
actors.push(actor);
133+
}
134+
else {
135+
this.#actorsByName.set(actor.name, [actor]);
136+
}
137+
}
138+
139+
unregisterActor(
140+
actor: Actor<TContext>
141+
) {
142+
this.#registeredActors.delete(actor);
143+
144+
const actors = this.#actorsByName.get(actor.name);
145+
if (actors) {
146+
const index = actors.indexOf(actor);
147+
if (index !== -1) {
148+
actors.splice(index, 1);
149+
}
150+
if (actors.length === 0) {
151+
this.#actorsByName.delete(actor.name);
152+
}
153+
}
154+
}
155+
156+
getActor(
157+
name: string
158+
): Actor<TContext> | null {
159+
const actors = this.#actorsByName.get(name);
160+
if (!actors) {
161+
return null;
162+
}
163+
164+
for (const actor of actors) {
165+
if (!actor.pendingForDestruction) {
166+
return actor;
167+
}
168+
}
169+
170+
return null;
171+
}
172+
145173
destroyComponent(
146-
component: ActorComponent<any>
174+
component: ActorComponent<TContext>
147175
) {
148176
if (component.pendingForDestruction) {
149177
return;

packages/engine/src/systems/World.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type ActorOptions
1212
} from "../actor/index.ts";
1313
import {
14-
type SceneContract
14+
type SceneManager
1515
} from "./SceneManager.ts";
1616
import { Input } from "../controls/Input.class.ts";
1717
import { GlobalAudio } from "../audio/GlobalAudio.ts";
@@ -33,7 +33,7 @@ export interface WorldOptions<
3333
> {
3434
enableOnExit?: boolean;
3535

36-
sceneManager: SceneContract;
36+
sceneManager: SceneManager<TContext>;
3737
input?: Input;
3838
audio?: GlobalAudio;
3939
context?: TContext;
@@ -52,7 +52,7 @@ export class World<
5252
renderer: Renderer<T>;
5353
input: Input;
5454
loadingManager: THREE.LoadingManager = new THREE.LoadingManager();
55-
sceneManager: SceneContract;
55+
sceneManager: SceneManager<TContext>;
5656
audio: GlobalAudio;
5757
context: TContext;
5858
loop: FixedTimeStep;

packages/engine/src/systems/rendering/ThreeRenderer.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type {
1010
RenderComponent,
1111
RendererEvents
1212
} from "./Renderer.ts";
13-
import type { SceneContract } from "../SceneManager.ts";
13+
import type { WorldDefaultContext } from "../World.ts";
14+
import type { SceneManager } from "../SceneManager.ts";
1415
import {
1516
type RenderMode,
1617
type RenderStrategy,
@@ -20,22 +21,24 @@ import {
2021

2122
export type ThreeRendererEvents = RendererEvents;
2223

23-
export interface ThreeRendererOptions {
24+
export interface ThreeRendererOptions<
25+
TContext = WorldDefaultContext
26+
> {
2427
/**
2528
* @default "direct"
2629
*/
2730
renderMode: RenderMode;
28-
sceneManager: SceneContract;
31+
sceneManager: SceneManager<TContext>;
2932
}
3033

31-
export class ThreeRenderer extends EventEmitter<
32-
ThreeRendererEvents
33-
> implements Renderer {
34+
export class ThreeRenderer<
35+
TContext = WorldDefaultContext
36+
> extends EventEmitter<ThreeRendererEvents> implements Renderer {
3437
webGLRenderer: THREE.WebGLRenderer;
3538
renderComponents: RenderComponent[] = [];
3639
renderStrategy: RenderStrategy;
3740
ratio: number | null = null;
38-
sceneManager: SceneContract;
41+
sceneManager: SceneManager<TContext>;
3942

4043
#resizeObserver: ResizeObserver | null = null;
4144
#pendingResizeWidth = 0;
@@ -44,7 +47,7 @@ export class ThreeRenderer extends EventEmitter<
4447

4548
constructor(
4649
canvas: HTMLCanvasElement,
47-
options: ThreeRendererOptions
50+
options: ThreeRendererOptions<TContext>
4851
) {
4952
super();
5053
const { sceneManager, renderMode = "direct" } = options;

packages/engine/test/mocks.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ export function createWorld(): {
2929
sceneManager: {
3030
componentsToBeStarted: ActorComponent[];
3131
tree: ReturnType<typeof createTreeActor>;
32+
registerActor: ReturnType<typeof mock.fn>;
33+
unregisterActor: ReturnType<typeof mock.fn>;
3234
};
3335
} {
3436
return {
3537
sceneManager: {
3638
componentsToBeStarted: [],
37-
tree: createTreeActor()
39+
tree: createTreeActor(),
40+
registerActor: mock.fn(),
41+
unregisterActor: mock.fn()
3842
}
3943
};
4044
}

0 commit comments

Comments
 (0)