hmans / miniplex Goto Github PK
View Code? Open in Web Editor NEWA ๐ฉโ๐ป developer-friendly entity management system for ๐น games and similarly demanding applications, based on ๐ ECS architecture.
License: MIT License
A ๐ฉโ๐ป developer-friendly entity management system for ๐น games and similarly demanding applications, based on ๐ ECS architecture.
License: MIT License
Summary:
Allowing archetype queries for foo*
.
Pros:
enemy:tank
, enemy:bomber
and enemy:infantry
Cons:
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.)
/* 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();
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.
...instead of fetching it from Google (GDPR problems inbound...)
addComponent(entity, name, value)
addComponents(entity, ...partialEntity[])
(addComponents
could also be called extendEntity
maybe?)
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.
When running in strict mode you'll get this error: Tried to add components to an entity that is not managed by this world.
. It's most likely from effects double rendering.
Broken sandbox: https://codesandbox.io/s/strict-mode-doesnt-work-spxtev?file=/src/index.tsx
Working sandbox: https://codesandbox.io/s/entity-deleted-when-updating-component-cmk0og?file=/src/index.tsx
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!
miniplex-react
)id
component<Entities>
and <Collection>
)It always returns an archetype representing Entity
, and not a subset with the requested components.
When you queue 1000 entities to be added to an archetype and then flush the queue, this will invoke the update signal a 1000 times. Let's change that :)
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.
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_
eg.
queue(() => world.remove(entity))
/* Later */
queue.flush()
id
is useful.world
might be useful, too, but it could lead to bad patterns.miniplex.id
and miniplex.__internal.{archetypes, world}
?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.
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.
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() })
<Collection>
use something other than a Tag to identify its collection of entities?The new <Collection>
component serves a double purpose:
tag
initial
prop is set to a non-zero value, it automatically spawns that number of entities, automatically tagged with tag
, and renders themThe 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:
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.<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.
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.)
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?
Drafting ideas for future packages we could provide.
Provides a collection of components and systems that are useful in many projects:
Provides extra glue for use in react-three-fiber:
useUpdate
and provides some debugging UICan you add some examples to the repo?
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.
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.queueLevel
is greater than 0. If it is, the mutations will be queued.world.queue()
exits and the queueLevel is back to 0, the queue is actually flushed.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.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?
Checklist:
addTag
and removeTag
functions__miniplex
componentArchetype
here?)world.archetype
without
where
iterator generatorarchetype
, with
, without
and where
world.with("foo").without("bar")
vs. world.without("bar").with("foo")
)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:
Potential complications:
Checklist:
World
to use numerical IDsRegisteredEntity
type? We probably no longer need itminiplex-react
accordinglyInstead 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" })
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.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.
<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>
, ...?
Let's try to describe all the things the user would want to do with miniplex-react.
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.
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>
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>
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.
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>
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?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.
<ManagedEntities>
๐คก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()
useCurrentEntity
is a bit iffy, and I can already see users confusing this with useEntity
. Is there a better name?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
?
The React demo app has some experimental code around segmented buckets, and it'd be cool to bake these into the library somehow.
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.
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>
And document in the README that for
with numerical index access should be used if performance becomes a concern.
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
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>
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:
Thoughts? Or is it better to always have the user assume at than entity's components might not be there?
It's confusing, and the user can just do this instead:
const [first] = archetype
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.
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" />
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),
});
I'm working on a Miniplex world inspector for my game:
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:
How does miniplex fare with getters/setters on entity objects? Are there any cool patterns we could use this for? Let's find out!
<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.)
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} />
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.
If you supply an array of entities it would have to return an array of booleans.
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. Maybeupdate
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 justIndex
. Allow function-based indices that track entities where a custom function returns true. Potentially have subclasses likeArchetypeIndex
andFunctionIndex
.
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".
The constructor for World could take a factory function that creates default new entities (and infer types from that)
TODO
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.