Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/ecsify/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ecsify",
"version": "0.0.8",
"version": "0.0.10",
"private": false,
"description": "A flexible, typesafe, and performance-focused Entity Component System (ECS) library for TypeScript.",
"keywords": [],
Expand Down
8 changes: 4 additions & 4 deletions packages/ecsify/src/app/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,12 @@ export function createApp<
return this._componentRegistry.markChanged(eid, component);
},

queryEntities(filter, options) {
return this._queryRegistry.queryEntities(filter, options);
queryEntities(queryOrFilter, options) {
return this._queryRegistry.queryEntities(queryOrFilter, options);
},

queryComponents(components, filter) {
return this._queryRegistry.queryComponents(components, filter);
queryComponents(components, queryOrFilter) {
return this._queryRegistry.queryComponents(components, queryOrFilter);
},

addSystem(
Expand Down
15 changes: 9 additions & 6 deletions packages/ecsify/src/app/types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TComponentDataTuple,
TEntity,
TExecuteQueryOptions,
TQuery,
TQueryFilter,
TQueryRegistry
} from '../../query';
Expand Down Expand Up @@ -38,7 +39,7 @@ export type TApp<GAppContext extends TAppContext = TAppContext> = GAppContext['a
r: GAppContext['resources'];

/**
* Add a plugin to the app
* Add a plugin to the app.
* @param plugin - The plugin to add
* @returns The new app with the plugin added
*/
Expand All @@ -47,7 +48,7 @@ export type TApp<GAppContext extends TAppContext = TAppContext> = GAppContext['a
): TApp<TAppContext<[GNewPlugin, ...TPluginsFromAppContext<GAppContext>]>>;

/**
* Add multiple plugins to the app
* Add multiple plugins to the app.
* @param plugins - The plugins to add
* @returns The new app with the plugins added
*/
Expand Down Expand Up @@ -144,7 +145,7 @@ export type TApp<GAppContext extends TAppContext = TAppContext> = GAppContext['a
* );
* ```
*/
queryEntities(filter: TQueryFilter, options?: TExecuteQueryOptions): TEntityId[];
queryEntities(queryOrFilter: TQueryFilter | TQuery, options?: TExecuteQueryOptions): TEntityId[];

/**
* Queries components and returns matching entities with component data.
Expand All @@ -165,7 +166,7 @@ export type TApp<GAppContext extends TAppContext = TAppContext> = GAppContext['a
*/
queryComponents<T extends readonly (TComponentRef | TEntity)[]>(
components: T,
filter?: TQueryFilter
queryOrFilter?: TQueryFilter | TQuery
): TComponentDataTuple<T>[];

/**
Expand All @@ -185,14 +186,14 @@ export type TApp<GAppContext extends TAppContext = TAppContext> = GAppContext['a
): void;

/**
* Read all events of a specific type without consuming them
* Read all events of a specific type without consuming them.
*/
readEvent<GType extends keyof GAppContext['events']>(
type: GType
): TEvent<GAppContext['events'][GType]>[];

/**
* Read and consume all events of a specific type
* Read and consume all events of a specific type.
*/
consumeEvent<GType extends keyof GAppContext['events']>(
type: GType
Expand All @@ -218,6 +219,8 @@ export type TAppContext<GPlugins extends TAnyPlugin[] = []> = TInnerAppContext<
TMergePlugins<GPlugins>
>;

export type TAppWithPlugins<GPlugins extends TAnyPlugin[]> = TApp<TAppContext<GPlugins>>;

export interface TInnerAppContext<GMergedPlugins extends Record<string, any> = {}> {
components: GMergedPlugins extends { components: infer GComponents }
? GComponents extends Record<string, any>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TQueryFilter } from './types';
import { TQueryFilter } from './query-filters';

/**
* Pre-categorizes a query's evaluation strategy.
Expand Down
49 changes: 1 addition & 48 deletions packages/ecsify/src/query/create-query-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest';
import { createComponentRegistry, TComponentRegistry } from '../component';
import { createEntityIndex, TEntityIndex } from '../entity';
import { createQueryRegistry, TQueryRegistry } from './create-query-registry';
import { Added, And, Changed, Or, Removed, With, Without } from './query-filters';
import { And, Or, With, Without } from './query-filters';
import { Entity } from './types';

describe('createQueryRegistry', () => {
Expand Down Expand Up @@ -331,53 +331,6 @@ describe('createQueryRegistry', () => {
});
});

describe('generateQueryHash', () => {
it('should generate string hashes', () => {
const Position = { x: [] as number[], y: [] as number[] };

const hash = queryRegistry.generateQueryHash(With(Position));

expect(typeof hash).toBe('string');
expect(hash.length).toBeGreaterThan(0);
});

it('should generate same hash for identical filters', () => {
const Position = { x: [] as number[], y: [] as number[] };

const hash1 = queryRegistry.generateQueryHash(With(Position));
const hash2 = queryRegistry.generateQueryHash(With(Position));

expect(hash1).toBe(hash2);
});

it('should generate different hashes for different filters', () => {
const Position = { x: [] as number[], y: [] as number[] };
const Health = [] as number[];

const withHash = queryRegistry.generateQueryHash(With(Position));
const withoutHash = queryRegistry.generateQueryHash(Without(Position));
const addedHash = queryRegistry.generateQueryHash(Added(Position));
const changedHash = queryRegistry.generateQueryHash(Changed(Position));
const removedHash = queryRegistry.generateQueryHash(Removed(Position));
const healthHash = queryRegistry.generateQueryHash(With(Health));

const hashes = [withHash, withoutHash, addedHash, changedHash, removedHash, healthHash];
const uniqueHashes = new Set(hashes);
expect(uniqueHashes.size).toBe(hashes.length);
});

it('should handle component order consistently', () => {
const Position = { x: [] as number[], y: [] as number[] };
const Velocity = { x: [] as number[], y: [] as number[] };

// Order shouldn't matter due to sorting in And
const hash1 = queryRegistry.generateQueryHash(And(With(Position), With(Velocity)));
const hash2 = queryRegistry.generateQueryHash(And(With(Velocity), With(Position)));

expect(hash1).toBe(hash2);
});
});

describe('checkEntity', () => {
it('should return true when entity matches query', () => {
const Position = { x: [] as number[], y: [] as number[] };
Expand Down
139 changes: 60 additions & 79 deletions packages/ecsify/src/query/create-query-registry.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { TComponentRef, TComponentRegistry } from '../component';
import { TEntityId, TEntityIndex } from '../entity';
import { categorizeEvaluationStrategy } from './categorize-evaluation-strategy';
import { And, With } from './query-filters';
import { Entity, TEntity, TQueryComponentValue, TQueryData, TQueryFilter } from './types';
import { createQuery, isQuery, TQuery } from './queries';
import { And, TQueryFilter, With } from './query-filters';
import { Entity, TEntity, TQueryComponentValue } from './types';

/**
* Creates a new query registry
Expand All @@ -16,59 +16,53 @@ export function createQueryRegistry(
_componentRegistry: componentRegistry,
_queryCache: new Map(),

queryEntities(filter, options = {}) {
queryEntities(filterOrQuery, options = {}) {
const { cache = true, ...getQueryOptions } = options;
const queryData = this.getQuery(filter, getQueryOptions);
const query = isQuery(filterOrQuery)
? filterOrQuery
: this.getQuery(filterOrQuery, getQueryOptions);

// Return cached result if available and not dirty
if (!queryData.isDirty && cache) {
return queryData.cachedResult;
if (!query.isDirty && cache) {
return query.cachedResult;
}

// Exit if no entities exist
if (this._entityIndex._aliveCount <= 0) {
queryData.cachedResult = [];
queryData.isDirty = false;
query.cachedResult = [];
query.isDirty = false;
return [];
}

// Find matching entities
// Dense iteration with O(1) bitmask checks - simple and cache-friendly
// If this becomes slow: consider archetype system (group entities by component signature)?
// https://www.youtube.com/watch?v=71RSWVyOMEY
const matchingEntities: TEntityId[] = [];
for (let i = 0; i < this._entityIndex._aliveCount; i++) {
const eid = this._entityIndex._dense[i];
if (eid != null && filter.evaluate(this, eid, queryData)) {
matchingEntities.push(eid);
}
}
const matchingEntities = query.query(this);

// Cache results
queryData.cachedResult = matchingEntities;
queryData.isDirty = false;
query.cachedResult = matchingEntities;
query.isDirty = false;

return matchingEntities;
},

queryComponents<GComponents extends readonly (TComponentRef | TEntity)[]>(
components: GComponents,
filter?: TQueryFilter
queryOrFilter?: TQueryFilter | TQuery
): TComponentDataTuple<GComponents>[] {
// Query entities matching the provided filter,
// or infer a filter from the given components if none is provided
const matchingEntities = filter
? this.queryEntities(filter)
: this.queryEntities(
And(
...components.reduce((acc, val) => {
if (val !== Entity) {
acc.push(With(val));
}
return acc;
}, [] as TQueryFilter[])
)
);
const matchingEntities =
queryOrFilter != null
? this.queryEntities(queryOrFilter)
: this.queryEntities(
And(
...components.reduce((acc, val) => {
if (val !== Entity) {
acc.push(With(val));
}
return acc;
}, [] as TQueryFilter[])
)
);

// For each entity, check if it has all components and get their data
const results: TComponentDataTuple<GComponents>[] = [];
Expand Down Expand Up @@ -126,46 +120,38 @@ export function createQueryRegistry(
},

getQuery(filter, options = {}) {
const { evaluationStrategy = categorizeEvaluationStrategy(filter) } = options;
const hash = filter.getHash(this);

// Return cached query if exists
if (this._queryCache.has(hash)) {
return this._queryCache.get(hash) as TQueryData;
return this._queryCache.get(hash) as TQuery;
}

// Create new query data
const queryData: TQueryData = {
hash,
filter,
evaluationStrategy,
cachedResult: [],
isDirty: true,
generations: []
};

// Let filter register
if (filter.register != null) {
filter.register(this, queryData);
}
return this.registerQuery(filter, options);
},

// Cache the query
this._queryCache.set(hash, queryData);
registerQuery(queryOrFilter, options = {}) {
const { evaluationStrategy } = options;

return queryData;
},
// Create new query data
const query = isQuery(queryOrFilter)
? queryOrFilter
: createQuery(this, queryOrFilter, {
evaluationStrategy,
register: false // Will be registered below
});

registerQuery(filter) {
return this.getQuery(filter);
},
// Let query register itself
query.register(this);

// Cache the query
this._queryCache.set(query.hash, query);

generateQueryHash(filter) {
return filter.getHash(this);
return query;
},

checkEntity(queryData, eid) {
// Use the stored filter's evaluate method with the query data
return queryData.filter.evaluate(this, eid, queryData);
checkEntity(query, eid) {
return query.evaluate(this, eid);
},

reset() {
Expand All @@ -180,53 +166,48 @@ export interface TQueryRegistry {
/** Reference to the component registry */
_componentRegistry: TComponentRegistry;
/** Cache of compiled queries by hash */
_queryCache: Map<string, TQueryData>;
_queryCache: Map<string, TQuery>;

/**
* Queries entities that match the specified filter and returns only entity IDs.
*/
queryEntities(filter: TQueryFilter, options?: TExecuteQueryOptions): TEntityId[];
queryEntities(queryOrFilter: TQueryFilter | TQuery, options?: TExecuteQueryOptions): TEntityId[];

/**
* Queries components and returns matching entities with component data.
*/
queryComponents<GComponents extends readonly (TComponentRef | TEntity)[]>(
components: GComponents,
filter?: TQueryFilter
queryOrFilter?: TQueryFilter | TQuery
): TComponentDataTuple<GComponents>[];

/**
* Gets or creates a compiled query
*/
getQuery(filter: TQueryFilter, options?: TGetQueryOptions): TQueryData;

/**
* Registers a query (alias for getOrCreateQuery)
* Gets or creates a compiled query.
*/
registerQuery(filter: TQueryFilter): TQueryData;
getQuery(filter: TQueryFilter, options?: TRegisterQueryOptions): TQuery;

/**
* Generates a hash for a query filter
* Registers a query.
*/
generateQueryHash(filter: TQueryFilter): string;
registerQuery(queryOrFilter: TQueryFilter | TQuery, options?: TRegisterQueryOptions): TQuery;

/**
* Checks if an entity matches a query
* Checks if an entity matches a query.
*/
checkEntity(queryData: TQueryData, eid: TEntityId): boolean;
checkEntity(query: TQuery, eid: TEntityId): boolean;

/**
* Resets the query registry to its initial state
* Resets the query registry to its initial state.
*/
reset(): void;
}

export interface TGetQueryOptions {
export interface TRegisterQueryOptions {
/** Evaluation strategy to use for the query */
evaluationStrategy?: 'bitmask' | 'individual';
}

export interface TExecuteQueryOptions extends TGetQueryOptions {
export interface TExecuteQueryOptions extends TRegisterQueryOptions {
/** Whether to cache the query result */
cache?: boolean;
}
Expand Down
1 change: 1 addition & 0 deletions packages/ecsify/src/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './categorize-evaluation-strategy';
export * from './create-query-registry';
export * from './queries';
export * from './query-filters';
export * from './types';
Loading
Loading