Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Ergonomics Improvements for 0.8.0 #7

Closed
Tracked by #6
hmans opened this issue Mar 24, 2022 · 4 comments
Closed
Tracked by #6

API Ergonomics Improvements for 0.8.0 #7

hmans opened this issue Mar 24, 2022 · 4 comments
Assignees

Comments

@hmans
Copy link
Owner

hmans commented Mar 24, 2022

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

@hmans hmans mentioned this issue Mar 24, 2022
9 tasks
@hmans hmans self-assigned this Mar 24, 2022
@akdjr
Copy link

akdjr commented Mar 24, 2022

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

Would treating world the same way you treat entities be a possibility? Basically a context and provider for the created singleton world and then a useWorld hook to access it?

@hmans
Copy link
Owner Author

hmans commented Mar 25, 2022

Would treating world the same way you treat entities be a possibility? Basically a context and provider for the created singleton world and then a useWorld hook to access it?

I specifically designed the React bits to create per-world instances of hooks and components for (at least) two reasons:

  1. Miniplex can be used both inside projects as well as libraries, with separate worlds for each. In fact, projects are encouraged to even have several separate worlds (depending on their use case). Within a React component tree, the risk of the contexts of two of these worlds overlapping is too high (since useContext will use the nearest context it finds.)

  2. In Typescript, createWorld is generic, and it will use the passed type (which describes the world's entity structure) to all the hooks and components it returns, allowing for type checking/hinting/autocomplete everywhere, which would simply not be possible with a plain context-based setup (since the miniplex world in use would only be determined at runtime.)

One alternative API that I have been thinking about was to provide an uncurried, global version of, for example, the useArchetype hook, that accepts a world as its first parameter:

const { entities } = useArchetype(world, "position", "velocity")

And then leave it to the user to curry it with their preferred world, but the end result would in practice be extremely close to what we have in miniplex/react today, just with more code.

I hope this makes sense?

@akdjr
Copy link

akdjr commented Mar 29, 2022

Yes! I knew I was missing something obvious and the separate overlapping worlds was the key thing I was missing there.

The uncurried, global version looks interesting, but yeah, it doesn't seem all that different than the current implementation.

@hmans hmans mentioned this issue Mar 30, 2022
2 tasks
@hmans
Copy link
Owner Author

hmans commented Mar 31, 2022

Most of this has now found its way into miniplex and miniplex-react, so I'm closing this for now.

@hmans hmans closed this as completed Mar 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants