@draug/engine 1.0.0
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/README.md +1 -0
- package/ecs/command.ts +44 -0
- package/ecs/components/component-storage.ts +86 -0
- package/ecs/components/index.ts +12 -0
- package/ecs/components/manager.ts +91 -0
- package/ecs/components/singleton-storage.ts +51 -0
- package/ecs/components/types.ts +14 -0
- package/ecs/components/utils.ts +18 -0
- package/ecs/constant.ts +3 -0
- package/ecs/entity.ts +44 -0
- package/ecs/events-buffer.ts +62 -0
- package/ecs/query.ts +169 -0
- package/ecs/resources/index.ts +1 -0
- package/ecs/resources/resources.ts +28 -0
- package/ecs/system.ts +214 -0
- package/ecs/world.ts +99 -0
- package/index.ts +74 -0
- package/package.json +26 -0
- package/plugin/index.ts +15 -0
- package/plugin/plugin.ts +200 -0
- package/runtime/clock.ts +31 -0
- package/runtime/game-loop.ts +32 -0
- package/runtime/runtime.ts +12 -0
- package/tsconfig.json +24 -0
package/ecs/system.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { DAGNode, topologicalSort } from '@draug/core/graph/dag';
|
|
2
|
+
import type { ClassType, ComponentType } from '@draug/types/class'
|
|
3
|
+
import type { World } from "./world";
|
|
4
|
+
import type { QueryParameters } from './query';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class SystemError extends Error {
|
|
8
|
+
constructor(target: Function) {
|
|
9
|
+
super(`[System Error] (System "${target.name}".`)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class ErrNotASystem extends Error {
|
|
13
|
+
constructor(target: Function) {
|
|
14
|
+
super(`Provided class "${target.name}" is not a System! Extend your class from SystemBase.`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class ErrMissingSystemMetadata extends SystemError {
|
|
18
|
+
constructor(target: SystemCtor) {
|
|
19
|
+
super(target);
|
|
20
|
+
this.message = `${this.message}: Missing system metadata! Define system class with @System decorator.`
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SystemMetadata = {
|
|
25
|
+
/**
|
|
26
|
+
* ECS query definition used to select entities for this system execution.
|
|
27
|
+
*
|
|
28
|
+
* Determines the iteration set passed to {@link SystemBase.compute}.
|
|
29
|
+
* The ECS runtime resolves entities based on this query each update.
|
|
30
|
+
*/
|
|
31
|
+
query: Readonly<QueryParameters>;
|
|
32
|
+
/**
|
|
33
|
+
* Explicit list of component types required by the system but not necessarily
|
|
34
|
+
* part of the iteration query.
|
|
35
|
+
*
|
|
36
|
+
* Used for:
|
|
37
|
+
* - ensuring component storages are initialized in the world
|
|
38
|
+
* - safe access via `world.components.getStorage`
|
|
39
|
+
* - dependencies that should not affect entity selection
|
|
40
|
+
*
|
|
41
|
+
* This does NOT influence entity iteration; only runtime validation/setup.
|
|
42
|
+
*/
|
|
43
|
+
requiredComponents: Set<ComponentType>;
|
|
44
|
+
/**
|
|
45
|
+
* Systems that must run before this one. Pass constructor arguments
|
|
46
|
+
* `super(OtherSystemCtor, ...)` to add edges; each listed system is scheduled earlier than
|
|
47
|
+
* this instance. Used when {@link SystemsManager.build} computes a topological execution order.
|
|
48
|
+
*/
|
|
49
|
+
computeAfter?: Set<SystemCtor>;
|
|
50
|
+
};
|
|
51
|
+
export type SystemDecoratorProps = {
|
|
52
|
+
query: SystemMetadata['query'];
|
|
53
|
+
requiredComponents?: ComponentType[];
|
|
54
|
+
computeAfter?: SystemCtor[];
|
|
55
|
+
};
|
|
56
|
+
const SystemMetadataSymbol = Symbol("system");
|
|
57
|
+
type FunctionWithMetadata = Function & { [SystemMetadataSymbol]?: SystemMetadata };
|
|
58
|
+
|
|
59
|
+
export function System(props: SystemDecoratorProps): ClassDecorator {
|
|
60
|
+
return (target: Function) => {
|
|
61
|
+
const systemTarget = target as FunctionWithMetadata;
|
|
62
|
+
|
|
63
|
+
if ('__proto__' in systemTarget && systemTarget.__proto__ !== SystemBase) {
|
|
64
|
+
throw new ErrNotASystem(target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const query = { ...props.query };
|
|
68
|
+
const requiredComponents = new Set(props.requiredComponents);
|
|
69
|
+
const computeAfter = new Set(props.computeAfter);
|
|
70
|
+
const metadata: SystemMetadata = { query, requiredComponents, computeAfter };
|
|
71
|
+
|
|
72
|
+
// Теперь TS позволяет записать значение
|
|
73
|
+
systemTarget[SystemMetadataSymbol] = metadata;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getSystemMetadata(system: SystemCtor): SystemMetadata {
|
|
78
|
+
if (hasMetadata(system)) {
|
|
79
|
+
return system[SystemMetadataSymbol] as SystemMetadata;
|
|
80
|
+
}
|
|
81
|
+
throw new ErrMissingSystemMetadata(system);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
export function hasMetadata(ctor: Function): ctor is Required<FunctionWithMetadata> {
|
|
86
|
+
return SystemMetadataSymbol in ctor;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isSystem(ctor: Function): boolean {
|
|
90
|
+
return hasMetadata(ctor);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
export type SystemCtor<T extends SystemBase = SystemBase> = ClassType<T>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Arguments passed to {@link SystemBase.compute} on each {@link SystemsManager.update} call.
|
|
98
|
+
*/
|
|
99
|
+
export type SystemComputeContext = {
|
|
100
|
+
/** Entity IDs from the query for this {@link SystemBase.compute} invocation. */
|
|
101
|
+
entities: number[];
|
|
102
|
+
/** ECS world instance. */
|
|
103
|
+
world: World;
|
|
104
|
+
/** Delta time (seconds or your engine's convention) since the previous update. */
|
|
105
|
+
dt: number;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Base class for ECS systems executed by {@link SystemsManager}.
|
|
110
|
+
*
|
|
111
|
+
* Subclasses declare which components they iterate over (`queryComponents`), which component
|
|
112
|
+
* types must exist in the world for registration (`requiredComponents`), and optional ordering
|
|
113
|
+
* relative to other systems (`computeAfter` / `super(OtherSystem)`).
|
|
114
|
+
*/
|
|
115
|
+
export abstract class SystemBase {
|
|
116
|
+
/**
|
|
117
|
+
* Logic for one systems pass: run for all entities in {@link SystemComputeContext.entities}.
|
|
118
|
+
*/
|
|
119
|
+
public abstract compute(ctx: SystemComputeContext): void;
|
|
120
|
+
|
|
121
|
+
public onInit?(world: World): void;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
export class SystemsManager {
|
|
126
|
+
private systems_ = new Map<SystemCtor, SystemBase>();
|
|
127
|
+
private executionOrder_: SystemBase[] = [];
|
|
128
|
+
private requiredComponents_: Set<ComponentType> = new Set();
|
|
129
|
+
|
|
130
|
+
private dirty_ = true;
|
|
131
|
+
|
|
132
|
+
constructor(
|
|
133
|
+
private readonly world: World,
|
|
134
|
+
) { }
|
|
135
|
+
|
|
136
|
+
public getRequiredComponents(): ComponentType[] {
|
|
137
|
+
return Array.from(this.requiredComponents_);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public register<T extends SystemBase>(sys: T): void {
|
|
141
|
+
const ctor = sys.constructor as SystemCtor<T>;
|
|
142
|
+
if (this.systems_.has(ctor)) throw new Error("Duplicate system");
|
|
143
|
+
const { query, requiredComponents } = getSystemMetadata(ctor);
|
|
144
|
+
|
|
145
|
+
this.systems_.set(ctor, sys);
|
|
146
|
+
|
|
147
|
+
const q = query;
|
|
148
|
+
|
|
149
|
+
for (const c of q.include ?? [])
|
|
150
|
+
this.requiredComponents_.add(c);
|
|
151
|
+
|
|
152
|
+
for (const c of q.exclude ?? [])
|
|
153
|
+
this.requiredComponents_.add(c);
|
|
154
|
+
|
|
155
|
+
for (const c of q.anyOf ?? [])
|
|
156
|
+
this.requiredComponents_.add(c);
|
|
157
|
+
for (const c of requiredComponents)
|
|
158
|
+
this.requiredComponents_.add(c);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public build(): void {
|
|
162
|
+
this.buildSystemsArray();
|
|
163
|
+
for (const sys of this.systems_.values())
|
|
164
|
+
sys.onInit?.(this.world);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private rebuild(): void {
|
|
168
|
+
this.build();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
public get<T extends SystemBase>(ctor: SystemCtor<T>): T {
|
|
172
|
+
const s = this.systems_.get(ctor);
|
|
173
|
+
|
|
174
|
+
if (!s)
|
|
175
|
+
throw new Error("System not registered");
|
|
176
|
+
return s as T;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public update(dt: number): void {
|
|
180
|
+
if (this.dirty_)
|
|
181
|
+
this.rebuild();
|
|
182
|
+
|
|
183
|
+
this.world.events.swapAll();
|
|
184
|
+
|
|
185
|
+
for (const s of this.executionOrder_) {
|
|
186
|
+
const { query } = getSystemMetadata(s.constructor as SystemCtor)
|
|
187
|
+
const entities = this.world.query(query);
|
|
188
|
+
s.compute({ entities, world: this.world, dt });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private buildSystemsArray(): void {
|
|
193
|
+
const map = new Map<SystemCtor, DAGNode<SystemBase>>();
|
|
194
|
+
|
|
195
|
+
for (const [ctor, system] of this.systems_.entries()) {
|
|
196
|
+
map.set(ctor, new DAGNode(system));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const ctor of this.systems_.keys()) {
|
|
200
|
+
const currentNode = map.get(ctor)!;
|
|
201
|
+
const { computeAfter } = getSystemMetadata(ctor);
|
|
202
|
+
for (const depCtor of computeAfter ?? []) {
|
|
203
|
+
const depNode = map.get(depCtor);
|
|
204
|
+
if (!depNode) {
|
|
205
|
+
throw new Error(`Dependency ${depCtor.name} not registered`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
depNode.vertices.push(currentNode);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.executionOrder_ = topologicalSort(map.values()).map(x => x.data);
|
|
213
|
+
}
|
|
214
|
+
}
|
package/ecs/world.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
ECS World implementation
|
|
3
|
+
⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
|
4
|
+
⢻⣿⡗⢶⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣄
|
|
5
|
+
⠀⢻⣇⠀⠈⠙⠳⣦⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⠶⠛⠋⣹⣿⡿
|
|
6
|
+
⠀⠀⠹⣆⠀⠀⠀⠀⠙⢷⣄⣀⣀⣀⣤⣤⣤⣄⣀⣴⠞⠋⠉⠀⠀⠀⢀⣿⡟⠁
|
|
7
|
+
⠀⠀⠀⠙⢷⡀⠀⠀⠀⠀⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⡾⠋⠀⠀
|
|
8
|
+
⠀⠀⠀⠀⠈⠻⡶⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣠⡾⠋⠀⠀⠀⠀
|
|
9
|
+
⠀⠀⠀⠀⠀⣼⠃⠀⢠⠒⣆⠀⠀⠀⠀⠀⠀⢠⢲⣄⠀⠀⠀⢻⣆⠀⠀⠀⠀⠀
|
|
10
|
+
⠀⠀⠀⠀⢰⡏⠀⠀⠈⠛⠋⠀⢀⣀⡀⠀⠀⠘⠛⠃⠀⠀⠀⠈⣿⡀⠀⠀⠀⠀
|
|
11
|
+
⠀⠀⠀⠀⣾⡟⠛⢳⠀⠀⠀⠀⠀⣉⣀⠀⠀⠀⠀⣰⢛⠙⣶⠀⢹⣇⠀⠀⠀⠀
|
|
12
|
+
⠀⠀⠀⠀⢿⡗⠛⠋⠀⠀⠀⠀⣾⠋⠀⢱⠀⠀⠀⠘⠲⠗⠋⠀⠈⣿⠀⠀⠀⠀
|
|
13
|
+
⠀⠀⠀⠀⠘⢷⡀⠀⠀⠀⠀⠀⠈⠓⠒⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⡇⠀⠀⠀
|
|
14
|
+
⠀⠀⠀⠀⠀⠈⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣧⠀⠀⠀
|
|
15
|
+
⠀⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠁⠀⠀⠀
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { type ClassType, type ComponentType } from "@draug/types/class";
|
|
19
|
+
import { EntitiesManager, type EntityID, EntityRef } from "./entity";
|
|
20
|
+
import { SystemsManager } from "./system";
|
|
21
|
+
import { ECS_DEFAULTS } from "./constant";
|
|
22
|
+
import { EventBus } from "./events-buffer";
|
|
23
|
+
import { ComponentsManager } from "./components";
|
|
24
|
+
import { ResourcesManager } from "./resources/resources";
|
|
25
|
+
import { Commands } from "./command";
|
|
26
|
+
import { QueryManager, type QueryParameters } from "./query";
|
|
27
|
+
import { PluginsManager } from "../plugin";
|
|
28
|
+
|
|
29
|
+
export class World {
|
|
30
|
+
public readonly entities = new EntitiesManager();
|
|
31
|
+
public readonly components = new ComponentsManager();
|
|
32
|
+
public readonly systems = new SystemsManager(this);
|
|
33
|
+
public readonly events = new EventBus();
|
|
34
|
+
public readonly resources = new ResourcesManager();
|
|
35
|
+
public readonly commands = new Commands(this);
|
|
36
|
+
public readonly queries = new QueryManager(this);
|
|
37
|
+
public readonly plugins = new PluginsManager();
|
|
38
|
+
|
|
39
|
+
private entityRefs_ = new Map<number, EntityRef>();
|
|
40
|
+
|
|
41
|
+
constructor(maxEntityCount: number = ECS_DEFAULTS.MAX_ENTITY_COUNT) {
|
|
42
|
+
this.components = new ComponentsManager(maxEntityCount);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public getEntityRef(id: number): EntityRef {
|
|
46
|
+
let ref = this.entityRefs_.get(id);
|
|
47
|
+
if (!ref) {
|
|
48
|
+
ref = new EntityRef(this, id);
|
|
49
|
+
this.entityRefs_.set(id, ref);
|
|
50
|
+
}
|
|
51
|
+
return ref;
|
|
52
|
+
}
|
|
53
|
+
public query(params: QueryParameters): number[] {
|
|
54
|
+
return this.queries.get(params);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public removeComponent<T extends object>(ref: EntityRef, component: ComponentType<T>): void;
|
|
58
|
+
public removeComponent<T extends object>(entity: EntityID, component: ComponentType<T>): void;
|
|
59
|
+
public removeComponent<T extends object>(
|
|
60
|
+
entity: EntityID | EntityRef,
|
|
61
|
+
component: ComponentType<T>
|
|
62
|
+
): void {
|
|
63
|
+
const id = typeof entity === 'number' ? entity : entity.id;
|
|
64
|
+
|
|
65
|
+
const storage = this.components.getStorage(component);
|
|
66
|
+
storage.remove(id);
|
|
67
|
+
|
|
68
|
+
this.queries.invalidate(component);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public addComponent<T extends object>(id: EntityID, component: ClassType<T>, initFn?: (obj: T) => void): T;
|
|
72
|
+
public addComponent<T extends object>(id: EntityRef, component: ClassType<T>, initFn?: (obj: T) => void): T
|
|
73
|
+
public addComponent<T extends object>(entity: EntityID | EntityRef, component: ClassType<T>, initFn?: (obj: T) => void): T {
|
|
74
|
+
const storage = this.components.getStorage(component);
|
|
75
|
+
let id: number;
|
|
76
|
+
if (typeof entity === 'number') {
|
|
77
|
+
id = entity;
|
|
78
|
+
} else {
|
|
79
|
+
id = entity.id;
|
|
80
|
+
}
|
|
81
|
+
const c = storage.add(id, (o) => {
|
|
82
|
+
if (initFn) {
|
|
83
|
+
initFn(o);
|
|
84
|
+
}
|
|
85
|
+
return o;
|
|
86
|
+
});
|
|
87
|
+
this.queries.invalidate(component);
|
|
88
|
+
return c;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
public update(dt: number): void {
|
|
92
|
+
this.systems.update(dt);
|
|
93
|
+
this.commands.flush(this);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public build(): void {
|
|
97
|
+
this.plugins.build();
|
|
98
|
+
}
|
|
99
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export {
|
|
2
|
+
System,
|
|
3
|
+
SystemBase,
|
|
4
|
+
SystemError,
|
|
5
|
+
ErrMissingSystemMetadata,
|
|
6
|
+
ErrNotASystem,
|
|
7
|
+
isSystem,
|
|
8
|
+
getSystemMetadata,
|
|
9
|
+
SystemsManager,
|
|
10
|
+
type SystemComputeContext
|
|
11
|
+
} from './ecs/system';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
World
|
|
15
|
+
} from './ecs/world';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
Component,
|
|
19
|
+
ComponentAlreadyRegisteredError,
|
|
20
|
+
ComponentStorage,
|
|
21
|
+
ComponentsManager,
|
|
22
|
+
type IStorage,
|
|
23
|
+
SingletonStorage,
|
|
24
|
+
} from './ecs/components';
|
|
25
|
+
|
|
26
|
+
export { ResourcesManager } from './ecs/resources';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
entry,
|
|
30
|
+
Commands,
|
|
31
|
+
type CreateEntityComponentEntry,
|
|
32
|
+
type ComponentInitFn,
|
|
33
|
+
type WorldCommand,
|
|
34
|
+
} from './ecs/command';
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
EntityMaskNotFoundError,
|
|
38
|
+
EntityRef,
|
|
39
|
+
EntitiesManager,
|
|
40
|
+
UnregisteredComponentStorageError,
|
|
41
|
+
type EntityID,
|
|
42
|
+
} from './ecs/entity';
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
Plugin,
|
|
46
|
+
isPlugin,
|
|
47
|
+
getPluginMetadata,
|
|
48
|
+
PluginBase,
|
|
49
|
+
PluginsManager,
|
|
50
|
+
PluginError,
|
|
51
|
+
ErrMissingPluginMetadata,
|
|
52
|
+
ErrNotAPlugin,
|
|
53
|
+
ErrPluginNotInit,
|
|
54
|
+
ErrUnknownPlugin,
|
|
55
|
+
type PluginID,
|
|
56
|
+
type PluginMetadata,
|
|
57
|
+
type PluginDependencies,
|
|
58
|
+
} from './plugin';
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
EventBuffer,
|
|
62
|
+
EventBus,
|
|
63
|
+
createEventKey,
|
|
64
|
+
} from './ecs/events-buffer';
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
Clock,
|
|
68
|
+
type TimeSource,
|
|
69
|
+
} from './runtime/clock';
|
|
70
|
+
|
|
71
|
+
export { GameLoop, type StepFunction } from './runtime/game-loop';
|
|
72
|
+
export {
|
|
73
|
+
Runtime
|
|
74
|
+
} from './runtime/runtime';
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@draug/engine",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "future_undefined",
|
|
8
|
+
"email": "evgenijantonenkov456@gmail.com",
|
|
9
|
+
"url": "https://github.com/yazmeyaa"
|
|
10
|
+
},
|
|
11
|
+
"description": "Amber game engine. Based on ECS architecture.",
|
|
12
|
+
"license": "GPL-3.0-only",
|
|
13
|
+
"readme": "README.md",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@draug/core": "workspace:^",
|
|
16
|
+
"bitmap-index": "^1.0.9",
|
|
17
|
+
"ts-sparse-set": "^1.0.5"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "echo Hello from engine!"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@draug/types": "workspace:^",
|
|
24
|
+
"typescript": "^6.0.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/plugin/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Plugin,
|
|
3
|
+
isPlugin,
|
|
4
|
+
getPluginMetadata,
|
|
5
|
+
PluginBase,
|
|
6
|
+
PluginsManager,
|
|
7
|
+
PluginError,
|
|
8
|
+
ErrMissingPluginMetadata,
|
|
9
|
+
ErrNotAPlugin,
|
|
10
|
+
ErrPluginNotInit,
|
|
11
|
+
ErrUnknownPlugin,
|
|
12
|
+
type PluginID,
|
|
13
|
+
type PluginMetadata,
|
|
14
|
+
type PluginDependencies,
|
|
15
|
+
} from './plugin'
|
package/plugin/plugin.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { ClassType, ComponentType } from "@draug/types/class";
|
|
2
|
+
import { SystemBase } from "../ecs/system";
|
|
3
|
+
import type { World } from "../ecs/world";
|
|
4
|
+
import { DAGNode, topologicalSort, ErrDAGCycleDetected } from '@draug/core/graph/dag';
|
|
5
|
+
|
|
6
|
+
export type PluginID = string;
|
|
7
|
+
|
|
8
|
+
export type PluginDependencies = {
|
|
9
|
+
components?: ComponentType[];
|
|
10
|
+
resources?: ClassType<any>[];
|
|
11
|
+
systems?: ClassType<SystemBase>[];
|
|
12
|
+
plugins?: Array<{ id: PluginID; version?: string }>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PluginMetadata {
|
|
16
|
+
id: PluginID;
|
|
17
|
+
version: string;
|
|
18
|
+
name: string;
|
|
19
|
+
dependencies?: PluginDependencies;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PluginMetadataSymbol = Symbol("plugin");
|
|
23
|
+
|
|
24
|
+
export function Plugin(metadata: PluginMetadata): ClassDecorator {
|
|
25
|
+
return (target: Function) => {
|
|
26
|
+
if ('__proto__' in target && target.__proto__ !== PluginBase)
|
|
27
|
+
throw new ErrNotAPlugin(target);
|
|
28
|
+
|
|
29
|
+
(target as FunctionWithMetadata)[PluginMetadataSymbol] = metadata;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getPluginMetadata(plugin: ClassType<PluginBase>): PluginMetadata {
|
|
34
|
+
if (hasMetadata(plugin)) {
|
|
35
|
+
return plugin[PluginMetadataSymbol] as PluginMetadata;
|
|
36
|
+
}
|
|
37
|
+
throw new ErrMissingPluginMetadata(plugin);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type FunctionWithMetadata = Function & { [PluginMetadataSymbol]: PluginMetadata };
|
|
41
|
+
|
|
42
|
+
export function hasMetadata(ctor: Function): ctor is FunctionWithMetadata {
|
|
43
|
+
return PluginMetadataSymbol in ctor;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isPlugin(ctor: Function): boolean {
|
|
47
|
+
return hasMetadata(ctor);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export abstract class PluginBase {
|
|
51
|
+
public onPluginLoad?: (world: World) => void;
|
|
52
|
+
public onPluginUnload?: (world: World) => void;
|
|
53
|
+
public onAfterWorldInit?: (world: World) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type PluginManagerInternalPluginStorageItem<T extends ClassType<PluginBase>> = {
|
|
57
|
+
ctor: T;
|
|
58
|
+
ctorParams: ConstructorParameters<T>;
|
|
59
|
+
instance?: InstanceType<T>;
|
|
60
|
+
metadata: PluginMetadata;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export class PluginError extends Error {
|
|
64
|
+
constructor(pluginId: string) {
|
|
65
|
+
super(`Plugin error! Plugin [${pluginId}]`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class ErrNotAPlugin extends Error {
|
|
70
|
+
constructor(target: Function) {
|
|
71
|
+
super(`Provided class ${target.name} is not a Plugin! Every plugin must extends of PluginBase class.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class ErrMissingPluginMetadata extends Error {
|
|
76
|
+
constructor(plugin: ClassType<PluginBase>) {
|
|
77
|
+
super(`Provided class ${plugin.name}: Missing plugin metadata! Define plugin class with @Plugin decorator.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class ErrUnknownPlugin extends PluginError {
|
|
82
|
+
constructor(pluginId: string) {
|
|
83
|
+
super(pluginId);
|
|
84
|
+
this.message = `${super.message}: Plugin not found in manager.`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class ErrPluginNotInit extends PluginError {
|
|
89
|
+
constructor(pluginId: string) {
|
|
90
|
+
super(pluginId);
|
|
91
|
+
this.message = `${super.message}: Plugin not initiated yet. You must use PluginsManager.build() before getting instance.`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class ErrMissingPluginDependency extends PluginError {
|
|
96
|
+
constructor(pluginId: string, missingDepId: string) {
|
|
97
|
+
super(pluginId);
|
|
98
|
+
this.message = `${super.message}: Missing required dependency [${missingDepId}]. Install it first.`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class ErrDAGCycleDetectedPlugin extends Error {
|
|
103
|
+
constructor() {
|
|
104
|
+
super(`Cycle detected in plugin dependencies!`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class PluginsManager {
|
|
109
|
+
private plugins_: Map<PluginID, PluginManagerInternalPluginStorageItem<any>> = new Map();
|
|
110
|
+
private isInitiated_ = false;
|
|
111
|
+
|
|
112
|
+
public install<T extends ClassType<PluginBase>>(plugin: T, ...constructorProps: ConstructorParameters<T>): void {
|
|
113
|
+
if (!isPlugin(plugin))
|
|
114
|
+
throw new ErrMissingPluginMetadata(plugin);
|
|
115
|
+
|
|
116
|
+
const metadata = getPluginMetadata(plugin);
|
|
117
|
+
|
|
118
|
+
if (this.plugins_.has(metadata.id))
|
|
119
|
+
return;
|
|
120
|
+
|
|
121
|
+
const entry: PluginManagerInternalPluginStorageItem<T> = {
|
|
122
|
+
ctor: plugin,
|
|
123
|
+
ctorParams: constructorProps,
|
|
124
|
+
metadata,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
this.plugins_.set(metadata.id, entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public build(): void {
|
|
131
|
+
const nodes = new Map<PluginID, DAGNode<PluginID>>();
|
|
132
|
+
for (const id of this.plugins_.keys()) {
|
|
133
|
+
nodes.set(id, new DAGNode(id));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const [id, entry] of this.plugins_) {
|
|
137
|
+
const node = nodes.get(id)!;
|
|
138
|
+
const depPlugins = entry.metadata.dependencies?.plugins ?? [];
|
|
139
|
+
|
|
140
|
+
for (const dep of depPlugins) {
|
|
141
|
+
const depNode = nodes.get(dep.id);
|
|
142
|
+
if (!depNode) {
|
|
143
|
+
throw new ErrMissingPluginDependency(id, dep.id);
|
|
144
|
+
}
|
|
145
|
+
node.vertices.push(depNode);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let sortedNodes: DAGNode<PluginID>[];
|
|
150
|
+
try {
|
|
151
|
+
sortedNodes = topologicalSort(nodes.values());
|
|
152
|
+
} catch (e) {
|
|
153
|
+
if (e instanceof ErrDAGCycleDetected) {
|
|
154
|
+
throw new ErrDAGCycleDetectedPlugin();
|
|
155
|
+
}
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const node of sortedNodes) {
|
|
160
|
+
const entry = this.plugins_.get(node.data)!;
|
|
161
|
+
const { ctor, ctorParams } = entry;
|
|
162
|
+
entry.instance = new ctor(...ctorParams);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.isInitiated_ = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public getPluginMetadata(pluginOrId: ClassType<PluginBase> | PluginID): PluginMetadata {
|
|
169
|
+
const id = this.resolveId(pluginOrId);
|
|
170
|
+
const entry = this.plugins_.get(id);
|
|
171
|
+
if (!entry) throw new ErrUnknownPlugin(id);
|
|
172
|
+
return entry.metadata;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public getPluginInstance<T extends PluginBase>(pluginOrId: ClassType<T> | PluginID): T {
|
|
176
|
+
if (!this.isInitiated_) {
|
|
177
|
+
throw new Error("Plugin instance is not initiated yet. Use PluginManager.build() before use plugins.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const id = this.resolveId(pluginOrId);
|
|
181
|
+
|
|
182
|
+
const entry = this.plugins_.get(id);
|
|
183
|
+
if (!entry) throw new ErrUnknownPlugin(id);
|
|
184
|
+
if (!entry.instance) throw new ErrPluginNotInit(id);
|
|
185
|
+
|
|
186
|
+
return entry.instance as T;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private resolveId(pluginOrId: ClassType<PluginBase> | PluginID): PluginID {
|
|
190
|
+
if (typeof pluginOrId === 'string') {
|
|
191
|
+
return pluginOrId;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!isPlugin(pluginOrId)) {
|
|
195
|
+
throw new ErrMissingPluginMetadata(pluginOrId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return getPluginMetadata(pluginOrId).id;
|
|
199
|
+
}
|
|
200
|
+
};
|
package/runtime/clock.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface TimeSource {
|
|
2
|
+
now(): number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class Clock {
|
|
6
|
+
private lastTimeMs_: number;
|
|
7
|
+
private elapsedTime_: number = 0;
|
|
8
|
+
private dt_: number = 0;
|
|
9
|
+
|
|
10
|
+
public constructor(
|
|
11
|
+
private readonly timeSource_: TimeSource,
|
|
12
|
+
) {
|
|
13
|
+
this.lastTimeMs_ = timeSource_.now();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
public get dt(): number {
|
|
18
|
+
return this.dt_;
|
|
19
|
+
}
|
|
20
|
+
public get ellapsedTime(): number {
|
|
21
|
+
return this.elapsedTime_;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public tick(): void {
|
|
25
|
+
const now = this.timeSource_.now();
|
|
26
|
+
const dt = now - this.lastTimeMs_;
|
|
27
|
+
this.dt_ = dt;
|
|
28
|
+
this.elapsedTime_ += dt;
|
|
29
|
+
this.lastTimeMs_ = now;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Clock } from "./clock";
|
|
2
|
+
|
|
3
|
+
export type StepFunction = (dt: number) => void;
|
|
4
|
+
|
|
5
|
+
export class GameLoop {
|
|
6
|
+
private running = false;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
private clock: Clock,
|
|
10
|
+
private stepFn: StepFunction,
|
|
11
|
+
) { };
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
public start(platformLoop: (callback: () => void) => void) {
|
|
15
|
+
this.running = true;
|
|
16
|
+
|
|
17
|
+
const loop = () => {
|
|
18
|
+
if (!this.running) return;
|
|
19
|
+
|
|
20
|
+
this.clock.tick();
|
|
21
|
+
this.stepFn(this.clock.dt);
|
|
22
|
+
|
|
23
|
+
platformLoop(loop);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
platformLoop(loop);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public stop() {
|
|
30
|
+
this.running = false;
|
|
31
|
+
}
|
|
32
|
+
};
|