@miniplex/react
TypeScript icon, indicating that this package has built-in type declarations

2.0.0-next.10 • Public • Published

Tests Downloads Bundle Size

miniplex-react

React glue for miniplex, the gentle game entity manager.

Note This package contains the React glue for Miniplex. This documentation assumes that you are familiar with how Miniplex works. If you haven't done so already, please read the Miniplex documentation first.

Installation

Add miniplex-react and its peer dependency miniplex to your application using your favorite package manager, eg.

npm install miniplex miniplex-react
yarn add miniplex miniplex-react
pnpm add miniplex miniplex-react

Usage

The main entry point for this library is the createECS function, which will create a miniplex world alongside a collection of useful hooks and React components that will interact with it.

import { createECS } from "miniplex-react"

It is recommended that you invoke this function from a module in your application that exports the generated object, and then have the rest of your project import that module, similar to how you would provide a global store:

/* state.js */
export const ECS = createECS()

TypeScript note: it is recommended that you define a type that describes the structure of your entities, and pass that to the createECS function. This will make sure that any and all interactions with the ECS world and the provided hooks and components have full type checking/hinting/autocomplete support:

/* state.ts */
import { createECS } from "miniplex-react"

type Entity = {
  position: { x: number; y: number; z: number }
  velocity?: { x: number; y: number; z: number }
  health?: number
}

export const ECS = createECS<Entity>()

Using an existing World

Alternatively, you can pass an existing instance of World into createECS to use that instead of creating a new one:

import { World } from "miniplex"
import { createECS } from "miniplex-react"

const world = new World<Entity>()
const ECS = createECS(world)

The World

createECS returns a world property containing the actual ECS world. You can interact with it like you would usually do to imperatively create, modify and destroy entities:

const entity = ECS.world.createEntity({ position: { x: 0, y: 0 } })

For more details on how to interact with the ECS world, please refer to the miniplex documentation.

Describing Entities and Components

As a first step, let's add a single entity to your React application. We use <Entity> to declare the entity, and <Component> to add components to it.

import { ECS } from "./state"

const Player = () => (
  <ECS.Entity>
    <ECS.Component name="position" data={{ x: 0, y: 0, z: 0 }} />
    <ECS.Component name="health" data={100} />
  </ECS.Entity>
)

This will, once mounted, create a single entity in your ECS world, and add the position and health components to it. Once unmounted, it will also automatically destroy the entity.

Capturing object refs into components

If your components are designed to store rich objects, and these can be expressed as React components providing Refs, you can pass a single React child to <Component>, and its Ref value will automatically be picked up. For example, let's imagine a react-three-fiber based game that allows entities to have a scene object:

import { ECS } from "./state"

const Player = () => (
  <ECS.Entity>
    <ECS.Component name="position" data={{ x: 0, y: 0, z: 0 }} />
    <ECS.Component name="health" data={100} />
    <ECS.Component name="three">
      <mesh>
        <sphereGeometry />
        <meshStandardMaterial color="hotpink" />
      </mesh>
    </ECS.Component>
  </ECS.Entity>
)

Now the player's three component will be set to a reference to the Three.js scene object created by the <mesh> element.

Enhancing existing entities

<Entity> can also represent previously created entities, which can be used to enhance them with additional components. This is tremendously useful if your entities are created somewhere else, but at render time, you still need to enhance them with additional components. For example:

import { ECS } from "./state"

const Game = () => {
  const [player] = useState(() =>
    ECS.world.createEntity({
      position: { x: 0, y: 0, z: 0 },
      health: 100
    })
  )

  return (
    <>
      {/* All sorts of stuff */}
      <RenderPlayer player={player} />
      {/* More stuff */}
    </>
  )
}

const RenderPlayer = ({ player }) => (
  <ECS.Entity entity={player}>
    <ECS.Component name="three">
      <mesh>
        <sphereGeometry />
        <meshStandardMaterial color="hotpink" />
      </mesh>
    </ECS.Component>
  </ECS.Entity>
)

When <Entity> is used to represent and enhance an existing entity, the entity will not be destroyed once the component is unmounted.

The useArchetype hook

Sometimes you'll write React components that need access to entities of a specific archetype, without rendering them. This is what the useArchetype hook is for. Similar to the world.archetype function provided by miniplex, this will return the requested archetype, but it will also automatically re-render the component whenever entities are added to or removed from the archetype's entities list.

const MovementSystem = () => {
  const { entities } = ECS.useArchetype("position", "velocity")

  /* Do something with the entities here */

  return null
}

Rendering a List of Entities

The <Entities> React component takes a list of entities and renders them. For example, imagine a game that has spaceships which are either tagged as enemy or friendly. You may now have two separate React components subscribing to the corresponding archetype, and passing its list of entities to <Entities>:

const EnemyShips = () => {
  const { entities } = ECS.useArchetype("ship", "enemy")

  return (
    <ECS.Entities entities={entities}>
      <ECS.Component name="three">
        <EnemyShipModel />
      </ECS.Component>
    </ECS.Entities>
  )
}

const FriendlyShips = () => {
  const { entities } = ECS.useArchetype("ship", "friendly")

  return (
    <ECS.Entities entities={entities}>
      <ECS.Component name="three">
        <FriendlyShipModel />
      </ECS.Component>
    </ECS.Entities>
  )
}

Since rendering all entities of a specific archetype is such a common operation, this library also provides a <ArchetypeEntities> component that does exactly that:

/* Archetypes can be specified using arrays of component names: */
const EnemyShips = () => (
  <ECS.ArchetypeEntities archetype={["ship", "enemy"]}>
    <ECS.Component name="three">
      <EnemyShipModel />
    </ECS.Component>
  </ECS.ArchetypeEntities>
)

/* Or just strings: */
const HealthPickups = () => (
  <ECS.ArchetypeEntities archetype="healthPickup">
    <ECS.Component name="three">
      <HealthPickupModel />
    </ECS.Component>
  </ECS.ArchetypeEntities>
)

Using Render Props

<Entity>, <Entities> and <ArchetypeEntities> all support the optional use of children render props, where instead of JSX children, you pass a function that receives each entity as its first and only argument, and is expected to return the JSX that is to be rendered. This is useful if you're rendering a collection of entities and need some code to run for each entity, for example when setting random values like in this example:

const EnemyShips = () => (
  <ECS.ArchetypeEntities archetype={["enemy"]}>
    {(entity) => (
      <ECS.Entity entity={entity}>
        {/* Randomize the value of the health component */}
        <ECS.Component name="health" value={Math.random() * 1000}>

        <ECS.Component name="three">
          <EnemyShipModel />
        </ECS.Component>
      </ECS.Entity>
    )}
  </ECS.ArchetypeEntities>
)

Advanced Usage

Hooking into the current entities

When you're composing entities from nested components, you may need to get the current entity context the React component is in. You can do this using the useCurrentEntity hook:

const Health = () => {
  const entity = ECS.useCurrentEntity()

  useEffect(() => {
    /* Do something with the entity here */
  })

  return null
}

Managed Entity Collections

Note This feature is still experimental and may change (or even be removed) in the future.

In games and other ECS-oriented applications, you will often have several distinct entity types -- like spaceships, asteroids, bullets, explosions, etc. -- even if these entities are composed of several shared ECS components. All entities within a specific entity type are typically composed from the same set of components (eg. spaceships always have a position and a velocity), and rendered in a similar manner (eg. bullets will always be rendered using a small box mesh, but with varying materials.)

The <ManagedEntities> React component is an abstraction over this. It will take over management and rendering of such an entity type, assuming that this type can be identified by the presence of a specific tag (a tag being a miniplex component that is always just true and doesn't hold any additional data; miniplex provides a Tag type and constant for this.)

Let's take a look at an example:

const Asteroids = () => (
  <ECS.ManagedEntities tag="asteroid" initial={100}>
    {(entity) => (
      <>
        <ECS.Component
          name="position"
          data={{
            x: Math.random() * 100 - 50,
            y: Math.random() * 100 - 50,
            z: Math.random() * 100 - 50
          }}
        />

        <ECS.Component name="three">
          <AsteroidModel />
        </ECS.Component>
      </>
    )}
  </ECS.ManagedEntities>
)

This code will do the following:

  • Create an initial set of 100 entities that have the asteroid tag
  • Render all of these entities, using the render function passed as a child
  • Reactively update when entities from this collection are added or removed outside of this component
  • When unmounted, destroy all entities that have the asteroid tag.

A couple of important notes:

  • The child does not have to be a render function, you can simply pass normal React children. We're using a render function here because we're randomizing the positions of newly spawned asteroids, and need these values to be different for every entity.
  • Note how the render function is passed a reference to the current entity as its first and only argument. You can use this to access existing data on the entity when needed.
  • The children of this component are automatically memoized, so if you've already rendered a hundred asteroids and a new asteroid is added, only that new asteroid will have the inner function executed. This is almost always what you want (because rerendering all items would quickly crush performance.) Keep in mind that if any of your inner components reactively rerender based on eg. state changes, this will still work fine.

Questions?

Find me on Twitter or the Poimandres Discord.

License

Copyright (c) 2022 Hendrik Mans

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Package Sidebar

Install

npm i @miniplex/react

Weekly Downloads

339

Version

2.0.0-next.10

License

MIT

Unpacked Size

43.1 kB

Total Files

13

Last publish

Collaborators

  • hendrik.mans