3mcd/javelin

Detecting when an entity is included or excluded from a query

a-type opened this issue · 2 comments

I'm currently looking into integration of Javelin ECS with React, specifically react-three-fiber. I saw #104 , which was a helpful starting point - however there are some significant performance concerns around calling setState every game step. React reconciliation begins to become a bottleneck.

I'd like to set up a collection of hooks which help organize a tree of React components to render the entities of my game, including utilization of observed component changes to strategically update components which don't change as frequently. To that end my goal is to create at least the following:

function CharacterRenderer() {
    // this hook keeps an updated list of entity IDs which match the provided
    // components. it doesn't trigger re-renders until the entity list changes.
	const entities = useEntities(Character, Position);

    return <>{entities.map(id => <CharacterEntity id={id} key={id} />)}</>;
}

// memoization ensures this component only re-renders when
// internal data changes
const CharacterEntity = React.memo(({ id }) => {
    // this hook just stores a component reference retrieved from World,
    // saving us a lookup each frame. We rely on `useEntities` in the parent
    // to ensure that this component is never rendered with an entity which
    // does not have Position, otherwise an error would throw here
    const position = useComponent(id, Position);
    // this hook automatically re-renders when the observed component is modified
	const character = useObservedComponent(id, Character);

	const ref = useRef();

    // this hook runs every frame (provided by react-three-fiber) and
    // will handle frequently-changing properties like transforms without
    // triggering a re-render at all
    useFrame(() => {
		ref.current.position.set(position.x, position.y, position.z);
    });

    return <mesh ref={ref}>{character.model === 'red' ? <RedGeometry /> : <BlueGeometry />}</mesh>;
});

I believe the useComponent and useObservedComponent hooks could be done today, but I believe in order to create an efficient useEntities, I'll need to have some sort of understanding of when an entity first matches a query, and when it no longer matches the query. An event-based approach might be the most convenient.

A naive approach might be to add a callback to each Archetype for when an entity is added or removed, then have the Queries register callbacks for their Archetypes and emit events when things change. The naive part is that if an Entity is moved between two Archetypes which both match a Query, it would issue the removed and added events immediately after each other - but while it's not ideal it is at least still correct and probably sufficient as a first pass and could probably be corrected with a bit of logic in the Query.

For reference, here's a working draft of the hooks: https://gist.github.com/a-type/baf6a44d5fae1d39d3cb2ee8d22f91af (utilizing the basic World provider from #104)

3mcd commented

This is resolved by #149 with monitors, that let you detect when entities match/no longer match a query, e.g.

const ab = query(A, B)

onAttach(ab).forEach(entity => ...)
onDetach(ab).forEach(entity => ...)