Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 81 additions & 41 deletions src/query/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,35 @@ import type Entity from "../entity.js";
import type Component from "../component.js";
import type { ComponentType, Instances } from "../types.js";

// Cache for query keys to avoid regenerating them
const _queryKeyCache = new WeakMap<ComponentType[], string>();

function _getQueryKey(types: ComponentType[]): string
{
const cached = _queryKeyCache.get(types);
if (cached !== undefined) { return cached; }

const length = types.length;
if (length === 1) { return `${types[0].Id}`; }
let key: string;

const ids = new Array<number>(length);
for (let i = 0; i < length; i += 1)
if (length === 1)
{
ids[i] = types[i].Id;
key = `${types[0].Id}`;
}
else
{
const ids = new Array<number>(length);
for (let i = 0; i < length; i += 1)
{
ids[i] = types[i].Id;
}

ids.sort((a, b) => (a - b));
ids.sort((a, b) => (a - b));
key = ids.join(",");
}

return ids.join(",");
_queryKeyCache.set(types, key);
return key;
}

function _setMaskBit(mask: number[], typeId: number): void
Expand All @@ -43,14 +58,21 @@ function _unsetMaskBit(mask: number[], typeId: number): void
mask[index] &= ~(bit);
}

function _createMask(types: ComponentType[]): number[]
// Cache for query masks to avoid recreating them
const _queryMaskCache = new Map<string, number[]>();

function _createMask(key: string, types: ComponentType[]): number[]
{
const cached = _queryMaskCache.get(key);
if (cached !== undefined) { return cached; }

const mask: number[] = [];
for (const type of types)
{
_setMaskBit(mask, type.Id);
}

_queryMaskCache.set(key, mask);
return mask;
}
function _matchMask(entityMask: number[], queryMask: number[]): boolean
Expand All @@ -66,6 +88,19 @@ function _matchMask(entityMask: number[], queryMask: number[]): boolean
return true;
}

// Helper function to gather components from an entity
// This centralizes the component gathering logic to reduce code duplication
function _gatherComponents(entity: Entity, types: ComponentType[], length: number): Component[]
{
const components = new Array<Component>(length);
for (let i = 0; i < length; i += 1)
{
components[i] = entity.components.get(types[i])!;
}

return components;
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export default class QueryManager<T extends CallbackMap<T> = { }>
{
Expand All @@ -75,6 +110,9 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
private readonly _queryMasks: Map<string, number[]>;
private readonly _entityMasks: WeakMap<Entity, number[]>;

// Component-to-entity index for faster single-component queries
private readonly _componentIndex: Map<ComponentType, Set<Entity>>;

private readonly _entities: ReadonlyMap<number, Entity>;
private readonly _views: Map<string, QueryView<Component[]>>;

Expand All @@ -86,6 +124,8 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
this._queryMasks = new Map();
this._entityMasks = new WeakMap();

this._componentIndex = new Map();

this._entities = entities;
this._views = new Map();
}
Expand All @@ -109,6 +149,15 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
const entityMask = this._getEntityMask(entity);
_setMaskBit(entityMask, type.Id);

// Update component index
let entities = this._componentIndex.get(type);
if (!(entities))
{
entities = new Set();
this._componentIndex.set(type, entities);
}
entities.add(entity);

const keys = this._typeKeys.get(type);
if (!(keys)) { return; }

Expand All @@ -121,12 +170,7 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
if (!(_matchMask(entityMask, queryMask))) { continue; }

const types = this._keyTypes.get(key)!;
const length = types.length;
const components = new Array<Component>(length);
for (let i = 0; i < length; i += 1)
{
components[i] = entity.components.get(types[i])!;
}
const components = _gatherComponents(entity, types, types.length);

view.set(entity, components);
}
Expand All @@ -138,6 +182,14 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
const entityMask = this._entityMasks.get(entity);
if (entityMask) { _unsetMaskBit(entityMask, type.Id); }

// Update component index
const entities = this._componentIndex.get(type);
if (entities)
{
entities.delete(entity);
if (entities.size === 0) { this._componentIndex.delete(type); }
}

const keys = this._typeKeys.get(type);
if (!(keys)) { return; }

Expand Down Expand Up @@ -175,12 +227,17 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
const view = this._views.get(key) as QueryView<[R]> | undefined;
if (view) { return view.components[0][0]; }

for (const entity of this._entities.values())
// Use component index for faster lookup
const entities = this._componentIndex.get(type);
if (entities)
{
if (!(entity.isEnabled)) { continue; }
for (const entity of entities)
{
if (!(entity.isEnabled)) { continue; }

const component = entity.components.get(type);
if (component?.isEnabled) { return component as R; }
const component = entity.components.get(type);
if (component?.isEnabled) { return component as R; }
}
}

return undefined;
Expand All @@ -199,7 +256,7 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
const view = this._views.get(key) as QueryView<R> | undefined;
if (view) { return view.components[0]; }

const queryMask = _createMask(types);
const queryMask = _createMask(key, types);
const length = types.length;

for (const entity of this._entities.values())
Expand All @@ -209,13 +266,7 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
const entityMask = this._entityMasks.get(entity);
if (!(entityMask) || !(_matchMask(entityMask, queryMask))) { continue; }

const components = new Array<Component>(length);
for (let i = 0; i < length; i += 1)
{
components[i] = entity.components.get(types[i])!;
}

return components as R;
return _gatherComponents(entity, types, length) as R;
}

return undefined;
Expand All @@ -235,7 +286,7 @@ export default class QueryManager<T extends CallbackMap<T> = { }>

const entities = this._entities;
const entityMasks = this._entityMasks;
const queryMask = _createMask(types);
const queryMask = _createMask(key, types);
const length = types.length;

return new SmartIterator(function* (): Generator<R>
Expand All @@ -247,13 +298,7 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
const entityMask = entityMasks.get(entity);
if (!(entityMask) || !(_matchMask(entityMask, queryMask))) { continue; }

const components = new Array<Component>(length);
for (let i = 0; i < length; i += 1)
{
components[i] = entity.components.get(types[i])!;
}

yield components as R;
yield _gatherComponents(entity, types, length) as R;
}
});
}
Expand All @@ -271,7 +316,7 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
let view = this._views.get(key) as QueryView<R> | undefined;
if (view) { return view; }

const queryMask = _createMask(types);
const queryMask = _createMask(key, types);
const length = types.length;
view = new QueryView<R>();

Expand All @@ -282,13 +327,7 @@ export default class QueryManager<T extends CallbackMap<T> = { }>
const entityMask = this._entityMasks.get(entity);
if (!(entityMask) || !(_matchMask(entityMask, queryMask))) { continue; }

const components = new Array<Component>(length);
for (let i = 0; i < length; i += 1)
{
components[i] = entity.components.get(types[i])!;
}

view.set(entity, components as R);
view.set(entity, _gatherComponents(entity, types, length) as R);
}

this._views.set(key, view);
Expand All @@ -309,5 +348,6 @@ export default class QueryManager<T extends CallbackMap<T> = { }>

this._keyTypes.clear();
this._typeKeys.clear();
this._componentIndex.clear();
}
}
21 changes: 19 additions & 2 deletions src/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class World<T extends CallbackMap<T> = { }>

private readonly _systems: Map<SystemType, System>;
private readonly _enabledSystems: System[];
private readonly _enabledSystemsIndex: Map<System, number>; // O(1) lookup for disable
public get systems(): ReadonlyMap<SystemType, System> { return this._systems; }

private readonly _contexts: Map<System, WorldContext<CallbackMap>>;
Expand Down Expand Up @@ -56,6 +57,7 @@ export default class World<T extends CallbackMap<T> = { }>

this._systems = new Map();
this._enabledSystems = [];
this._enabledSystemsIndex = new Map();

this._contexts = new Map();
this._dependencies = new Map();
Expand Down Expand Up @@ -106,14 +108,28 @@ export default class World<T extends CallbackMap<T> = { }>
else { left = middle + 1; }
}

// Update indices for all systems after insertion point
for (let i = left; i < this._enabledSystems.length; i += 1)
{
this._enabledSystemsIndex.set(this._enabledSystems[i], i + 1);
}

this._enabledSystems.splice(left, 0, system);
this._enabledSystemsIndex.set(system, left);
}
private _disableSystem(system: System): void
{
const index = this._enabledSystems.indexOf(system);
if (index === -1) { return; }
const index = this._enabledSystemsIndex.get(system);
if (index === undefined) { return; }

this._enabledSystems.splice(index, 1);
this._enabledSystemsIndex.delete(system);

// Update indices for all systems after removal point
for (let i = index; i < this._enabledSystems.length; i += 1)
{
this._enabledSystemsIndex.set(this._enabledSystems[i], i);
}
}

private _addDependency(system: System, type: ResourceType): Resource
Expand Down Expand Up @@ -528,6 +544,7 @@ export default class World<T extends CallbackMap<T> = { }>

this._systems.clear();
this._enabledSystems.length = 0;
this._enabledSystemsIndex.clear();

try
{
Expand Down
Loading