npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@voces/miniplex

v0.7.3

Published

A developer-friendly entity management system for games and similarly demanding applications, based on ECS architecture.

Downloads

4

Readme

Version Downloads Bundle Size

Miniplex

🚨 WORK IN PROGRESS! Miniplex is mostly feature complete, but parts of the API are still being fine-tuned. Feel free to poke around, but please be ready for breaking changes!

Introduction

Miniplex is an entity management system for games and similarly demanding applications. Instead of creating separate buckets for different types of entities (eg. asteroids, enemies, pickups, the player, etc.), you throw all of them into a single store, describe their properties through components, and then write code that performs updates on entities of specific types.

If you're familiar with Entity Component System architecture, this will sound familiar to you -- and rightfully so, for Miniplex is, first and foremost, a very straight-forward ECS implementation.

If you're hearing about this approach for the first time, it may sound counter-intuitive -- but once you dive into it, you will understand how it can help you decouple concerns and keep your codebase well-structured and maintainable. This post has a nice summary:

An ECS library can essentially thought of as an API for performing a loop over a homogeneous set of entities, filtering them by some condition, and pulling out a subset of the data associated with each entity. The goal of the library is to provide a usable API for this, and to do it as fast as possible.

For a more in-depth explanation, please also see Sander Mertens' wonderful Entity Component System FAQ.

Headline Features

  • A very strong focus on developer experience. Miniplex aims to be the most convenient to use ECS implementation while still providing great performance.
  • Tiny package size and zero dependencies. (Yay!)
  • Comes with React glue, but works with any framework, and of course vanilla JavScript.
  • Can power your entire project or just parts of it.
  • Written in TypeScript, with full type checking for your entities.

Main differences from other ECS implementations

If you've used other Entity Component System implementations before, here's how Miniplex is different from some of them:

Entities are just normal JavaScript objects

Entities are just plain JavaScript objects, and components are just properties on those objects. Component data can be anything you need, from primitive values to entire class instances, or even reactive stores. Miniplex aims to put developer experience first, and the most important way it does this is by making its usage feel as natural as possible in a JavaScript setting.

Miniplex does not expect you to programmatically declare component types before using them, but if you're using TypeScript, you can provide a type describing your entities and Miniplex will provide full edit- and compile-time type hints and safety.

Miniplex does not have a built-in notion of systems

Unlike most other ECS implementations, Miniplex does not have any built-in notion of systems, and does not perform any of its own scheduling. This is by design; your project will likely already have an opinion on how to schedule code execution, and instead of providing its own and potentially conflicting setup, Miniplex will neatly snuggle into the one you already have.

Systems are extremely straight-forward: just write simple functions that operate on the Miniplex world, and run them in whatever fashion fits best to your project (setInterval, requestAnimationFrame, useFrame, your custom ticker implementation, and so on.)

Archetypal Queries

Entity queries are performed through archetypes, with archetypes representing a subset of your world's entities that have a specific set of components. More complex querying capabilities may be added at a later date.

Focus on Object Identities over numerical IDs

Most interactions with Miniplex are using object identity to identify entities or archetypes (instead of numerical IDs). However, entities do automatically get a built-in id component with an auto-incrementing numerical ID once they're added to the world; this is mostly meant as a convenience for situations where you need to provide a unique scalar reference (eg. as the key prop when rendering a list of entities as React components.)

Basic Usage

Miniplex can be used in any JavaScript or TypeScript project, regardless of which extra frameworks you might be using. Some React glue is provided out of the box, but let's talk about framework-less usage first.

Typing your Entities (optional)

If you're using TypeScript, you can define a type that describes your entities:

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

Creating a World

Miniplex manages entities in worlds, which act as a containers for entities as well as an API for interacting with them. You can have one big world in your project, or several smaller worlds handling separate concerns.

Note for TypeScript users: When you provide a type like we do here, every interaction with the world will provide full type hints:

import { World } from "miniplex"

const world = new World<Entity>()

Creating Entities

The main interactions with a Miniplex world are creating and destroying entities, and adding or removing components from these entities.

Let's create an entity. Note how we're immediately giving it a position component:

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

Adding Components

Now let's add a velocity component to the entity:

world.addComponent(entity, "velocity", { x: 10, y: 0, z: 0 })

Now the entity has two components: position and velocity.

Querying Entities

We're going to write some code that moves entities according to their velocity. You will typically implement this as something called a system, which, in Miniplex, are typically just normal functions that fetch the entities they are interested in, and then perform some operation on them.

Fetching only the entities that a system is interested in is the most important part in all this, and it is done through something called archetypes that can be thought of as something akin to database indices.

Since we're going to move entities, we're interested in entities that have both the position and velocity components, so let's create an archetype for that:

const movingEntities = world.archetype("position", "velocity")

Implementing Systems

Now we can implement our system, which is really just a function -- or any other piece of code -- that uses the archetype to fetch the associated entities and then iterates over them:

function movementSystem(world) {
  for (const { position, velocity } of movingEntities.entities) {
    position.x += velocity.x
    position.y += velocity.y
    position.z += velocity.z
  }
}

Note: Since entities are just plain JavaScript objects, they can easily be destructured into their components, like we're doing above.

Destroying Entities

At some point we may want to remove an entity from the world (for example, an enemy spaceship that got destroyed by the player):

world.destroyEntity(entity)

This will immediately remove the entity from the Miniplex world and all associated archetypes.

Queued Commands

All functions that modify the world (createEntity, destroyEntity, addComponent and removeComponent) also provide an alternative function that will not perform the action immediately, but instead put it into a queue:

world.queue.destroyEntity(bullet)

Once you're ready to execute the queued operations, you can flush the queue likes this:

world.queue.flush()

Note: Please remember that flushing the queue is left to you. You might, for example, do this in your game's main loop, after all systems have finished executing.

Usage with React

🚨 Warning: the React glue provided by this package is still incomplete and should be considered unstable. (It works, but there will be breaking changes!)

Even though Miniplex can be used without React (it is entirely framework agnostic), it does ship with some useful React glue, available in the miniplex/react module.

import { createECS } from "miniplex/react"

This will create an object containing a newly created Miniplex world as well as a collection of useful React components and hooks. 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.

export default createECS()

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 (see the chapters above.)

useArchetype

The useArchetype hook lets you get the entities of the specified archetype (similar to the world.get above) from within a React component. More importantly, this hook will make the component re-render every time entities are added to or removed from the archetype. This is useful for implementing systems as React components, or writing React components that render entities:

const MovementSystem = () => {
  const { entities } = useArchetype(movingEntities)

  useFrame(() => {
    for (const { position, velocity } of entities) {
      position.x += velocity.x
      position.y += velocity.y
      position.z += velocity.z
    }
  })

  return null
}

createECS also provides Entity and Component React components that you can use to declaratively create (or add components to) entities:

const Car = () => (
  <Entity>
    <Component name="position" data="{ x: 0, y: 0, z: 0 }" />
    <Component name="position" data="{ x: 10, y: 0, z: 0 }" />
    <Component name="sprite" data="/images/car.png" />
  <Entity>
)

Performance Hints

Use for instead of forEach

You might be tempted to use forEach in your system implementations, like this:

function movementSystem(world) {
  movingEntities.entities.forEach(({ position, velocity }) => {
    position.x += velocity.x
    position.y += velocity.y
    position.z += velocity.z
  })
}

This might incur a modest, but noticeable performance penalty, since you would be calling and returning from a function for every entity in the archetype. It is typically recommended to use either a for/of loop:

function movementSystem(world) {
  for (const { position, velocity } of movingEntities.entities) {
    position.x += velocity.x
    position.y += velocity.y
    position.z += velocity.z
  }
}

Or a classic for loop:

function movementSystem(world) {
  for (let i = 0; i < movingEntities.entities.length; i++) {
    const { position, velocity } = movingEntities.entities[i]
    position.x += velocity.x
    position.y += velocity.y
    position.z += velocity.z
  }
}

If your system code will under some circumstances immediately remove entities, you might even want to go the safest route of iterating through the collection in reversed order:

const withHealth = world.archetype("health")

function healthSystem(world) {
  /* Note how we're going through the list in reverse order: */
  for (let i = withHealth.entities.length; i >= 0; i--) {
    const entity = withHealth.entities[i]

    /* If health is depleted, destroy the entity */
    if (entity.health <= 0) {
      world.destroyEntity(entity)
    }
  }
}

Reuse archetypes where possible

The archetype function aims to be idempotent and will reuse existing archetypes for the same categories of entities, so you will never risk accidentally creating multiple indices of the same archetypes. It is, however, a comparatively heavyweight function, and you are advised to, wherever possible, reuse previously created archetypes.

For example, creating your archetypes within a system function like this will work, but unnecessarily create additional overhead, and is thus not recommended:

function healthSystem(world) {
  const movingEntities = world.archetype("position", "velocity")

  for (const { position, velocity } of movingEntities.entities) {
    position.x += velocity.x
    position.y += velocity.y
    position.z += velocity.z
  }
}

Instead, create the archetype outside of your system:

const movingEntities = world.archetype("position", "velocity")

function healthSystem(world) {
  for (const { position, velocity } of movingEntities.entities) {
    position.x += velocity.x
    position.y += velocity.y
    position.z += velocity.z
  }
}

Questions?

Find me on Twitter or the Poimandres Discord.