Skip to content

Queries

Pedro Clerici edited this page Jul 26, 2025 · 1 revision

Queries allow you to find all entities that have a specific collection of components. They're one of the most fundamental building blocks of any ECS and are the primary way you'll access component data. In Thyseus, queries are system parameters, so systems can specify as many queries as they need.

Accessing Data

To query for a specific component, you can provide it as the first generic of the Query type. Your system will receive a query as an argument that can be iterated over:

import { Health } from './components';

export function querySystem(query: Query<Health>) {
	for (const health of query) {
		console.log(`${health.current}/${health.max}`);
	}
}

Here, we're querying for all entities with the Health component. In our system we iterate over each of those entities and log the current and max values; the yielded health value will be an instance of Health.

If you'd like to access multiple components in the same query, you can use a tuple:

import { struct } from 'thyseus';
import { Position, Velocity } from './components';

export function queryABSystem(query: Query<[Position, Velocity]>) {
	for (const [pos, vel] of query) {
		console.log(pos, vel);
	}
}

In this case, the query will yield [Position, Velocity] tuples. You can use tuples of any size to specify the components you need, but you cannot nest tuples.

You may also wrap your components in Readonly<T> if you'd like to narrow the types further.

Optional Access

If you'd like to access data only if it exists, but still have queries match if not, you can use Maybe<T> modifiers. These will yield either the queried component or undefined.

import { Query, Maybe } from 'thyseus';

function mySystem(query: Query<[A, Maybe<B>]>) {
	for (const [a, maybeB] of query) {
		console.log(a, maybeB);
	}
}

Filters

Queries also allow you to specify that entities must have or not have specific components without needing access to those components. These are known as filters. Filters are the second argument of the Query descriptor creator, and must always be wrapped in some filter specifier.

With

As the name implies, this filter requires that entities have a specific component. If they do not, the query will fail to match. With filters are particularly useful for zero-sized components, which you may use to "tag" entities.

function yourSystem(query: Query<A, With<B>>) {
	// Query iteration is the same as if there was no filter, but it's
	// guaranteed that all matched entities have both A and B
	for (const a of query) {
	}
}

You can pass either a single component to With, or an array of components if you want to require multiple (an "And" clause):

// Matched Entities will have A, B, and C
// This is identical to Query(A, And<With(B), With(C)>)
function yourSystem(query: Query<A, With<B, C>>) {}

Without

The opposite of With, Without requires that queried entities do not have specific components.

// All matched entities have A, and do NOT have B
function yourSystem(query: Query<A, Without<B>>) {}

Or

Or filters allow more complex query logic, where entities may satisfy either condition provided in order to match a query. Or can only accept two arguments, but you may nest them as deeply as you need. The provided arguments must be valid filters - With, Without, Or, or some tuple ("And") of those items.

// All matched entities have A, and have either B or C
function yourSystem(query: Query<A, Or<With<B>, With<C>>>) {}

Impossible Queries

When queries are created, they will throw an error if it is logically impossible for them to match any entity. For example, the following query will throw an error:

function yourSystem(query: Query<A, Without<A>>) {}

Note that queries cannot detect if they cannot match any entity merely by convention. For example, if you query for A and B but there is no mechanism in your application by which both A and B can be added to an entity, the query will not be able to detect this and no error will be thrown.

Clone this wiki locally