Search Unity

Question Heterogenous components on an Entity (with Entities 1.0)?

Discussion in 'Entity Component System' started by TP-Fab, Mar 9, 2023.

  1. TP-Fab

    TP-Fab

    Joined:
    Jul 10, 2019
    Posts:
    37
    Hi there,

    Still exploring Entities 1.0 and I've been wondering whether a bit of refactoring I'm considering for our system still makes sense with this new version.

    Our game includes a relatively small number of entities (< 1000 characters, 1000-2000 items etc...) but they're all very heterogenous with a long list of optional components, meaning we have many archetypes of pretty big entities, worst possible thing for chunk usage.

    So, I've been considering for a while moving these components to their own entities, similar to the FSM architecture discussed a while ago on the Slack channel (can't remember who).

    Now, all these single-component entities will gravitate around their central character/item master entity, and will have to read/write quite a lot from them.

    ComponentLookup seems like the way to go, although it comes with its caveats already (lots of boilerplate code as this can require dozens of CLs per system, lots of sync points because they're shared between systems, and Updating the CL at the start of the loop isn't cost free).

    That still seems like a reasonable tradeof to switch from a couple entities per chunk to something much more reasonable in the hundreds.

    Does this still seem reasonable with Entities 1.0?

    Is there a better way to write onto the parent component than using a RW ComponentLookup?


    Thanks!
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    If two components are tightly coupled together and systems need to access both components at the same time, do not split them into separate entities. It can actually make performance worse than low chunk occupancy.

    Splitting entities is viable, but you need to do it in a way that minimizes the amount of component lookups between the entities and maximizes the chunk iteration on the split up entities.

    My heuristic for when it is valuable:
    magic_value = max(1, 10 * old_chunk_occupancy / new_chunk_occupancy)

    if (number_of_chunk_iteration_operations / number_of_lookup_operations > magic_value)
    :)
    else
    :(
     
    Thygrrr and TP-Fab like this.
  3. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,117
    Have you considered using EnableableComponents for the optional ones ?
     
  4. Thygrrr

    Thygrrr

    Joined:
    Sep 23, 2013
    Posts:
    700
    You can use data instead of composition.

    For example, have a field in an componentdata that is a FixedList of some type (e.g. personality traits, character stats, etc.), and iterate through this whenever evaluating.

    Have the state machine for a character brain or animation be fully encapsulated in one of or few componentdatas, then update state in data instead of in structural changes.
     
    UniqueCode likes this.
  5. TP-Fab

    TP-Fab

    Joined:
    Jul 10, 2019
    Posts:
    37
    yes, however with the amount of types (and they're dynamic buffers) i'd most likely end up with entities around 8kB (so 2 entities per chunk) :(
     
    Opeth001 likes this.
  6. TP-Fab

    TP-Fab

    Joined:
    Jul 10, 2019
    Posts:
    37
    we already have that for the relevant data. Some of them as fixed size arrays in a component, some of them as dynamic buffers.
     
  7. TP-Fab

    TP-Fab

    Joined:
    Jul 10, 2019
    Posts:
    37
    Interesting stuff. Note slave components wouldn't need to access each other (in general), only the master components. So, if i can't decouple slaves from master everyone stays together. :(

    Like mentioned above, I could reduce fragmentation by having all slaves components on every master entity, but that'd make huge entities.

    I'll try working out your heuristic, by chunk iteration do you count each entity iteration, or only whenever going to another chunk?
     
  8. Thygrrr

    Thygrrr

    Joined:
    Sep 23, 2013
    Posts:
    700
    Yeah you gotta be conservative with what data you really NEED to have in the ECS.
    I put all my economy stuff into (sometimes) jobified, non Entities code, and ECS only holds 32 bit keys for those data structures.

    Notably, splitting up into separate entities *might* indeed be good for you if you have as much as ~8k data per entity, because execution will benefit from multiple cache lines being used if for one "semantic" entity, you need to pull that from 20 chunks instead of 1, so and cache utilization is spread out better.
     
    TP-Fab likes this.
  9. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    You might need to start providing more concrete examples, because unless your slave components are really fat and your master components are really skinning, I don't think you gain much here. You may be better off breaking things apart a different way.

    Per component access within a job. So if there are multiple lookups per entity, you count each one. And if there are multiple components in the chunk you access, you count each one as well.
     
    TP-Fab likes this.
  10. TP-Fab

    TP-Fab

    Joined:
    Jul 10, 2019
    Posts:
    37
    The worst example that comes to mind is an FSM :

    Common parts
    - Base data for the FSM to exist (very skinny)
    - A few dynamic buffers of blackboard variables (4-5 types: float, int, bool, Entity... and default buffer size set around 10)

    "Optional" bits
    A list of actions scattered around 200+ types in my case (wait, move, randomize...)
    - Each of them needing to be in dynamic buffers as you'll probably have more than one of each in each state
    - Each also need a common type to allow some sort of control (sequential/parallel trigger, looping etc...) which is updated at the start of the frame depending on where we stand in the state (and for state transition)

    So yes overall, skinny master entity and "fat sum" of optional components, leading to the "one entity per action" approach commonly selected afaik.

    For any other case less obvious than this one, your heuristic is interesting and I'll try some calculation in the concrete cases that make me hesitate, knowing that there's nothing better apparently than the RW ComponentLookup approach.
     
  11. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    An entity per action makes sense if an action is at least 64 bytes in size (ideally 120). But otherwise, you are wasting chunk memory due to too small of entities.

    For FSMs, I strongly recommend if possible to bake all static data related to them inside BlobAssets. DMotion does this, and it works very well, while drastically decreasing the size of data that needs to live in chunks, and reducing instantiation and structural change times (blobs don't need to be deep-copied). Keeping dynamic buffers out of chunks can also be really helpful.
     
  12. TP-Fab

    TP-Fab

    Joined:
    Jul 10, 2019
    Posts:
    37
    By "entity per action" i mean per instance of the action. So if 100 characters are running ActionWait, there are 100 entities :) (so not much more chunk waste than any other entity archetype in the project)

    Very good point about BlobAssets, thanks. Instance-specific data couldn't be put into common references like this, but it'd certainly be a huge chunk space saver. How do these fare on memory access though? Won't reading these break the contiguous memory read?

    Re. dynamic buffer, what you suggest is to declare a tiny InternalBufferCapacity so they're moved out of the chunk? I'd guess the question would be to find the right balance between chunk count & heap access, right?
     
  13. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    If you are iterating through all your data in every blob asset, then you are hitting cache for all but the first read. As long as your blobs aren't tiny, that's usually just as good. If you aren't iterating through all your data, then you weren't iterating through all your data in a chunk either. So for anything complex, blobs have strong advantages, as they improve chunk capacity, and they offer the ability for multiple entities to share the same blob and improve temporal cache coherency. And that's not to mention overall memory size and structural change benefits.

    Yes. Any buffer that doesn't exactly use its non-zero InternalBufferCapacity is wasting chunk space. You do have to find the right balance, but out-of-chunk is faster more often than you would think.
     
    TP-Fab likes this.
  14. TP-Fab

    TP-Fab

    Joined:
    Jul 10, 2019
    Posts:
    37
    Nice, thanks!

    Didn't realize blobs would change the iteration order by grouping entities using the same blob, but it does make sense indeed. So here too the matter is to find the proper balance, avoiding a combination of blob assets fragments the data beyond what non-blob data wastes in chunk space.
     
  15. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,269
    No. blobs don't egroup entities in chunks. But if multiple entities have references to the same blob, then there's a much higher chance for each entity that the blob is already in cache from a previous entity. That's all I meant.
     
    TP-Fab likes this.