miniplex icon indicating copy to clipboard operation
miniplex copied to clipboard

miniplex-react: The Next Iteration

Open hmans opened this issue 2 years ago • 0 comments

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 and Entities 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 with useEntity. Is there a better name?

hmans avatar Jun 08 '22 06:06 hmans