miniplex
miniplex copied to clipboard
miniplex-react: The Next Iteration
After using miniplex and miniplex-react in a bunch of projects, here's what I think the next round of polish (especially for the React components) could look like.
Splitting up <ManagedEntities>
?
<ManagedEntities>
(formerly known as <Collection>
) is a massively useful component, but it has caused a significant amount of confusion among early users of the library. The reason for this confusion is that this component combines both the concern of rendering state, but also managing it, which is an unusual pattern in the reactive world.
Another drawback to this is that in order to be able to pull this off, it can't consume "any" archetype, but needs to identify the entities it manages and renders via a single tag component (identified through its tag
prop.)
To keep things easy to reason about, I suggest that we split up the two concerns into separate components and/or hooks. The first concern, the rendering of entities of a specific archetype, is already covered by the <Entities>
component, so all we need is a primitive (which can be a hook, or a component, or maybe both?) for declaratively spawning a specified number of entities (and optionally destroying them when the component unmounts.)
Naming Note: we also have <Entity>
, which creates an entity, so having a component that is named <Entities>
but only renders existing components may be confusing. Maybe the component should be named more explicitly, eg. <ViewEntities>
, <RenderEntities>
, ...?
Use Cases
Let's try to describe all the things the user would want to do with miniplex-react.
Interact with the ECS world imperatively
createECS
always returns the world
created by the miniplex
package, so all situations where the user would want to interact with it imperatively are already covered. For this reason, the remaining use cases will focus on React-specific situations where you want to do things either declaratively in JSX, or through a hook.
Render existing entities
The user may already have one or more entities that they want to render (in the JSX sense.) For example, in a react-three-fiber game, certain entities may be rendered twice: once as part of the main gameplay scene, but also as icons on an overhead map.
Rendering an array of entities:
<Entities entities={entities}>
{/* ... */}
</Entities>
Rendering all entities of an archetype, and automatically re-rendering when entities get added to/removed from the archetype:
<Entities archetype="enemy">
{/* ... */}
</Entities>
<Entities archetype={["enemy", "showInMap"]}>
{/* ... */}
</Entities>
Extend existing entities
The user may be applying a pattern where an entity that already exists in the world must be extended with additional components. The typical example is a game where new enemies are spawned just by creating a new entity with the isEnemy
tag, and then having a declarative React component mix in additional components (sprites, physics, health, ...) through JSX.
This use case is already covered with the <Entities>
component used above:
<Entities archetype="isEnemy">
<Component name="health" data={1000} />
<Component name="sceneObject">
<EnemyAsset />
</Component>
</Entities>
Create singular new entities
The user will often want to create entities in React components (and have them destroyed when the component unmounts.)
In JSX:
<Entity>
<Component name="health" data={100} />
</Entity>
As a hook:
const entity = useEntity(() => ({ health: 100 }))
Both of these will create an entity with the components specified, and destroy it again once the React component unmounts.
Create multiple new entities
The user will sometimes want to create a whole series of entities (likely of the same archetype) from within a React component.
As a hook:
const entities = useEntities(10, () => ({ health: 100 }))
Both of these will create 10 entities with the components specified, and destroy them again once the React component unmounts.
<Entities count={10}>
<Component name="health" data={100} />
</Entities>
- ⚠️ The functionality of
Entity
andEntities
are equivalent, with one simply being the singular form of the other. Should both be combined into a single React component/hook? What would this be named?
Cleaning up all entities of a given archetype
In games especially, you often have the situation where an entity with a specific set of components represents a certain type of game object, and you maybe spawn an initial number of them (or not), but definitely want to destroy all of them once a specific branch of your React component tree unmounts.
For example, you might have bullets, identified by the isBullet
tag. One of your React components is rendering all bullets that currently exist in the game using <Entities archetype="isBullet">
. Some imperative code spawns new bullets through world.createEntity({ isBullet: true })
. When the React component unmounts, you may want to run some code that destroys all bullets.
Without any extra glue provided by miniplex-react, this could be done in userland through a useEffect
hook that only provides a return function:
useEffect(() => {
return () => {
for (const entity of world.archetype("isBullet").entities) {
world.queue.destroyEntity(entity)
}
}
}, [])
That is quite a bit of code, so we might want to provide something here. As a first step, maybe miniplex
itself could provide a convenience function for quickly destroy all entities of a given archetype?
useEffect(() => {
return () => {
world.queue.destroyAllEntities("isBullet")
}
}, [])
miniplex-react
could similarly provide this as a hook:
useCleanupEntities("isBullet")
It is kind of weird to have a hook that just destroys things, though. We could also have a hook that can automatically spawn entities of a specific archetype, in a variation of the useEntities
hook:
const entities = useManagedEntities("isBullet", 10, () => ({ health: 100 }))
The first argument is the tag that this hook uses to identify the entities it is supposed to manage (and destroy on unmount), the second the (optional) number of entities to spawn on mount (defaults to 0
), and the third the (optional) factory function to call to initialize each entity.
- ⚠️ This is really just the hook version of
<ManagedEntities>
🤡
Access the parent entity
The user might be writing React components that interact with the entity that contains them. For this, we need a hook that allows the user to get this parent entity.
const entity = useCurrentEntity()
- ⚠️ The naming
useCurrentEntity
is a bit iffy, and I can already see users confusing this withuseEntity
. Is there a better name?