@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 ADDED
@@ -0,0 +1 @@
1
+ # engine
package/ecs/command.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { ComponentType } from "./components";
2
+ import { World } from "./world";
3
+
4
+ export type WorldCommand = (world: World) => void;
5
+ export type ComponentInitFn<C extends ComponentType> =
6
+ (component: InstanceType<C>) => void;
7
+
8
+ export type CreateEntityComponentEntry<T extends ComponentType = ComponentType> =
9
+ T extends unknown ? [T, ComponentInitFn<T>] : never;
10
+
11
+ export function entry<T extends ComponentType>(
12
+ component: T,
13
+ init: ComponentInitFn<T> = () => { }
14
+ ): CreateEntityComponentEntry {
15
+ return [component, init as ComponentInitFn<ComponentType>];
16
+ }
17
+
18
+ export class Commands {
19
+ private readonly commandsQueue_: WorldCommand[] = [];
20
+
21
+ constructor(
22
+ private readonly world: World
23
+ ) { }
24
+
25
+ public add(cmd: WorldCommand): void {
26
+ this.commandsQueue_.push(cmd);
27
+ };
28
+ public flush(world: World): void {
29
+ for (const cmd of this.commandsQueue_)
30
+ cmd(world);
31
+ this.commandsQueue_.length = 0;
32
+ }
33
+ public createEntity(...entries: CreateEntityComponentEntry[]): number {
34
+ const id = this.world.entities.create();
35
+
36
+ const cmd = (world: World) => {
37
+ for (const [cls, initFn] of entries) {
38
+ world.addComponent(id, cls, initFn);
39
+ }
40
+ }
41
+ this.add(cmd);
42
+ return id;
43
+ }
44
+ };
@@ -0,0 +1,86 @@
1
+ import { ObjectPool } from "@draug/core/memory/pool";
2
+ import { Bitmap } from "bitmap-index";
3
+ import { SparseSet } from "ts-sparse-set";
4
+ import { ECS_DEFAULTS } from "../constant";
5
+ import type { IStorage } from "./types";
6
+ import type { ClassType } from "@draug/types/class";
7
+
8
+ export class ComponentStorage
9
+ // Only pointer-types.
10
+ <T extends object> implements IStorage<T> {
11
+ private bits_: Bitmap;
12
+ private set_: SparseSet<T>;
13
+ private pool_: ObjectPool<T>;
14
+ private id_: number = 0;
15
+ private cls: ClassType<T>
16
+
17
+ constructor(cap = ECS_DEFAULTS.MAX_ENTITY_COUNT, factory: () => T, cls: ClassType<T>) {
18
+ this.set_ = new SparseSet(cap);
19
+ this.bits_ = new Bitmap(cap);
20
+ this.pool_ = new ObjectPool(factory, cap);
21
+ this.cls = cls;
22
+ }
23
+ public bitmap(): Bitmap {
24
+ return this.bits_;
25
+ }
26
+
27
+ public get id(): number {
28
+ return this.id_;
29
+ }
30
+
31
+ public _internalSetId(id: number): number {
32
+ return this.id_ = id;
33
+ }
34
+
35
+ public add(id: number, initFn?: (obj: T) => T): T {
36
+ const obj = this.pool_.acquire();
37
+ initFn?.(obj);
38
+ const value = this.set_.add(id, obj);
39
+ this.bits_.set(id);
40
+ return value;
41
+ }
42
+
43
+ public remove(id: number): void {
44
+ const obj = this.set_.get(id);
45
+ if (!obj) return;
46
+ this.bits_.remove(id);
47
+ this.pool_.release(obj);
48
+ this.set_.remove(id);
49
+ }
50
+
51
+ public get(id: number): T | null {
52
+ return this.set_.get(id);
53
+ }
54
+
55
+ public tryGet(id: number): T {
56
+ const x = this.set_.get(id);
57
+ if (!x)
58
+ throw new Error(`[ComponentStorage "${this.cls.name}"]: Requesting non-existing item with ID ${id}.`);
59
+ return x;
60
+ }
61
+
62
+ public writeComponentsToBuf(ids: ReadonlyArray<number>, out: T[]): number {
63
+ let len = 0;
64
+ for (const id of ids) {
65
+ const obj = this.set_.get(id);
66
+ if (obj !== null) out[len++] = obj;
67
+ }
68
+ return len;
69
+ }
70
+
71
+ public has(id: number): boolean {
72
+ return this.bits_.contains(id);
73
+ }
74
+
75
+ public entityIds(): number[] {
76
+ return Array.from(this.bits_);
77
+ }
78
+
79
+ public size(): number {
80
+ return this.bits_.count();
81
+ }
82
+
83
+ public forEach(cb: (id: number) => void): void {
84
+ this.bits_.range(x => cb(x));
85
+ }
86
+ };
@@ -0,0 +1,12 @@
1
+ export type {
2
+ ComponentType,
3
+ IStorage,
4
+ } from './types'
5
+ export {
6
+ ComponentsManager,
7
+ ComponentAlreadyRegisteredError,
8
+ ComponentStorageType,
9
+ } from './manager'
10
+ export { Component, getComponentId } from './utils'
11
+ export { ComponentStorage } from './component-storage'
12
+ export { SingletonStorage } from './singleton-storage'
@@ -0,0 +1,91 @@
1
+ import { UnregisteredComponentStorageError } from "../entity";
2
+ import { ECS_DEFAULTS } from "../constant";
3
+ import { ComponentStorage } from "./component-storage";
4
+ import type { ComponentType, IStorage } from "./types";
5
+ import { SingletonStorage } from "./singleton-storage";
6
+ import { getComponentId } from "./";
7
+
8
+ export enum ComponentStorageType {
9
+ COMPONENT_STORAGE = 1,
10
+ SINGLETON_STORAGE = 2,
11
+ };
12
+
13
+ type RegisterSingletoneComponentOptions<T extends object> = {
14
+ storageType: ComponentStorageType.SINGLETON_STORAGE;
15
+ factory: () => T;
16
+ }
17
+ type RegisterComponentStorageOptions<T extends object> = {
18
+ storageType: ComponentStorageType.COMPONENT_STORAGE;
19
+ factory?: () => T;
20
+ }
21
+
22
+ type RegisterComponentOptions<T extends object> = RegisterSingletoneComponentOptions<T> | RegisterComponentStorageOptions<T>
23
+
24
+ export class ComponentAlreadyRegisteredError extends Error {
25
+ constructor(component: ComponentType) {
26
+ super(`Component ${component.name} already registered!`)
27
+ }
28
+ }
29
+
30
+ export class ComponentsManager {
31
+ private readonly storages_ = new Map<ComponentType, IStorage<any>>();
32
+ private currId_ = 0;
33
+ private nextId(): number {
34
+ return ++this.currId_;
35
+ }
36
+
37
+ constructor(
38
+ private maxEntityCount: number = ECS_DEFAULTS.MAX_ENTITY_COUNT
39
+ ) { }
40
+
41
+ public register<T extends object>(
42
+ component: ComponentType<T>,
43
+ opts?: RegisterComponentOptions<T>
44
+ ): IStorage<T> {
45
+ if (this.storages_.has(component))
46
+ return this.storages_.get(component)!;
47
+
48
+ let store: IStorage<T>;
49
+
50
+ switch (opts?.storageType) {
51
+ case ComponentStorageType.SINGLETON_STORAGE:
52
+ store = this.createSingletonStore(opts);
53
+ break;
54
+ case ComponentStorageType.COMPONENT_STORAGE:
55
+ store = this.createComponentStore(component, opts);
56
+ break;
57
+ default:
58
+ store = this.createComponentStore(component, opts);
59
+ break;
60
+ }
61
+
62
+ this.storages_.set(component, store);
63
+ return store;
64
+ }
65
+
66
+ private createComponentStore<T extends object>(component: ComponentType<T>, opts?: RegisterComponentOptions<T>): IStorage<T> {
67
+ const factory = opts?.factory ?? ((...args: any[]) => new component(...args));
68
+ const store = new ComponentStorage(this.maxEntityCount, factory, component);
69
+ store._internalSetId(this.nextId());
70
+ return store;
71
+ }
72
+
73
+ private createSingletonStore<T extends object>(opts?: RegisterSingletoneComponentOptions<T>): IStorage<T> {
74
+ if(!opts?.factory) {
75
+ throw new Error("For singletone storage provide factory is required!")
76
+ }
77
+ return new SingletonStorage(opts.factory);
78
+ }
79
+
80
+ public getStorage<T extends object>(component: ComponentType<T>): IStorage<T> {
81
+ const store = this.storages_.get(component);
82
+ if (store === undefined)
83
+ throw new UnregisteredComponentStorageError(component);
84
+
85
+ return store;
86
+ }
87
+
88
+ public getComponentId(ctor: ComponentType): number {
89
+ return getComponentId(ctor);
90
+ }
91
+ }
@@ -0,0 +1,51 @@
1
+ import type { Bitmap } from "bitmap-index";
2
+ import type { IStorage } from "./types";
3
+
4
+ export class SingletonStorage<T extends object> implements IStorage<T> {
5
+ private value: T | null = null;
6
+ private entityId: number | null = null;
7
+ constructor(private factory: () => T) { }
8
+ bitmap(): Bitmap {
9
+ throw new Error("Singletone component cannot has a bitmap!");
10
+ }
11
+
12
+ public add(id: number, initFn?: ((obj: T) => T) | undefined): T {
13
+ if (this.value !== null)
14
+ throw new Error("Singleton already initiated");
15
+
16
+ this.entityId = id;
17
+ this.value = this.factory();
18
+ initFn?.(this.value);
19
+ return this.value;
20
+
21
+ }
22
+ public remove(id: number): void {
23
+ if (!this.validateId(id))
24
+ return;
25
+ this.value = null;
26
+ this.entityId = null;
27
+ }
28
+ public get(id: number): T | null {
29
+ if (!this.validateId(id))
30
+ return null;
31
+ return this.value;
32
+ }
33
+ public tryGet(id: number): T {
34
+ if (!this.validateId(id))
35
+ throw new Error("[SingletoneStorage]: ID missmatch.");
36
+ return this.value!;
37
+ }
38
+ public has(id: number): boolean {
39
+ return this.validateId(id);
40
+ }
41
+ public size(): number {
42
+ return this.value !== null ? 1 : 0;
43
+ }
44
+ public forEach(cb: (id: number) => void): void {
45
+ if(this.entityId !== null)
46
+ cb(this.entityId);
47
+ }
48
+ private validateId(id: number): boolean {
49
+ return this.entityId !== null && id === this.entityId;
50
+ }
51
+ }
@@ -0,0 +1,14 @@
1
+ import type { ClassType } from "@draug/types/class";
2
+ import type { Bitmap } from "bitmap-index";
3
+
4
+ export type ComponentType<T extends object = object> = ClassType<T>;
5
+ export interface IStorage <T extends object> {
6
+ add(id: number, initFn?: (obj: T) => T): T;
7
+ remove(id: number): void;
8
+ get(id: number): T | null;
9
+ tryGet(id: number): T;
10
+ has(id: number): boolean
11
+ size(): number;
12
+ forEach(cb: (id: number) => void): void;
13
+ bitmap(): Bitmap;
14
+ };
@@ -0,0 +1,18 @@
1
+ const registry = new Map<Function, number>();
2
+ let id = 0;
3
+
4
+ export function Component(): ClassDecorator {
5
+ return (target: Function) => {
6
+ if (registry.has(target)) return;
7
+
8
+ registry.set(target, ++id);
9
+ }
10
+ };
11
+
12
+ export function getComponentId(ctor: Function): number {
13
+ const id = registry.get(ctor);
14
+ if (id === undefined) {
15
+ throw new Error(`Component not registered: ${ctor.name}`);
16
+ }
17
+ return id;
18
+ }
@@ -0,0 +1,3 @@
1
+ export const ECS_DEFAULTS = {
2
+ MAX_ENTITY_COUNT: Math.pow(2, 16),
3
+ } as const;
package/ecs/entity.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { ComponentType } from "./components";
2
+ import { World } from "./world";
3
+
4
+ export type EntityID = number;
5
+
6
+ export class UnregisteredComponentStorageError extends Error {
7
+ constructor(component: ComponentType) {
8
+ super(`Cannot get storage for component ${component.name}. Seems like it's not registered in world.`);
9
+ }
10
+ }
11
+
12
+ export class EntityMaskNotFoundError extends Error {
13
+ constructor(id: EntityID) {
14
+ super(`Cannot find bitmask for entity [${id}]. Seems like it's not registered in the EntityManager.`)
15
+ }
16
+ }
17
+
18
+ export class EntitiesManager {
19
+ private id_: EntityID = 0;
20
+ private nextId(): EntityID {
21
+ return ++this.id_;
22
+ }
23
+
24
+ public create(): EntityID {
25
+ return this.nextId();
26
+ }
27
+ };
28
+
29
+ export class EntityRef {
30
+ constructor(
31
+ private world: World,
32
+ public id: EntityID,
33
+ ) { }
34
+
35
+ public with<
36
+ T extends ComponentType[],
37
+ Result extends { [K in keyof T]: InstanceType<T[K]> }
38
+ >(...components: T): Result {
39
+ return components.map(c => {
40
+ const s = this.world.components.getStorage(c);
41
+ return s.tryGet(this.id);
42
+ }) as Result;
43
+ };
44
+ }
@@ -0,0 +1,62 @@
1
+ export class EventBuffer<T extends unknown> {
2
+ private readBuf: T[] = [];
3
+ private writeBuf: T[] = [];
4
+ public write(event: T): void {
5
+ this.writeBuf.push(event);
6
+ };
7
+
8
+ /**
9
+ * Advances the buffer to the next frame.
10
+ *
11
+ * Performs a double-buffer flip:
12
+ * - Promotes all events written during the previous frame (`writeBuf`)
13
+ * to be readable in the current frame (`readBuf`).
14
+ * - Reuses the previous `readBuf` as the new `writeBuf` and clears it
15
+ * to collect events for the next frame.
16
+ *
17
+ * After calling this method:
18
+ * - `get()` will return a stable snapshot of events produced in the previous frame.
19
+ * - `add()` will write into an empty buffer for the current frame.
20
+ *
21
+ * Guarantees:
22
+ * - No events written during the current frame are visible until the next `swap()`.
23
+ * - Readers observe a consistent, immutable snapshot within a frame.
24
+ *
25
+ * Expected to be called exactly once per frame, before system execution.
26
+ */
27
+ public swap() {
28
+ const tmp = this.readBuf;
29
+ this.readBuf = this.writeBuf;
30
+ this.writeBuf = tmp;
31
+ this.writeBuf.length = 0;
32
+ }
33
+
34
+ public read(): ReadonlyArray<T> {
35
+ return this.readBuf;
36
+ };
37
+ public size(): number {
38
+ return this.readBuf.length;
39
+ }
40
+ };
41
+
42
+ type EventKey<T> = symbol & { __type?: T };
43
+
44
+ export function createEventKey<T>(description?: string): EventKey<T> {
45
+ return Symbol(description) as EventKey<T>;
46
+ }
47
+
48
+ export class EventBus {
49
+ private storage: Map<symbol, EventBuffer<any>> = new Map();
50
+ public swapAll(): void {
51
+ this.storage.forEach(s => s.swap());
52
+ }
53
+
54
+ public getBuffer<T>(key: EventKey<T>): EventBuffer<T> {
55
+ let buf = this.storage.get(key);
56
+ if (!buf) {
57
+ buf = new EventBuffer<T>();
58
+ this.storage.set(key, buf);
59
+ }
60
+ return buf;
61
+ }
62
+ }
package/ecs/query.ts ADDED
@@ -0,0 +1,169 @@
1
+ import type { ComponentType } from "./components";
2
+ import type { EntityID } from "./entity";
3
+ import type { World } from "./world";
4
+ import { Bitmap } from "bitmap-index";
5
+
6
+ export type QueryParameters = {
7
+ include?: ComponentType[];
8
+ exclude?: ComponentType[];
9
+ anyOf?: ComponentType[];
10
+ excludeEntitiesIds?: number[];
11
+ filter?: (id: number) => boolean;
12
+ };
13
+
14
+ type CachedQuery = {
15
+ params: QueryParameters;
16
+ bitmap: Bitmap;
17
+ dirty: boolean;
18
+ deps: Set<ComponentType>;
19
+ };
20
+
21
+ export class QueryManager {
22
+ private cache = new Map<string, CachedQuery>();
23
+
24
+ constructor(
25
+ private readonly world: World,
26
+ ) { }
27
+
28
+ public get(params: QueryParameters): EntityID[] {
29
+ const key = this.getKey(params);
30
+ let entry = this.cache.get(key);
31
+
32
+ if (!entry) {
33
+ entry = {
34
+ params,
35
+ bitmap: this.compute(params),
36
+ dirty: false,
37
+ deps: this.collectDeps(params),
38
+ };
39
+ this.cache.set(key, entry);
40
+ }
41
+
42
+ if (entry.dirty) {
43
+ entry.bitmap = this.compute(entry.params);
44
+ entry.dirty = false;
45
+ }
46
+
47
+ // 1. Динамически применяем исключение конкретных ID
48
+ let targetBitmap = entry.bitmap;
49
+ if (params.excludeEntitiesIds?.length) {
50
+ targetBitmap = entry.bitmap.clone();
51
+ const excludeBm = new Bitmap();
52
+ for (const id of params.excludeEntitiesIds) {
53
+ excludeBm.set(id);
54
+ }
55
+ targetBitmap.andNot(excludeBm);
56
+ }
57
+
58
+ // 2. Динамически применяем фильтр
59
+ if (params.filter) {
60
+ const result: number[] = [];
61
+ targetBitmap.range(id => {
62
+ if (params.filter!(id)) result.push(id);
63
+ });
64
+ return result;
65
+ }
66
+
67
+ return this.extractIds(targetBitmap);
68
+ }
69
+
70
+ public invalidate(component: ComponentType): void {
71
+ for (const entry of this.cache.values()) {
72
+ if (entry.deps.has(component)) {
73
+ entry.dirty = true;
74
+ }
75
+ }
76
+ }
77
+
78
+ private getKey(q: QueryParameters): string {
79
+ return [
80
+ this.ids(q.include),
81
+ this.ids(q.exclude),
82
+ this.ids(q.anyOf),
83
+ ].join("|");
84
+ }
85
+
86
+ private ids(arr?: ComponentType[]): string {
87
+ if (!arr || arr.length === 0) return "";
88
+ return arr
89
+ .map(c => this.world.components.getComponentId(c))
90
+ .sort((a, b) => a - b)
91
+ .join(",");
92
+ }
93
+
94
+ private collectDeps(q: QueryParameters): Set<ComponentType> {
95
+ const set = new Set<ComponentType>();
96
+ q.include?.forEach(c => set.add(c));
97
+ q.exclude?.forEach(c => set.add(c));
98
+ q.anyOf?.forEach(c => set.add(c));
99
+ return set;
100
+ }
101
+
102
+ private compute(params: QueryParameters): Bitmap {
103
+ let result = this.combineBitmaps(params.include, 'and');
104
+
105
+ const any = this.combineBitmaps(params.anyOf, 'or');
106
+ if (any) {
107
+ result = result ? result.and(any) : any;
108
+ }
109
+
110
+ if (!result) {
111
+ return new Bitmap();
112
+ }
113
+
114
+ this.applyExclusions(result, params.exclude);
115
+ return result;
116
+ }
117
+
118
+ private combineBitmaps(
119
+ components: ComponentType[] | undefined,
120
+ op: 'and' | 'or'
121
+ ): Bitmap | null {
122
+ if (!components?.length) return null;
123
+
124
+ let result: Bitmap | null = null;
125
+ let hasAtLeastOneValid = false;
126
+
127
+ for (const c of components) {
128
+ const bm = this.world.components.getStorage(c)?.bitmap();
129
+
130
+ if (!bm) {
131
+ if (op === 'and') {
132
+ return new Bitmap();
133
+ }
134
+ continue;
135
+ }
136
+
137
+ hasAtLeastOneValid = true;
138
+ if (!result) {
139
+ result = bm.clone();
140
+ } else {
141
+ op === 'and' ? result.and(bm) : result.or(bm);
142
+ }
143
+ }
144
+
145
+ if (op === 'or' && !hasAtLeastOneValid) {
146
+ return new Bitmap();
147
+ }
148
+
149
+ return result;
150
+ }
151
+
152
+ private applyExclusions(
153
+ target: Bitmap,
154
+ excludeComponents?: ComponentType[],
155
+ ): void {
156
+ if (excludeComponents?.length) {
157
+ for (const c of excludeComponents) {
158
+ const bm = this.world.components.getStorage(c)?.bitmap();
159
+ if (bm) target.andNot(bm);
160
+ }
161
+ }
162
+ }
163
+
164
+ private extractIds(bitmap: Bitmap): number[] {
165
+ const result: number[] = [];
166
+ bitmap.range(id => { result.push(id) });
167
+ return result;
168
+ }
169
+ }
@@ -0,0 +1 @@
1
+ export { ResourcesManager } from './resources'
@@ -0,0 +1,28 @@
1
+ import type { ClassType } from "@draug/types/class";
2
+
3
+ export class ResourcesManager {
4
+ private readonly items_ = new Map<ClassType<any>, unknown>();
5
+ public insert<T extends object>(type: ClassType<T>, value: T): T {
6
+ this.items_.set(type, value);
7
+ return value;
8
+ };
9
+ public get<T extends object>(type: ClassType<T>): T {
10
+ const value = this.items_.get(type);
11
+ if (!value)
12
+ throw new Error(`Resource of class ${type.name} does not exist!`);
13
+
14
+ return value as T;
15
+ }
16
+ public getOrInsert<T extends object>(type: ClassType<T>, factory: () => T): T {
17
+ let value: T | null = (this.items_.get(type) ?? null) as T | null;
18
+ if (value === null) {
19
+ value = factory();
20
+ this.items_.set(type, value);
21
+ }
22
+
23
+ return value;
24
+ }
25
+ public remove<T>(type: ClassType<T>): void {
26
+ this.items_.delete(type);
27
+ }
28
+ };