Giter VIP home page Giter VIP logo

hmans / miniplex Goto Github PK

View Code? Open in Web Editor NEW
800.0 11.0 38.0 3.82 MB

A ๐Ÿ‘ฉโ€๐Ÿ’ป developer-friendly entity management system for ๐Ÿ•น games and similarly demanding applications, based on ๐Ÿ›  ECS architecture.

License: MIT License

TypeScript 98.23% JavaScript 0.41% HTML 0.87% CSS 0.49%
ecs entity-component-system game-development state-management typescript react

miniplex's Introduction

Miniplex
Version GitHub Workflow Status (with event) Downloads Bundle Size

Miniplex - the gentle game entity manager.

  • ๐Ÿš€ Manages your game entities using the Entity Component System pattern.
  • ๐Ÿณ Focuses on ease of use and developer experience.
  • ๐Ÿ’ช Can power your entire project, or just parts of it.
  • ๐Ÿงฉ Written in TypeScript, for TypeScript. (But works in plain JavaScript, too!)
  • โš›๏ธ React bindings available. They're great! (But Miniplex works in any environment.)
  • ๐Ÿ“ฆ Tiny package size and minimal dependencies.

Testimonials

From verekia:

Miniplex has been the backbone of my games for the past year and it has been a delightful experience. The TypeScript support and React integration are excellent, and the API is very clear and easy to use, even as a first ECS experience.

From Brian Breiholz:

Tested @hmans' Miniplex library over the weekend and after having previously implemented an ECS for my wip browser game, I have to say Miniplex feels like the "right" way to do ECS in #r3f.

From VERYBOMB:

Rewrote my game with Miniplex and my productivity has improved immeasurably ever since. Everything about it is so intuitive and elegant.

Table of Contents

Example

/* Define an entity type */
type Entity = {
  position: { x: number; y: number }
  velocity?: { x: number; y: number }
  health?: {
    current: number
    max: number
  }
  poisoned?: true
}

/* Create a world with entities of that type */
const world = new World<Entity>()

/* Create an entity */
const player = world.add({
  position: { x: 0, y: 0 },
  velocity: { x: 0, y: 0 },
  health: { current: 100, max: 100 }
})

/* Create another entity */
const enemy = world.add({
  position: { x: 10, y: 10 },
  velocity: { x: 0, y: 0 },
  health: { current: 100, max: 100 }
})

/* Create some queries: */
const queries = {
  moving: world.with("position", "velocity"),
  health: world.with("health"),
  poisoned: queries.health.with("poisoned")
}

/* Create functions that perform actions on entities: */
function damage({ health }: With<Entity, "health">, amount: number) {
  health.current -= amount
}

function poison(entity: With<Entity, "poisoned">) {
  world.addComponent(entity, "poisoned", true)
}

/* Create a bunch of systems: */
function moveSystem() {
  for (const { position, velocity } of queries.moving) {
    position.x += velocity.x
    position.y += velocity.y
  }
}

function poisonSystem() {
  for (const { health, poisoned } of queries.poisoned) {
    health.current -= 1
  }
}

function healthSystem() {
  for (const entity of queries.health) {
    if (entity.health.current <= 0) {
      world.remove(entity)
    }
  }
}

/* React to entities appearing/disappearing in queries: */
queries.poisoned.onEntityAdded.subscribe((entity) => {
  console.log("Poisoned:", entity)
})

Overview

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 that have specific component configurations.

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 implementation of this pattern!

If you're hearing about this approach for the first time, maybe it will sound a little 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. A nice forum post that I can't link to because it's gone offline had a nice explanation:

An ECS library can essentially be 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.

Differences from other ECS libraries

If you've used other Entity Component System libraries 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 entire reactive stores. Miniplex puts developer experience first, and the most important way it does this is by making its usage feel as natural as possible in a JavaScript environment.

Miniplex does not expect you to programmatically declare component types before using them; 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. (Hint: you can even write some classes and use their instances as entities!)

Miniplex does not have a built-in notion of systems

Unlike the majority of ECS libraries, 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, informed by whatever framework you are using; 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 archetypal queries, with individual queries indexing and holding a subset of your world's entities that have (or don't have) a specific set of components.

Focus on Object Identities over numerical IDs

Most interactions with Miniplex are using object identity to identify entities (instead of numerical IDs). Miniplex provides an optional lightweight mechanism to generate unique IDs for your entities if you need them. In more complex projects that need stable entity IDs, especially when synchronizing entities across the network, the user is encouraged to implement their own ID generation and management.

Installation

Add the miniplex package to your project using your favorite package manager:

npm add miniplex
yarn add miniplex
pnpm add miniplex

Basic Usage

Miniplex can be used in any JavaScript or TypeScript project, regardless of which extra frameworks you might be using. This document focuses on how to use Miniplex without a framework, but please also check out the framework-specific documentation available:

Creating a World

Miniplex manages entities in worlds, which act as 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 sections of your game.

import { World } from "miniplex"

const world = new World()

Typing your Entities (optional, but recommended!)

If you're using TypeScript, you can define a type that describes your entities and provide it to the World constructor to get full type support in all interactions with it:

import { World } from "miniplex"

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

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. Entities are just plain JavaScript objects that you pass into the world's add and remove functions, like here:

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

We've directly added a position component to the entity. If you're using TypeScript, the component values here will be type-checked against the type you provided to the World constructor.

Note Adding the entity will make it known to the world and all relevant queries, but it will not change the entity object itself in any way. In Miniplex, entities can live in multiple worlds at the same time! This allows you to split complex simulations into entirely separate worlds, each with their own queries, even though they might share some (or all) entities.

Adding Components

The World instance provides addComponent and removeComponent functions for adding and removing components from entities. Let's add a velocity component to the entity. Note that we're passing the entity itself as the first argument:

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

Now the entity has two components: position and velocity.

Querying Entities

Let's write some code that moves entities, which have a position, according to their velocity. You will typically implement this as something called a system, which, in Miniplex, is typically just a normal function that fetches the entities it is interested in, and then performs 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 queries that can be thought of as something similar 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 a query for that:

/* Get all entities with position and velocity */
const movingEntities = world.with("position", "velocity")

Note There is also without, which will return all entities that do not have the specified components:

const active = world.without("paused")

Queries can also be nested:

const movingEntities = world.with("position", "velocity").without("paused")

Implementing Systems

Now we can implement a system that operates on these entities! Miniplex doesn't have an opinion on how you implement systems โ€“ they can be as simple as a function. Here's a system that uses the movingEntities query we created in the previous step, iterates over all entities in it, and moves them according to their velocity:

function movementSystem() {
  for (const { position, velocity } of movingEntities) {
    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.

Now all we need to do is make sure that this system is run on a regular basis. If you're writing a game, the framework you are using will already have a mechanism that allows you to execute code once per frame; just call the movementSystem function from there!

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). We can do this through the world's remove function:

world.remove(entity)

This will immediately remove the entity from the Miniplex world and all existing queries.

Note While this will remove the entity object from the world, it will not destroy or otherwise change the object itself. In fact, you can just add it right back into the world if you want to!

Advanced Usage

We're about to dive into some advanced usage patterns. Please make sure you're familiar with the basics before continuing.

Reacting to added/removed entities

Instances of World and Query provide the built-in onEntityAdded and onEntityRemoved events that you can subscribe to to be notified about entities appearing or disappearing.

For example, in order to be notified about any entity being added to the world, you may do this:

world.onEntityAdded.subscribe((entity) => {
  console.log("A new entity has been spawned:", entity)
})

This is useful for running system-specific initialization code on entities that appear in specific queries:

const withHealth = world.with("health")

withHealth.onEntityAdded.subscribe((entity) => {
  entity.health.current = entity.health.max
})

Predicate Queries using where

Typically, you'll want to build queries the check entities for the presence of specific components; you have been using the with and without functions for this so far. But there may be the rare case where you want to query by value; for this, Miniplex provides the where function. It allows you to specify a predicate function that your entity will be checked against:

const damagedEntities = world
  .with("health")
  .where(({ health }) => health.current < health.max)

const deadEntities = world.with("health").where(({ health }) => health <= 0)

It is extremely important to note that queries that use where are in no way reactive; if the values within the entity change in a way that would change the result of your predicate function, Miniplex will not pick this up automatically.

Instead, once you know that you are using where to inspect component values, you are required to signal an updated entity by calling the reindex function:

function damageEntity(entity: With<Entity, "health">, amount: number) {
  entity.health.current -= amount
  world.reindex(entity)
}

Depending on the total number of queries you've created, reindexing can be a relatively expensive operation, so it is recommended that you use this functionality with care. Most of the time, it is more efficient to model things using additional components. The above example could, for example, be rewritten like this:

const damagedEntities = world.with("health", "damaged")

const deadEntities = world.with("health", "dead")

function damageEntity(entity: With<Entity, "health">, amount: number) {
  entity.health.current -= amount

  if (entity.health.current < entity.health.max) {
    world.addComponent(entity, "damaged")
  }

  if (entity.health.current <= 0) {
    world.addComponent(entity, "dead")
  }
}

ID Generation

When interacting with Miniplex, entities are typically identified using their object identities, which is one of the ways where Miniplex is different from typical ECS implementations, which usually make use of numerical IDs.

Most Miniplex workloads can be implemented without the use of numerical IDs, but if you ever do need such an identifier for your entities โ€“ possibly because you're wiring them up to another non-Miniplex system that expects them โ€“ Miniplex worlds provide a lightweight mechanism to generate them:

const entity = world.add({ count: 10 })
const id = world.id(entity)

You can later use this ID to look up the entity in the world:

const entity = world.entity(id)

Best Practices

Use addComponent and removeComponent for adding and removing components

Since entities are just normal objects, you might be tempted to just add new properties to (or delete properties from) them directly. This is a bad idea because it will skip the indexing step needed to make sure the entity is listed in the correct queries. Please always go through addComponent and removeComponent!

It is perfectly fine to mutate component values directly, though.

/* โœ… This is fine: */
const entity = world.add({ position: { x: 0, y: 0, z: 0 } })
entity.position.x = 10

/* โ›”๏ธ This is not: */
const entity = world.add({ position: { x: 0, y: 0, z: 0 } })
entity.velocity = { x: 10, y: 0, z: 0 }

Iterate over queries using for...of

The world as well as all queries derived from it are iterable, meaning you can use them in for...of loops. This is the recommended way to iterate over entities in a query, as it is highly performant, and iterates over the entities in reverse order, which allows you to safely remove entities from within the loop.

const withHealth = world.with("health")

/* โœ… Recommended: */
for (const entity of withHealth) {
  if (entity.health <= 0) {
    world.remove(entity)
  }
}

/* โ›”๏ธ Avoid: */
for (const entity of withHealth.entities) {
  if (entity.health <= 0) {
    world.remove(entity)
  }
}

/* โ›”๏ธ Especially avoid: */
withHealth.entities.forEach((entity) => {
  if (entity.health <= 0) {
    world.remove(entity)
  }
})

Reuse queries where possible

The functions creating and returning queries (with, without, where) aim to be idempotent and will reuse existing queries for the same set of query attributes. Checking if a query for a specific set of query attributes already exists is a comparatively heavyweight function, though, and you are advised to, wherever possible, reuse previously created queries.

/* โœ… Recommended: */
const movingEntities = world.with("position", "velocity")

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

/* โ›”๏ธ Avoid: */
function movementSystem(world) {
  /* This will work, but now the world needs to check if a query for "position" and "velocity" already exists every time this function is called, which is pure overhead. */
  for (const { position, velocity } of world.with("position", "velocity")) {
    position.x += velocity.x
    position.y += velocity.y
    position.z += velocity.z
  }
}

Questions?

If you have questions about Miniplex, you're invited to post them in our Discussions section on GitHub.

License

Copyright (c) 2023 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.

miniplex's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

miniplex's Issues

Issues in React.StrictMode

React 18's StrictMode executes effects twice, which causes Miniplex 0.9.x to fail with "Tried to add components to an entity that is not managed by this world" errors. I can't currently say exactly why this happens, but let's figure things out.

[2.0] Documentation Refresh

  • Update existing README to reflect new bits
  • Provide a summary example at the top of the README
  • Add links to codesandbox and stackblitz sandboxes with the demo game
  • Describe/document the demos
  • Document world.archetype
  • Document without
  • Document the where iterator generator
  • Document generics of archetype, with, without and where
  • Document typical pitfalls (like world.with("foo").without("bar") vs. world.without("bar").with("foo"))
  • Add a "How It Works" section that explains buckets etc.
  • Add a section on performance

Refactor React components

<Collection> is a bit iffy because the name doesn't really communicate well what the component does, so let's do a refactoring pass on the available entities. The goal:

  • <Entity>: create a new entity (and destroy it on unmounting)
  • <Entity entity={entity}>: extend an existing entity (and don't destroy it on unmounting)
  • <Entity tag={tag}>: automatically add the specified tag to the entity. Just to mirror the API of <ManagedEntities>, and to make the entity's tag more prominent.
  • <Entities entities={entities}>: render a list of entities, memoizing the inner children/render function.
  • <Entities archetype={...}>: render entities from a specific archetype (subscribing to the archetype, and memoizing the inner children/render function.)
    • A little unsure about this one, since it makes the API a little more fuzzy (there's a conflict between the entities and the archetype props), and in the end it's just a shortcut for eg. <Entities entities={useArchetype(...)}>, so the overall value is debatable.)
  • <ManagedEntities tag={...}> (formerly <Collection>): manage a collection of entities identified by a specific tag. Can automatically spawn an initial number of these entities by specifying the spawn (formerly initial) attribute, and will automatically destroy all entities of that tag when unmounting.
  • <Tag name="..."> component for adding a tag; just a wrapper around <Component name="..." value={Tag} />
    • but maybe wait before we've made up our mind regarding tags, see #52

[miniplex-react] Allow renaming the `entity` of the `as` component

In order to not get lost manipulating entity in every component hooked up to an as prop, it is convenient to rename the entity prop to a more specific name. For instance:

const Enemy = ({ entity: enemy }: { entity: Enemy }) => {
  // enemy.position.x = ...
  // enemy.position.y = ...
  // enemy.position.z = ...
}

const Enemies = <Entities in={bucket} as={Enemy} />

A minor improvement would be to be able to configure which prop name the entity will be injected in:

const Enemy = ({ enemy }: { enemy: Enemy }) => {
  // enemy.position.x = ...
  // enemy.position.y = ...
  // enemy.position.z = ...
}

const Enemies = <Entities in={bucket} as={Enemy} entityProp="enemy" />

RFC: returning/passing numerical IDs instead of entities

We've been having this focus on using objects and object identities everywhere, but let's try what it feels like when createEntity returns the entity's numerical ID, and addComponent, removeComponent and destroyEntity all expect numerical IDs (or maybe either a numerical ID or an entity?)

Potential benefits:

  • It might be easier to integrate a miniplex world with non-miniplex systems. A numerical ID would allow you to use this ID in array-based systems (or standalone TypedArrays, etc.).
  • It would probably cut down on a lot of the sanity checks we're currently doing. At the moment, when an entity is passed in, it could theoretically be "any" object, so we need to check a) if it's already been registered as an entity, b) if it's registered with this world, etc.

Potential complications:

  • It might make the entire library a little harder to grok
  • Are IDs "random", or do they represent the position of the entity within the world's entity array? If it's the latter, how will this be affected by entities being removed? Would we need to handle this differently (eg. by, instead of removing an element from the array, just nulling it?)

Checklist:

  • Refactor World to use numerical IDs
  • Remove RegisteredEntity type? We probably no longer need it
  • Adjust/improve tests
  • Change miniplex-react accordingly

miniplex-react causes useLayoutEffect warnings on SSR pages

Using miniplex-react on a page with SSR causes the following warning:

Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://reactjs.org/link/uselayouteffect-ssr for common fixes.

miniplex-react: 0.2.0

Support a component prop (or as prop) for ArchetypeEntities

It would be nice to have a prop to render directly a component for each entity, instead of using the children:

<ArchetypeEntities archetype="enemy" as={Enemy} />

Instead of:

<ArchetypeEntities archetype="enemy">
  <Enemy />
</ArchetypeEntities>

RFC: Tags as first-class citizens

Summary:

So far we've been modeling tags as normal components that always have a value of true, with miniplex providing both a Tag type and constant, both equal to true. But what if we made tags first-class citizens of the library?

  • Would make the API around tags clearer
  • and also a bit more flexible, since tags don't need to exist as components in the Entity type

Checklist:

  • Provide addTag and removeTag functions
  • Keep a list of the entity's tags in the __miniplex component
  • Index tagged entities (can we reuse Archetype here?)
  • Remove Tag constant and type

Future Packages

Drafting ideas for future packages we could provide.

miniplex-kit

Provides a collection of components and systems that are useful in many projects:

  • Entity age
  • Cooldowns/Timeouts
  • Auto destroy entities
  • Intervals

miniplex-fiber

Provides extra glue for use in react-three-fiber:

  • World Inspector UI
  • System Runner that integrates with r3f's upcoming useUpdate and provides some debugging UI

"Changed" iterable in archetype?

Firstly really nice work with this library!

I am in the process of looking for something new for our game battletabs.com and have been working my way through all the options listed here: https://github.com/noctjs/ecs-benchmark. Although I would love to get the best performance I can I just don't like the developer experience sacrifices that you have to make to make it as performant as an SoA implementation and quite frankly I dont think our game needs that level of ultra-performance.

One thing I was wondering about however was that bitecs has a concept of a "changed" query (https://github.com/NateTheGreatt/bitECS/blob/master/docs/INTRO.md#-query). This means that you handle entity additions and removals from an archetype within your main system loop rather than having some systems activated out-of-band.

What are your thoughts on this?

Is there a way Archetype onEntityRemoved listeners can still access the removed component?

There are scenarios where you would like to access the removed component in the onEntityRemoved handler of an archetype so you can perform some sort of cleanup when an entity is removed.

For example:

const archetype = this.world.archetype("mesh", "pick");

archetype.onEntityAdded.add((e) => {
  const action = new ExecuteCodeAction(ActionManager.OnPickTrigger, (event) => {
    // do something with this
  });
  
  // We save the action so we can unregister it when removed
  e.pick.action = action;
  e.mesh.actionManager.registerAction(action);
});

archetype.onEntityRemoved.add((e) => {  
    // oops, "pick" has been removed from the entity so we cannot access the action to "unlisten" it
  e.mesh.actionManager.unregisterAction(e.pick.action)
});

As the above example shows however the "pick" component no longer exists if it was removed from the entity (which caused it to be removed from this archetype) which is correct but it means that we are now no longer able to "unregister" the action and on actionManager on mesh.

Perhaps the removed components could be supplied to onEntityRemoved too? Or perhaps another signal onBeforeEntityRemoved?

[@miniplex/react] Provide an out of the box mechanism for ref capture components

Instead of doing this:

type Entity = {
  ...,
  transform: THREE.Object3D
}

<ECS.Entity>
  <ECS.Component name="transform">
    <mesh />
  </ECS.Component>
</ECS.Entity>

Let's bake support for captured refs into @miniplex/react! This could look like the following, where the first child of <Entity> is captured into the ref component by default:

type Entity = {
  ...,
  ref: THREE.Object3D
}

<ECS.Entity>
  <mesh />

  <ECS.Component name="health" value={100} />
</ECS.Entity>

Now the ref of the THREE.Mesh is captured into the ref component of the world's entity type, which would need to be typed accordingly (ie. ref: THREE.Object3D.)

For cases where the component name needs to be something different than ref, we could make this configurable through createReactAPI, eg.:

/* Set the default ref capture component to sceneObject */
const ECS = createReactAPI(world, { refComponent: "sceneObject" })

Potential Problems

  • We can't force Entity to only have a single child, because that would block the user from setting additional components through JSX. This means that the remaining heuristic we can apply here is to look for the first child, which feels a little bit iffy/easy to break/etc.

Improve API clarity of `createEntity`

createEntity should always only accept a single argument that must satisfy the world type. The current API allows for passing multiple partials, which is fun, but is a little too confusing (and requires us to loosen types a little too much.) For a component factory-centric approach, we can instead do this:

const entity = createEntity({
  ...position(0, 0),
  ...velocity(10, 0),
});

API Ergonomics Improvements for 0.8.0

A collection of potential tweaks and improvements to the Miniplex API. Miniplex puts DX first, so let's make this awesome! If you've used Miniplex in the past (you're very courageous!) and have opinions on this, please chime in.

React API factory still feels weird

When using Miniplex vanilla, you just create a new World() instance. The React integration module provides a createECS() function that returns an object containing the world alongside a collection of React hooks and components. This works, but the difference in usage feels off. (OTOH, it might no longer be an issue if we move the React glue into its own package.)

Result: the React integration has been moved into its own package, where we will continue to experiment.

API drift between createEntity and addComponent

createEntity will accept an entity object, while addComponent expects the name of the component and its data as separate arguments. This by itself is not a huge deal (and it matches the API of removeComponent, which expects a component name as an argument), but there may be some nice opportunities for using functions to compose (and initialize) entities and components that would be blocked by this API.

For example, we might provide some API tweaks that would allow you to do the following:

type IHealth = {
  health: {
    max: number
    current: number
  }
}

type IPosition = {
  position: {
    x: number,
    y: number
  }
}

type Entity = IEntity & Partial<IHealth & IPosition>

/* Convenience component factories */
const Health = (amount: number): IHealth => ({
  health: {
    max: amount,
    current: amount
  }
})

const Position = (x = 0, y = 0): IPosition => ({
  position: { x, y }
})

/* Pass a list of partial entity objects, which will then be assembled into one entity */
world.createEntity(Health(100), Position(5, -3))

This kind of stuff would be easy to add to createEntity, but immediately clash with the current addComponent signature.

Result: Improvements are being implemented in #9. The suggested API is already possible with today's implementation of createEntity: world.createEntity({ ...Position(), ...Health() })

Should <Collection> use something other than a Tag to identify its collection of entities?

The new <Collection> component serves a double purpose:

  • it reactively renders a collection of entities identified by a tag
  • when its optional initial prop is set to a non-zero value, it automatically spawns that number of entities, automatically tagged with tag, and renders them
  • (Bonus: it removes all of the collection's entities from the world when the component is unmounted.)

The idea here is that you have a single place in your application code that renders a collection of "things" (enemies, bullets, explosions) that are initially created as entities with just the tag identifying them, and then having the render function "mix in" new components. This is mostly intended as convenience glue to make game code expressive and easy to reason about.

Example:

<ECS.Collection tag="box" initial={10} memoize>
  {(entity) => (
    <>
      <ECS.Component name="transform">
        <mesh position={[Math.random() * 100, Math.random() * 100, Math.random() * 100]}>
          <boxGeometry />
          <meshStandardMaterial />
        </mesh>
      </ECS.Component>

      <ECS.Component name="autorotate" data={{ speed: Math.random() * 50 }} />
    </>
  )}
</ECS.Collection>

Identifying discrete collections of entity "types" by tag is a very opinionated approach. It has worked well so far, but maybe this mechanism can be refined further? Some ideas:

  • Just like Miniplex already exports a Tag type and constant for convenience (both of which are just equal to true), it could export a CollectionTag flavor. Mechanically it would be the same thing, but in a complex entity type, it might make the intent clearer.
  • ...?

Should <Collection> and <Entities> memoize by default?

The two components take a (currently) optional memoize prop that will make sure its children/render function will be converted into a memo'd component before being used. This is almost always what you want; consider that these two components will re-render every time an entity is added to or removed from "their" collection of entities; you wouldn't want to re-render 999 items when the 1000th is added.

It is very easy to forget this prop and accidentally ruin the performance of your application, so maybe the memoization should become the default. In this scenario, the question is how to express this in the component API. There would need to be a new prop, but what would be call it? unmemoized? rerender? active?

Result: Collection and Entities now memoize always. Let's find out how often users actually don't want non-memoizing behavior before we add code for it.

Provide addTag and removeTag?

Miniplex already has a built-in notion of Tag components, providing a Tag type and constant, both of which equal to true. In addition to addComponent and removeComponent, Miniplex could provide addTag and removeTag for convenience.

Result: not worth the increase in API surface (it's 4 new methods, considering we also need queued versions.)

Ideas for 2.0

Some stuff I'd like to try/do in 2.0:

Remove the internal __miniplex component

Done! No more __miniplex component in 2.0. Whatever the library needs to track about entities now happens in separate internal data structures.

Provide an update(entity, update)-style mutation to make the world work a little bit more like a store

Cancelled! I experimented with this. The pattern was fun, but too inviting for bad patterns that would cause too many index updates. I will revisit this at some point though. The new buckets now have a touch function that doesn't perform updates to the entity, but just notifies connected buckets that they should reevaluate it.

and relax addComponent/removeComponent a little. Maybe update becomes the main interaction in 2.0?

Done (minus the update bit)! The World class that inherits from Bucket now has addProperty, setProperty and removeProperty, all of which will no-op if instead of throwing errors in case there's nothing to do for them.

Rename Archetype to just Index. Allow function-based indices that track entities where a custom function returns true. Potentially have subclasses like ArchetypeIndex and FunctionIndex.

Done! Except the base class is called Bucket, and it's really good. Bucket.derive(predicate) creates a derived bucket that automatically receives all entities that match the given predicate. If the predicate provides a type guard, all entities in the bucket will inherit that type. Cool!

Play around with some ideas around "chained archetypes/indices". Treat these as essentially derived state.

Done! This is now possible using the .derive function mentioned above.

Rework queuing. It's a little cumbersome that the queue API currently reflects the interaction functions 1:1. Maybe we can instead have a generic world.queue(() => world.destroyEntity(entity)) etc.?

TODO

Even if this breaks with the usual ECS terminology, maybe Miniplex should call components "properties"? It matches Miniplex' stance that entities are just objects, and would resolve potential confusion between ECS components and React components.

Done! Miniplex now calls them properties, and the related React component is now <Property>. This is closer to familiar JS nomenclature and resolves the annoying collision in terminology between React components and ECS components.

Cancelled! Turns out that this merely shifted the terminology collision to properties, with awkward conversations about "putting property x on the entity's y property". Ouch! It's now back to "components".

Other ideas:

The constructor for World could take a factory function that creates default new entities (and infer types from that)

TODO

Instrumentation Ideas/Draft

I'm working on a Miniplex world inspector for my game:

Screen.Recording.2022-10-03.at.21.26.08.mov

It would be great to add some instrumentation features to Miniplex' World class to make it easier for tools like this to get useful instrumentation data.

Things that might be nice to measure:

  • Total number of entities
  • Total number of archetypes
  • Components of individual archetypes
  • Number of active subscribers on each archetype
  • Archetype "pressure" (rate of entities added or removed). This can be useful to identify archetypes that aren't really used much (but still infer an indexing cost every time an entity is added/removed/modified.)
  • Average time needed to add/index/remove entities

Introduce safe mode vs. unsafe mode

Miniplex currently (0.10.0) does a lot of sanity checks (like checking if an entity is actually managed by the world before mutating it). This is very useful while in development, but of course incurs a (slight) performance penalty. Let's introduce a toggle to allow the user to enable/disable these checks.

[miniplex-react] Updating Component data prop results in the owning entity being deleted and re-added

Package: [email protected]
Sandbox: https://codesandbox.io/s/entity-deleted-when-updating-component-cmk0og?file=/src/App.tsx


When re-rendering a Component component it calls world.removeComponent and then world.addComponent in an effect. During which it unexpectedly, at least as far as the signals are considered, deleting and then adding the entity to the world. This proves to be very bad when using the useArchetype hook as it re-renders a lot.

This also seems counter productive as I wouldn't have expected the component to be replaced, but instead mutated, after the first render. Modifying the code to this locally to set on initial render and mutate on subsequent ones fixes the problem but since it's replacing the reference it might be doing something dangerous according to the docs:

useIsomorphicLayoutEffect(() => {
  if (name in entity) {
    entity[name] = data;
  } else {
    world.addComponent(entity, name, data ?? (ref.current as any));
  }
}, [entity, name, data]);

useIsomorphicLayoutEffect(() => {
  return () => {
    if ("__miniplex" in entity) {
      world.removeComponent(entity, name);
    }
  };
}, [name, entity]);

There may be a bug in the achetype indexing. Regardless, I'd imagine skipping the need for indexing could be beneficial.

Dont throw an error when trying to remove component if it doesnt exist?

I usually am in favour of being more explicit with throwing errors when something is wrong but in this instance I wonder if it would be better just to carry on if the component doesnt exist rather than throwing an error?

https://github.com/hmans/miniplex/blob/main/packages/miniplex/src/World.ts#L207

Just makes things a little bit nicer to work with in user land (dont need to do a check before removing) and I cant see any instance where I really care that im trying to remove a non-existent component.

Please do let me know if im missing something tho.

overload add / remove/ set component to operate on an array of entities too

This is just a minor Quality of Life improvement..

It would be nice if addComponent, removeComponent and setComponent of world were able to operate on an array of entities in addition to just a single one.

I noticed that these functions return a boolean to indicate if the operation was successful or not.

https://github.com/hmans/miniplex/blob/changeset-release%2Fnext/packages/miniplex-core/src/World.ts#L63

If you supply an array of entities it would have to return an array of booleans.

RFC: Allow component name globbing in archetypes?

Summary:

Allowing archetype queries for foo*.

Pros:

  • This would allow for usage patterns where component names have a semantic hierarchy. For example, different enemy types in a game could be tagged with enemy:tank, enemy:bomber and enemy:infantry
  • It seems relatively straight forward to implement, with only very little performance cost.

Cons:

  • This might lead to patterns that are kind of considered antipatterns in the ECS world, since you may end up introducing a hierarchy into a system that is explicitly intended to avoid hierarchies.
  • In Typescript, archetypes are fully typed, and refactoring the typings to fully support this may prove very difficult.

Potential memory leak

When an entity gets destroyed, its world.entities slot is not being reused when a new entity is created.

This might a problem for shooter games where a lot of
projectile entities are frequently created and destroyed,
because the world.entries array will grow indefinitely.

Example code to illustrate what I mean:

const world = new World<Entity>();

for (let i = 0; i < 10; i++) {
    const entity = world.createEntity({
        /* ... */
    });

    if (i % 2 === 0) {
        world.destroyEntity(entity);
    }
}

console.log(world.entities); // [ null, Entity, null, Entity, null, Entity, null, Entity, ... ]

It would be nice if it could be have something like this:

const e1 = world.createEntity({}); // e1.__miniplex.id = 0
const e2 = world.createEntity({}); // e2.__miniplex.id = 1
const e3 = world.createEntity({}); // e3.__miniplex.id = 3

// Free up the world.entities[1] slot
world.destroyEntity(e2); 
console.log(world.entities); // [e1, null, e3]

const e4 = world.createEntity({}); // e4.__miniplex.id = 2 (uses the first free slot)
console.log(world.entities); // [e1, e4, e3]

_P.S.

I've wanted a library exactly like this one for years.
Tried to write my own multiple times,
with varying degrees of success.

And im so glad someone managed to write a nice, intuitive, typesafe ecs library.

Thank you_

[miniplex-react] Pass along any extra props of `Entities` to the `as` component

const Enemy = ({ entity, someExtraProp }: { entity: Entity, someExtraProp: number }) => {
  // ...
}

const Enemies = <Entities in={bucket} as={Enemy} someExtraProp={10} />

This is convenient to avoid having to create a context to pass along some data from the parent to the as component.

An example of actual use case is to pass along instanced meshes provided by Drei Merged:

const Enemy = ({ entity, BodyMesh, SwordMesh, ShieldMesh }) => (
  <group position={entity.position}>
    <BodyMesh />
    <SwordMesh />
    <ShieldMesh />
  </group>
)

const Enemies = 
  <Merged meshes={[body, sword, shield]}>
    {(BodyMesh, SwordMesh, ShieldMesh) => (
      <Entities
        in={bucket}
        as={Enemy}
        BodyMesh={BodyMesh}
        SwordMesh={SwordMesh}
        ShieldMesh={ShieldMesh} />
    )}
  </Merged>

RFC: Queueing Improvements

Instead of explicitly invoking the queued versions of mutator functions, let's change the API to the following:

world.queue(() => {
  world.createEntity(...)
  world.addComponent(...)
  world.removeComponent(...)
  /* etc. */
})
/* The queue will be flushed at the end of the function. */

It's important to have this work in a nested fashion, so I think the logic should work something like this:

  • World gains a new internal state 'queueLevel' that is 0 when no queue is active, and increased by world.queue(), and decreased when world.queue() finishes.
  • The mutator functions check if queueLevel is greater than 0. If it is, the mutations will be queued.
  • When world.queue() exits and the queueLevel is back to 0, the queue is actually flushed.

Problems with this:

  • The new mutator functions could be a little hard to type (since the synchronous version of createEntity would return an entity, but the queued version could not return anything), especially since it's not clear at the time of invocation if the requests are currently being queued or not.

Destructuring performance

I was wondering about the performance of destructuring within systems so I decided to run some tests:

import { World } from "miniplex";
import { Entity } from "./miniplex";

const count = 1_000_000;
const iterations = 10;

const world = new World<Entity>();

const positions = world.archetype("position");

const profile = <T>(name: string, fn: () => T) => {
  const before = Date.now();
  const result = fn();
  const after = Date.now();
  console.log(`${name} took ${after - before}ms`);
  return result;
};

const createEntities = () => {
  const entities: Entity[] = [];

  for (let i = 0; i < count; i++)
    entities.push(world.createEntity({ position: { x: 0, y: i, z: 0 }, age: i }));

  return entities;
};

it(`works`, () => {
  profile(`create`, createEntities);

  profile(`destructured`, () => {
    for (let iter = 0; iter < iterations; iter++)
      for (const { position } of positions.entities) position.x += Math.random();
  });

  profile(`not-destructured`, () => {
    for (let iter = 0; iter < iterations; iter++)
      for (const entity of positions.entities) entity.position.x += Math.random();
  });

  profile(`simple for`, () => {
    for (let iter = 0; iter < iterations; iter++)
      for (let i = 0; i < positions.entities.length; i++)
        positions.entities[i]!.position.x += Math.random();
  });
});

Results are:

  console.log
    create took 1794ms

  console.log
    destructured took 2363ms

  console.log
    not-destructured took 1502ms

  console.log
    simple for took 1473ms

So it looks like destructuring has quite a large impact.. having said that tho there could be some CPU vectorization / JIT'ing going on here that might cause oddness.

createEntity could return a stronger type

Because after you createEntity you guarantee that certain components are going to be there you could return the type from createEntity with "required" components.

For now I am hacking this with an "as", but it would be nice is that wasnt needed as it could be inferred from the input to createEntity:

export const createCell = ({
  resources,
  world
}: {
  resources: ResourceCache;
  world: GameWorld;
}) => {
  return world.createEntity({
    position: { x: 0, y: 0, z: 0 },
    mesh: { mesh: resources.cellMesh.clone() }
  }) as EntityWith<"position" | "mesh">;
};

where:

import { World } from "miniplex";
import { Camera, FreeCamera, Mesh } from "@babylonjs/core";

export interface Entity {
  position?: { x: number; y: number; z: number };
  velocity?: { x: number; y: number; z: number };
  mesh?: { mesh: Mesh };
  camera?: { camera: FreeCamera };
}

export type ComponentNames = keyof Entity;

export type EntityWith<TComponentNames extends ComponentNames> = Entity & Required<
  Pick<Entity, TComponentNames>
>;

so then we can do this:

image

Thoughts? Or is it better to always have the user assume at than entity's components might not be there?

bug: `world.clear()` forgets the world's archetypes, but should just clear them

When you invoke world.clear(), the world will only "forget" its known archetype objects, but it won't clear those first. If the user is keeping references to them around, they will still contain the entities of the world before clearing, and they will stop automatically updating.

Solution: in world.clear(), iterate through all known archetypes, and clear their entities. But keep the known archetypes around.

Rethink `miniplex` component

  • Some of the stuff in there is only for internal use
  • but at least the id is useful.
  • world might be useful, too, but it could lead to bad patterns.
  • We could split it - miniplex.id and miniplex.__internal.{archetypes, world}?
  • It should happen before 0.9.0

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

[2.0] Update entities, mark as dirty, reindex

Miniplex 2.0 supports predicate-based queries, which allows the user to also check for component values (instead of just presence/absence). For this to work, there also needs to be a mechanism that allows the user to mark an entity as dirty, so that it will be reindexed (because it's important that Miniplex mutates entities directly, instead of treating them like microstores.)

Suggested API (naming TBD):

/* Given a bucket containing entities with low health: */
const lowHealth = world.where((e) => e.health < 10);

/* Mutate the entity directly, as usual */
entity.health -= 10;

/* Mark the entity as dirty */
world.dirty(entity);

/* Regulary -- eg. once per tick -- reindex the world. At this point the entity might show up in the `lowHealth` bucket. */
world.flushDirty();

Changed events?

There's an opportunity here to implement an onEntityChanged event (or similar) that may help with #154. However, since entities are mutated directly (by design), it'll be difficult to impossible to provide the same sort of information (or even API fidelity) as eg. a reactive store solution (at least not without creating a potentially high number of new objects every frame.) There is a big potential for a significant divide between user expectations and what this library could realistically deliver.

0.8.0 Release Checklist

I'm preparing a 0.8.0 release for Miniplex. The library is now sufficiently stable that I want to start documenting and promoting it without putting an "experimental" disclaimer on everything, so this is going to be a significant release! This issue here serves as a checklist for things that need to happen before the release can be happen.

If you've used Miniplex in your own projects and have opinions on it (specifically usage ergonomics, which I want to be as great as we can make them), please chime in!

Checklist:

  • #7
  • Write additional tests, especially for the new React bits mentioned above
  • Extract the React bits into a separate package (eg. miniplex-react)

Documentation Checklist:

  • Update README with up to date usage information
  • Document the automatically generated id component
  • Document all the new React glue (specifically <Entities> and <Collection>)

Misc/Followup:

  • Update the WIP benchmark submissions (and finally submit them!)
  • Update the Boids demo to use the new version
  • Maybe provide a demo that is even more basic than the Boids demo

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.