Code Monkey home page Code Monkey logo

miniplex's People

Contributors

benwest avatar dependabot[bot] avatar github-actions[bot] avatar hamzakubba avatar hmans avatar jure avatar verekia avatar voces avatar

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  avatar

miniplex's Issues

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.

[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.

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.

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

[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.

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_

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

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

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

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

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.

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.

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

[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

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] 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.

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?

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?

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.

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>

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

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

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?

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.

[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" />

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),
});

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

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

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.

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

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.

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.