3mcd/javelin

Best way to refactor attached and detached, since they seem to be removed in 1.0 release

Closed this issue ยท 4 comments

Hi! I was trying to refactor my project to support the new 1.0 alpha release. Currently I'm a bit confused how to refactor something like this that uses the attached and detached handlers:
const actorsCreated = query(attached(Actor), Transform)
const actorsDestroyed = query(detached(Actor), Transform)

Could you provide some insight on this matter that what I should do to replace those? ๐Ÿ™‚

Also, thank you for the awesome library and the time and effort you have put into it! I really think this is the best JavaScript ECS implementation there is! ๐ŸŽ‰๐Ÿš€

Kind of answering my own question: I guess the new approach is to utilize the useMonitor as such:

const actors = query(Actor, Transform)
useMonitor(
  actors,
  (entity, [component1, component2]) => { /* do something like initiate to world*/ },
  (entity, [component1, component2]) => { /* do something like remove from world*/ }
)

This rises some questions as for example, I previously used a system to handle all this and now my actual actorSystem has nothing in it... ๐Ÿ˜† Should these monitors be in their own files in a bigger project or...

3mcd commented

Hey @panuchka, this is a great question.

You're exactly right: useMonitor is planned to become the new way of handling attach/detach events. Although I've become fairly comfortable with the API, I'll admit its still a bit experimental and I'm totally open for suggestions.

I made the decision to remove attached and detached filters due to their implications on component storage and iteration. Each component needed a status that it transitioned between (orphaned, attached, detached), which complicated the data model a bit. Statuses also added small amount of overhead to iteration since filter-less queries needed to check if a component's status was "attached" to include the entity in its results.

useMonitor is expected to be executed in the context of a system, e.g.

const physics = () => {
  useMonitor(
    actors,
    (e, [a, t]) => simulation.createRigidBody(...),
    (e) => simulation.destroyRigidBody(...),
  )
}

If you feel that the useMonitor callbacks have too much responsibility now, we could explore a different interface which returns iterable collections of entity/component tuples that transitioned into/out of the query, e.g.

const [enter, exit] = useMonitor(actors)
for (const [entity, [component1, component2]] of enter) {
  ...
}

The collections could then be consumed by systems which handle them, rather than requiring the side-effect logic be contained in the callbacks.

As always I am totally open to suggestions and actively looking for feedback. Thanks so much for the kind words! It makes the work I put into Javelin feel all the more rewarding.

Thank you for a quick reply! I managed to figure out that useMonitors are supposed to live in the systems by digging your examples in the next branch ๐Ÿ˜„

I thought a while about the different approaches of useMonitor and it is, in my opinion, much clearer than the previous attached/detached functionality. The two styles you presented with useMonitor both have their ups and downs I think:

  • Using useMonitor in its current form differs a bit from the regular flow of having to loop things yourself, but it is kind of nice that you don't have to do it yourself and can just pass a interation callback to it.
  • Using useMonitor when it returns just an iterable collections kind of feels even more clearer to me, I think. But then you have to loop it yourself ๐Ÿ™‚ I think I would prefer this one though. It kind of gives me the React hooks / Vue composition vibes.

All in all whichever you choose will be a good decision in the end ๐Ÿ‘

3mcd commented

@panuchka I just set up a community Discord for Javelin if you'd like to join: https://discord.gg/AbEWH3taWU.