miniplex

repository·main·Indexed 21 days ago

https://github.com/hmans/miniplex

An Entity Component System (ECS) manager featuring a core World class for entity lifecycle and reactive querying, a Bucket class for optimized entity collection management, and miniplex-react for bridging imperative game logic with declarative React UI via components like <Entity>, <Entities>, and hooks such as useEntities.

Tokens
7.1K
Snippets
29
Records
33
Agent score
75%

What's inside miniplex

  1. Best Practice: Use imperative code for world mutations

    main

    While <ECS.Entity> is useful for one-off entities (like a Player), you should use imperative code (e.g., ECS.world.add()) for high-frequency or bulk entity management (like enemies or projectiles). Design your React components to react to these changes via <Entities> or useEntities rather than using React to manage the entire lifecycle of every entity.

    // 1. Define imperative logic in a module
    export const spawnEnemy = () =>
      ECS.world.add({
        position: { x: 0, y: 0, z: 0 },
        velocity: { x: 0, y: 0, z: 0 },
        health: 100,
        enemy: true
      })
    
    // 2. Use React to react to those changes
    export const Enemies = () => (
      <ECS.Entities in={ECS.world.with("enemy")}>
        <ECS.Component name="three">
          <EnemyShipModel />
        </ECS.Component>
      </ECS.Entities>
    )
  2. Implement systems using framework hooks

    main

    Miniplex does not provide a built-in system scheduler. Instead, you implement systems by using your existing framework's loop (e.g., useFrame from @react-three/fiber) to iterate over entities matching a specific query.

    import { useFrame } from "@react-three/fiber"
    import { ECS } from "./state"
    
    const movingEntities = ECS.world.with("position", "velocity")
    
    const MovementSystem = () => {
      useFrame((_, dt) => {
        for (const entity of movingEntities) {
          entity.position.x += entity.velocity.x * dt
          entity.position.y += entity.velocity.y * dt
          entity.position.z += entity.velocity.z * dt
        }
      })
    
      return null
    }
  3. Initialize the React API with createReactAPI

    main

    The core pattern for using miniplex-react is to create a React API object from an existing Miniplex World. It is recommended to do this in a dedicated module (e.g., state.ts) and export the resulting object so it can be imported throughout your application.

    createReactAPI automatically inherits the entity type from your World, providing full TypeScript support for all components and hooks.

    /* state.ts */
    import { World } from "miniplex"
    import createReactAPI from "miniplex-react"
    
    /* Our entity type */
    export type Entity = {
      /* ... */
    }
    
    /* Create a Miniplex world that holds our entities */
    const world = new World<Entity>()
    
    /* Create and export React bindings */
    export const ECS = createReactAPI(world)
  4. Access the underlying Miniplex world via ECS.world

    main

    The object returned by createReactAPI contains a world property. This allows you to interact with the ECS world imperatively (adding, modifying, or destroying entities) using the standard Miniplex API.

    const entity = ECS.world.add({ position: { x: 0, y: 0 } })
  5. Capture object refs into components

    main

    If a component is designed to store rich objects (like Three.js objects) and provides a React ref, you can pass a single React child to <ECS.Component>. The component's ref value will be automatically captured and stored as the component's data in the ECS world.

    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>
    )
  6. Render lists of entities with <Entities>

    main

    The <ECS.Entities> component renders a list of entities based on the in prop. The in prop accepts a Miniplex query, a world, or an array of entities.

    • Reactive Rendering: If you pass a Miniplex query (e.g., world.with('tag')), the component will automatically re-render whenever the list of entities matching that query changes.
    • Static Rendering: If you want to prevent re-renders when the query changes, pass an array of entities directly (e.g., query.entities).
    import { ECS } from "./state"
    
    const asteroids = ECS.world.with("isAsteroid")
    
    /* Automatically re-renders when the query changes */
    const Asteroids = () => (
      <ECS.Entities in={asteroids}>
        <ECS.Component name="three">
          <AsteroidModel />
        </ECS.Component>
      </ECS.Entities>
    )
    
    /* Does NOT re-render when the query changes */
    const StaticAsteroids = () => (
      <ECS.Entities in={asteroids.entities}>
        <ECS.Component name="three">
          <AsteroidModel />
        </ECS.Component>
      </ECS.Entities>
    )
  7. Subscribe to entity changes with useEntities

    main

    The useEntities hook allows a component to subscribe to a Miniplex query or world. The component will automatically re-render whenever entities are added to or removed from the selection. This is useful for implementing side effects that depend on the presence of specific entities.

    const cameraTargets = ECS.world.with("cameraTarget", "object3d")
    
    const MyCamera = () => {
      const camera = useRef<PerspectiveCamera>()
      const [cameraTarget] = useEntities(cameraTargets)
    
      useEffect(() => {
        if (!camera.current || !cameraTarget) return
        camera.current.lookAt(cameraTarget.object3d.position)
      }, [cameraTarget])
    
      return <PerspectiveCamera ref={camera} makeDefault />
    }
  8. Get the current entity context with useCurrentEntity

    main

    When composing entities using nested components, you can use the useCurrentEntity hook to retrieve the entity represented by the nearest <ECS.Entity> ancestor in the React tree.

    const Health = () => {
      const entity = ECS.useCurrentEntity()
    
      useEffect(() => {
        /* Do something with the entity */
      }, [entity])
    
      return null
    }
  9. Use Render Props with <Entity> and <Entities>

    main

    Both <ECS.Entity> and <ECS.Entities> support children render props. Instead of JSX children, you can pass a function that receives the entity as its first argument. This is useful for accessing entity data during the render loop or performing per-entity logic (like randomization).

    const enemies = ECS.world.with("enemy")
    
    const EnemyShips = () => (
      <ECS.Entities in={enemies}>
        {(entity) => {
          const health = Math.random() * 1000
          return (
            <ECS.Entity entity={entity}>
              <ECS.Component name="health" data={health} />
              <ECS.Component name="three">
                <EnemyShipModel />
              </ECS.Component>
            </ECS.Entity>
          )
        }}
      </ECS.Entities>
    )
  10. Enhance existing entities with <Entity entity={...}>

    main

    You can use <ECS.Entity> to represent an entity that was already created elsewhere (e.g., via imperative code). This allows you to 'enhance' that entity by attaching additional components via React.

    Note: Unlike a standard <ECS.Entity>, an entity passed via the entity prop will not be destroyed when the component unmounts.

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

    main

    Use <ECS.Entity> to declare a new entity and <ECS.Component> to attach data to it.

    Lifecycle: When an <ECS.Entity> component mounts, it creates the entity in the world. When it unmounts, it automatically destroys the entity.

    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>
    )