rlipsc / polymorph

A fast and frugal entity-component-system library with a focus on code generation and compile time optimisation.
Apache License 2.0
140 stars 7 forks source link

Concept of component manager? #3

Closed zacharycarter closed 2 years ago

zacharycarter commented 4 years ago

Is there any concept of something like a component manager in polymorph? If I want to iterate over all of the instances of component X - not the entities which have component X via a system - is there any way to do this?

zacharycarter commented 4 years ago

I just saw forAllComponents which I think will work - but if there's a better way to achieve this type of functionality, I'm all ears :) Great library by the way!

rlipsc commented 4 years ago

Is there any concept of something like a component manager in polymorph?

There's no component manager in the traditional sense as Polymorph shifts this work to compile-time with static system state changes.

The design is orientated around systems holding their slice of the entity-component state and the management aspect is performed by the state changes themselves (such as newEntityWith, addComponent, removeComponent, and delete), which update systems directly. All state change operations are generated at compile-time using the system definitions, so systems are core to how state is managed.

Systems are essentially update driven "work lists" rather than queries to a manager.

For example if there's a component that's only used by one system, adding this component to an entity generates a single static update just for that system. This means state updates are generally very efficient as only the relevant systems are being touched and there's no run-time cost for determining what needs updating. It also means systems have no run-time setup cost; they just go through their work list. This is handy for performance but also lets you design a really tiny ECS with one or two components for say, embedded work - ultimately the code boils down to simple arrays and loops. You can always view all the generated code by passing -d:debugComponentGeneration, which will create a log file at run-time.

There's no mechanism for gathering entities from arbitrary sets of components. To do this you'd make a system which would then be updated upon state change.

If I want to iterate over all of the instances of component X - not the entities which have component X via a system - is there any way to do this?

forAllComponents ComponentX does indeed sound like what you're looking for. This will iterate over the component storage and there's no way to get a reference to the component's entity.

if there's a better way to achieve this type of functionality, I'm all ears :)

In general the library is built assuming any operations with components is done with systems, so most of the time they're the most ergonomic solution even for single components or one-off operations. Systems also have the advantage of a set order of execution, as the order of component reads and writes can be really important with ECS. Notable exceptions to using systems is things like serialisation which operate "outside" of the ECS, doesn't perform logic, and may operate on the state as a whole.

Systems also offer quite a bit of flexibility over manually iterating components, for example you can make them run at intervals whilst still maintaining a set system run order:

makeSystemOpts("displayComp", [ComponentX], ECSSysOptions(timings: stRunEvery)):
  all: echo item.componentX

sysDisplayComp.runEvery = 1.0 / 30.0

For one-off manual operations a pattern I sometimes use is with the system's paused flag:

makeSystem("runOnce", [ComponentX]):
  init: sys.paused = true
  all: echo "Some code to do the one off thing with ComponentX"
  finish: sys.paused = true

When you want the system to run, setting sysRunOnce.paused = false causes the system to run in the defined order next tick, then pause itself.

You can see an example of this in the Polymers particle demo here. The releaseGrabbed system is manually activated when the user releases the mouse button and causes the Grabbed tag to be replaced with a different tag before pausing itself again. Incidentally the Polymers library is probably a good place to see examples and/or some useful components you can import and use if you haven't already checked it out.

Having described the above, it's worth noting that systems do have some minor overhead in terms of (by default) an indirection and the storage for this indirection, which forAllComponents does not. Both these overheads can be removed however if the system owns its components (which means the system actually stores the component's full data):

defineSystemOwner("forAllX", [ComponentX], [ComponentX], defaultSystemOptions)

makeSystemBody("forAllX"):
  all:
    echo item.componentX # `componentX` is the source type.

sysForAllX is now the storage for ComponentX so is effectively the same as forAllComponents ComponentX. Only one system can own an individual component type, but systems can have multiple owned components and be mixed between owned and not owned. For now this comes with a big caveat: external reference such as let myRef = entity.fetchComponent ComponentX may be invalidated when a state change deletes the component from an owned system.

On my TODO list is to make systems with only one component automatically iterate over the storage of the component like forAllComponents, avoiding all indirection and not requiring extra storage. This will happen automatically behind the scenes, and will mean systems with only one component are effectively the same as using forAllComponents ComponentX.

Great library by the way!

Cheers! I'm using it quite intensively for a few projects, including a game world with a need for running a few tens of thousands of active entities with a high dynamic requirement (procgen world, weapons, and creatures) so it's being "dogfooded". Let me know if you have any more queries or issues! :)