Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/excaliburjs/Excalibur. 🎮 Your friendly TypeScript 2D game engine for the web 🗡️ excaliburjs.com
excalibur excaliburjs game-development game-engine game-framework gamedev games html5-canvas typescript
Something went wrong. Try again.
8.4 kB · 270 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271import type { Entity } from './entity';import { Observable } from '../util/observable';import type { Component, ComponentCtor } from '../entity-component-system/component';
export type ComponentInstance<T> = T extends ComponentCtor<infer R> ? R : never;
/** * Turns `Entity<A | B>` into `Entity<A> | Entity<B>` */export type DistributeEntity<T> = T extends infer U extends Component ? Entity<U> : never;
export interface QueryParams< TKnownComponentCtors extends ComponentCtor<Component> = never, TAnyComponentCtors extends ComponentCtor<Component> = never> { components?: { all?: TKnownComponentCtors[]; any?: TAnyComponentCtors[]; not?: ComponentCtor<Component>[]; }; tags?: { all?: string[]; any?: string[]; not?: string[]; };}
export type QueryEntity< TAllComponentCtors extends ComponentCtor<Component> = never, TAnyComponentCtors extends ComponentCtor<Component> = never> = [TAnyComponentCtors] extends [never] // (trick to exclude `never` explicitly) ? Entity<ComponentInstance<TAllComponentCtors>> : Entity<ComponentInstance<TAllComponentCtors>> | DistributeEntity<ComponentInstance<TAnyComponentCtors>>;
/** * Represents query for entities that match a list of types that is cached and observable * * Queries can be strongly typed by supplying a type union in the optional type parameter * ```typescript * const queryAB = new ex.Query<ComponentTypeA | ComponentTypeB>(['A', 'B']); * ``` */export class Query< TAllComponentCtors extends ComponentCtor<Component> = never, TAnyComponentCtors extends ComponentCtor<Component> = never> { public readonly id: string;
private _entities: QueryEntity<TAllComponentCtors, TAnyComponentCtors>[] = []; public get entities() { if (this._dirty) { this._entities = Array.from(this.entitiesSet); this._dirty = false; } return this._entities; }
public entitiesSet: Set<QueryEntity<TAllComponentCtors, TAnyComponentCtors>> = new Set();
/** * This fires right after the component or tag is added */ public entityAdded$ = new Observable<QueryEntity<TAllComponentCtors, TAnyComponentCtors>>(); /** * This fires right before the component or tag is actually removed from the entity, it will still be available for cleanup purposes */ public entityRemoved$ = new Observable<QueryEntity<TAllComponentCtors, TAnyComponentCtors>>();
public readonly filter = { components: { all: new Set<TAllComponentCtors>(), any: new Set<TAnyComponentCtors>(), not: new Set<ComponentCtor<Component>>() }, tags: { all: new Set<string>(), any: new Set<string>(), not: new Set<string>() } }; private _dirty: boolean = false;
constructor(params: TAllComponentCtors[] | QueryParams<TAllComponentCtors, TAnyComponentCtors>) { if (Array.isArray(params)) { params = { components: { all: params } } as QueryParams<TAllComponentCtors, TAnyComponentCtors>; }
this.filter.components.all = new Set(params.components?.all ?? []); this.filter.components.any = new Set(params.components?.any ?? []); this.filter.components.not = new Set(params.components?.not ?? []); this.filter.tags.all = new Set(params.tags?.all ?? []); this.filter.tags.any = new Set(params.tags?.any ?? []); this.filter.tags.not = new Set(params.tags?.not ?? []);
this.id = Query.createId(params); }
static createId(params: Function[] | QueryParams<any, any>) { // TODO what happens if a user defines the same type name as a built in type // ! TODO this could be dangerous depending on the bundler's settings for names // Maybe some kind of hash function is better here? if (Array.isArray(params)) { params = { components: { all: params } } as QueryParams<any, any>; }
const anyComponents = params.components?.any ? `any_${Query.hashComponents(new Set(params.components?.any))}` : ''; const allComponents = params.components?.all ? `all_${Query.hashComponents(new Set(params.components?.all))}` : ''; const notComponents = params.components?.not ? `not_${Query.hashComponents(new Set(params.components?.not))}` : '';
const anyTags = params.tags?.any ? `any_${Query.hashTags(new Set(params.tags?.any))}` : ''; const allTags = params.tags?.all ? `all_${Query.hashTags(new Set(params.tags?.all))}` : ''; const notTags = params.tags?.not ? `not_${Query.hashTags(new Set(params.tags?.not))}` : '';
return [anyComponents, allComponents, notComponents, anyTags, allTags, notTags].filter(Boolean).join('-'); }
static hashTags(set: Set<string>) { return Array.from(set) .map((t) => `t_${t}`) .sort() .join('-'); }
static hashComponents(set: Set<ComponentCtor<Component>>) { return Array.from(set) .map((c) => `c_${c.name}`) .sort() .join('-'); }
matchesNotFilter(entity: Entity, removedComponent?: ComponentCtor<Component>): boolean { for (const component of this.filter.components.not) { if (removedComponent === component) { continue; } if (entity.has(component)) { return true; } }
// check if entity has none of the tags for (const tag of this.filter.tags.not) { if (entity.hasTag(tag)) { return true; } } return false; }
matches(entity: Entity, removedComponent?: ComponentCtor<Component>): boolean { // Components & Tags
// IMPORTANT: Check NOT conditions first, all exclusions first
// check if entity has none of the components for (const component of this.filter.components.not) { if (removedComponent === component) { continue; } if (entity.has(component)) { return false; } }
// check if entity has none of the tags for (const tag of this.filter.tags.not) { if (entity.hasTag(tag)) { return false; } }
// check if entity has all components for (const component of this.filter.components.all) { if (component === removedComponent) { return false; } if (!entity.has(component)) { return false; } }
// check if entity has all tags for (const tag of this.filter.tags.all) { if (!entity.hasTag(tag)) { return false; } }
// check if entity has any components if (this.filter.components.any.size > 0) { let found = false; for (const component of this.filter.components.any) { if (entity.has(component)) { found = true; break; } }
if (!found) { return false; } }
// check if entity has any tags if (this.filter.tags.any.size > 0) { let found = false; for (const tag of this.filter.tags.any) { if (entity.hasTag(tag)) { found = true; break; } }
if (!found) { return false; } }
return true; }
/** * Potentially adds or removes an entity from a query index, returns true if added, false if not added or was removed. * @param entity * @param {optional} removedComponent that should be treated as removed from the entity */ checkAndModify(entity: Entity, removedComponent?: ComponentCtor<Component>): boolean { const inCurrentQuery = this.entitiesSet.has(entity);
if (inCurrentQuery && this.matchesNotFilter(entity, removedComponent)) { this.removeEntity(entity); return false; }
const matches = this.matches(entity, removedComponent);
if (inCurrentQuery && !matches) { this.removeEntity(entity); return false; }
if (!inCurrentQuery && matches) { this._dirty = true; this.entitiesSet.add(entity); this.entityAdded$.notifyAll(entity); return true; }
return false; }
removeEntity(entity: Entity) { const removed = this.entitiesSet.delete(entity); if (removed) { this._dirty = true; this.entityRemoved$.notifyAll(entity); } }
/** * Returns a list of entities that match the query * @param sort Optional sorting function to sort entities returned from the query */ public getEntities(sort?: (a: Entity, b: Entity) => number): QueryEntity<TAllComponentCtors, TAnyComponentCtors>[] { if (sort) { this.entities.sort(sort); } return this.entities as QueryEntity<TAllComponentCtors, TAnyComponentCtors>[]; }}