Skip to content

Commit b9018c2

Browse files
authored
Merge pull request #34 from JollyPixel/engine-actor-tree
Engine actor tree
2 parents 7889049 + 7a4de58 commit b9018c2

File tree

8 files changed

+191
-91
lines changed

8 files changed

+191
-91
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/engine/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"author": "GENTILHOMME Thomas <gentilhomme.thomas@gmail.com>",
2222
"license": "MIT",
2323
"dependencies": {
24+
"picomatch": "^4.0.3",
2425
"stats.js": "^0.17.0",
2526
"three": "^0.179.1"
2627
}

packages/engine/src/Actor.ts

Lines changed: 18 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as THREE from "three";
44
// Import Internal Dependencies
55
import { type GameInstance } from "./systems/GameInstance.js";
66
import { type Component } from "./ActorComponent.js";
7+
import { ActorTree } from "./ActorTree.js";
78
import { Behavior } from "./Behavior.js";
89
import { Transform } from "./Transform.js";
910

@@ -21,13 +22,12 @@ export interface ActorOptions {
2122
layer?: number | number[];
2223
}
2324

24-
export class Actor {
25+
export class Actor extends ActorTree {
2526
gameInstance: GameInstance;
2627

2728
name: string;
2829
awoken = false;
2930
parent: Actor | null = null;
30-
children: Actor[] = [];
3131
components: Component[] = [];
3232
behaviors: Record<string, Behavior<any>[]> = {};
3333
transform: Transform;
@@ -39,7 +39,8 @@ export class Actor {
3939
gameInstance: GameInstance,
4040
options: ActorOptions
4141
) {
42-
const { name, parent = null, visible = true, layer = 0 } = options;
42+
super();
43+
const { name, parent = null, visible = true, layer } = options;
4344

4445
if (parent !== null && parent.pendingForDestruction) {
4546
throw new Error("Cannot add actor to a parent that is pending for destruction.");
@@ -53,16 +54,18 @@ export class Actor {
5354
this.threeObject.name = this.name;
5455
this.threeObject.userData.isActor = true;
5556

56-
const layers = Array.isArray(layer) ? layer : [layer];
57-
for (const layer of layers) {
58-
this.threeObject.layers.enable(layer);
59-
this.gameInstance.threeScene.layers.enable(layer);
57+
if (layer) {
58+
const layers = Array.isArray(layer) ? layer : [layer];
59+
for (const layer of layers) {
60+
this.threeObject.layers.enable(layer);
61+
this.gameInstance.threeScene.layers.enable(layer);
62+
}
6063
}
6164

6265
this.transform = new Transform(this.threeObject);
6366

6467
if (parent) {
65-
parent.children.push(this);
68+
parent.add(this);
6669
parent.threeObject.add(this.threeObject);
6770
this.threeObject.updateMatrixWorld(false);
6871
}
@@ -118,44 +121,17 @@ export class Actor {
118121
}
119122
else {
120123
this.parent.threeObject.remove(this.threeObject);
121-
this.parent.children.splice(this.parent.children.indexOf(this), 1);
124+
this.parent.remove(this);
122125
}
123126

124127
this.threeObject.clear();
125128
}
126129

127130
markDestructionPending() {
128131
this.pendingForDestruction = true;
129-
this.children.forEach((child) => child.markDestructionPending());
130-
}
131-
132-
getChild(name: string) {
133-
const nameParts = name.split("/");
134-
135-
const findChildByPath = (currentActor: Actor, pathParts: string[]): Actor | null => {
136-
if (pathParts.length === 0) {
137-
return currentActor;
138-
}
139-
140-
const [nextName, ...remainingParts] = pathParts;
141-
const foundChild = Array.from(this.gameInstance.tree.walkFromNode(currentActor))
142-
.find(({ actor }) => actor.name === nextName && !actor.isDestroyed());
143-
144-
return foundChild ?
145-
findChildByPath(foundChild.actor, remainingParts) :
146-
null;
147-
};
148-
149-
const result = findChildByPath(this, nameParts);
150-
151-
return result === this ? null : result;
152-
}
153-
154-
getChildren(): Actor[] {
155-
return this.children.filter((child) => !child.isDestroyed());
132+
this.destroyAllActors();
156133
}
157134

158-
// Behaviors
159135
addBehavior<T extends new(...args: any) => Behavior>(
160136
behaviorClass: T,
161137
properties: ConstructorParameters<T>[0] = Object.create(null)
@@ -229,16 +205,16 @@ export class Actor {
229205
this.transform.getGlobalMatrix(Transform.Matrix);
230206
}
231207

232-
const oldSiblings = (this.parent === null) ? this.gameInstance.tree.root : this.parent.children;
233-
oldSiblings.splice(oldSiblings.indexOf(this), 1);
208+
const oldSiblings = (this.parent === null) ? this.gameInstance.tree : this.parent;
209+
oldSiblings.remove(this);
234210
this.threeObject.parent?.remove(this.threeObject);
235211

236212
this.parent = newParent;
237213

238214
const siblings = (newParent === null) ?
239-
this.gameInstance.tree.root :
240-
newParent.children;
241-
siblings.push(this);
215+
this.gameInstance.tree :
216+
newParent;
217+
siblings.add(this);
242218
const threeParent = (newParent === null) ?
243219
this.gameInstance.threeScene :
244220
newParent.threeObject;

packages/engine/src/ActorChildrenTree.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/engine/src/ActorTree.ts

Lines changed: 141 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Import Third-party Dependencies
2+
import pm from "picomatch";
3+
14
// Import Internal Dependencies
25
import { Actor } from "./Actor.js";
36

@@ -15,10 +18,11 @@ export class ActorTree extends EventTarget {
1518
#addCallback?: (actor: Actor) => void;
1619
#removeCallback?: (actor: Actor) => void;
1720

18-
root: Actor[] = [];
19-
actorsToBeDestroyed: Actor[] = [];
21+
children: Actor[] = [];
2022

21-
constructor(options: ActorTreeOptions = {}) {
23+
constructor(
24+
options: ActorTreeOptions = {}
25+
) {
2226
super();
2327
this.#addCallback = options.addCallback;
2428
this.#removeCallback = options.removeCallback;
@@ -27,21 +31,119 @@ export class ActorTree extends EventTarget {
2731
add(
2832
actor: Actor
2933
): void {
30-
this.root.push(actor);
34+
this.children.push(actor);
3135
this.#addCallback?.(actor);
3236
}
3337

3438
remove(actor: Actor): void {
35-
const index = this.root.indexOf(actor);
39+
const index = this.children.indexOf(actor);
3640
if (index !== -1) {
37-
this.root.splice(index, 1);
41+
this.children.splice(index, 1);
3842
this.#removeCallback?.(actor);
3943
}
4044
}
4145

46+
* getActors(
47+
pattern: string
48+
): IterableIterator<Actor> {
49+
if (pattern.includes("/")) {
50+
yield* this.#getActorsByPatternPath(pattern);
51+
52+
return;
53+
}
54+
55+
const isPatternMatching = pm(pattern);
56+
57+
for (const { actor } of this.walk()) {
58+
if (isPatternMatching(actor.name) && !actor.pendingForDestruction) {
59+
yield actor;
60+
}
61+
}
62+
}
63+
64+
* #getActorsByPatternPath(
65+
pattern: string
66+
): IterableIterator<Actor> {
67+
const parts = pattern.split("/").filter((part) => part !== "");
68+
69+
for (const rootActor of this.children) {
70+
if (!rootActor.pendingForDestruction) {
71+
yield* this.#matchActorPath(rootActor, parts, 0);
72+
}
73+
}
74+
}
75+
76+
* #matchActorPath(
77+
actor: Actor,
78+
patternParts: string[],
79+
patternIndex: number
80+
): IterableIterator<Actor> {
81+
if (patternIndex >= patternParts.length) {
82+
return;
83+
}
84+
85+
const currentPattern = patternParts[patternIndex];
86+
const isLastPattern = patternIndex === patternParts.length - 1;
87+
88+
// eslint-disable-next-line func-style
89+
const matchSinglePattern = (name: string, pattern: string) => pm(pattern)(name);
90+
91+
if (currentPattern === "**") {
92+
if (isLastPattern) {
93+
for (const { actor: descendant } of this.#walkDepthFirstGenerator(actor)) {
94+
if (!descendant.pendingForDestruction) {
95+
yield descendant;
96+
}
97+
}
98+
99+
return;
100+
}
101+
102+
const nextPattern = patternParts[patternIndex + 1];
103+
for (const { actor: descendant } of this.#walkDepthFirstGenerator(actor)) {
104+
if (descendant.pendingForDestruction) {
105+
continue;
106+
}
107+
108+
if (matchSinglePattern(descendant.name, nextPattern)) {
109+
if (patternIndex + 1 === patternParts.length - 1) {
110+
yield descendant;
111+
}
112+
else {
113+
yield* this.#matchActorPath(descendant, patternParts, patternIndex + 2);
114+
}
115+
}
116+
}
117+
118+
return;
119+
}
120+
121+
if (matchSinglePattern(actor.name, currentPattern)) {
122+
if (isLastPattern) {
123+
if (!actor.pendingForDestruction) {
124+
yield actor;
125+
}
126+
}
127+
else {
128+
for (const child of actor.children) {
129+
yield* this.#matchActorPath(child, patternParts, patternIndex + 1);
130+
}
131+
}
132+
}
133+
}
134+
135+
/**
136+
* @example
137+
* const player = actor.children.getActor("player");
138+
* const playerPhysicsBox = actor.children.getActor("player/physics_box");
139+
*/
42140
getActor(
43141
name: string
44142
): Actor | null {
143+
if (name.includes("/")) {
144+
return this.#getActorByPath(name);
145+
}
146+
45147
for (const { actor } of this.walk()) {
46148
if (actor.name === name && !actor.pendingForDestruction) {
47149
return actor;
@@ -51,8 +153,31 @@ export class ActorTree extends EventTarget {
51153
return null;
52154
}
53155

156+
#getActorByPath(
157+
path: string
158+
): Actor | null {
159+
const parts = path.split("/").filter((part) => part !== "");
160+
const parentNode = this.getActor(parts[0]);
161+
if (!parentNode) {
162+
return null;
163+
}
164+
165+
let currentNode: Actor | null = parentNode;
166+
for (let i = 1; i < parts.length; i++) {
167+
if (!currentNode) {
168+
break;
169+
}
170+
171+
currentNode = [...currentNode.children].find(
172+
(child) => child.name === parts[i]
173+
) ?? null;
174+
}
175+
176+
return currentNode;
177+
}
178+
54179
* getRootActors(): IterableIterator<Actor> {
55-
for (const rootActor of this.root) {
180+
for (const rootActor of this.children) {
56181
if (!rootActor.pendingForDestruction) {
57182
yield rootActor;
58183
}
@@ -69,7 +194,6 @@ export class ActorTree extends EventTarget {
69194
actor: Actor
70195
) {
71196
if (!actor.pendingForDestruction) {
72-
this.actorsToBeDestroyed.push(actor);
73197
actor.markDestructionPending();
74198
}
75199
}
@@ -94,7 +218,7 @@ export class ActorTree extends EventTarget {
94218
}
95219

96220
* walk(): IterableIterator<ActorTreeNode> {
97-
for (const child of this.root) {
221+
for (const child of this.children) {
98222
yield* this.#walkDepthFirstGenerator(child, undefined);
99223
}
100224
}
@@ -106,4 +230,12 @@ export class ActorTree extends EventTarget {
106230
yield* this.#walkDepthFirstGenerator(child, rootNode);
107231
}
108232
}
233+
234+
* [Symbol.iterator](): IterableIterator<Actor> {
235+
for (const actor of this.children) {
236+
if (!actor.pendingForDestruction) {
237+
yield actor;
238+
}
239+
}
240+
}
109241
}

0 commit comments

Comments
 (0)