Search Unity

Struggling with thinking in ECS

Discussion in 'Entity Component System' started by jsmilovic, Jun 2, 2021.

  1. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    I've got an existing codebase for a game I made before I came across ECS. However, I did adopt some bits and pieces of ECS unwittingly.

    ---- Current System ----
    I have a large StateTree, mostly just a collection of "Entity Containers," basically Dictionary<Guid, T>. Each "entity" has a unique identifier (UUID). There are no methods/behaviours on these entities. There is no nesting of data, but unlike ECS, I do have properties on every entity. I have no distinction explicitly for a Component. I handle relationships via an "Engine" I wrote that handles storing lists of relationships by hash keys for fast lookup. I don't have systems. I dispatch Events that get routed to the proper method that handles it (Technically, this is a redux style with reducers)

    This has worked pretty well, I have done a lot of custom parallel code, and it's fast, but I think with ECS, it could be much, much faster. I am struggling, though, when it comes to understanding an Entity vs. a Component when sometimes they seem to overlap for me. I'm not sure if it's the vocabulary of how I've structured my game or what, but I'm hoping for some help "Thinking in ECS" :).

    ---- A quick case study example for my election simulation game. ----

    I have Voters who have opinions on Candidates regarding Issues. So the way I have it structured now is Voters "own" VoterIssues, which "own" VoterIssueCandidates, which have approval ratings. When I say "own," I mean an object type is indexed by something (A parent ID usually) for lookup. The player can run Ads on issues, which can sway a Voter's Candidate Approval, for an Issue. We can run Polls to see how a candidate is performing in a state, broken down by the issues. Issues also own things like Topics, but trying to keep the example smaller!

    I struggle to understand how to create hierarchical type content such as this in an ECS mindset. I would tend to say a Voter is an Entity, as is an Ad. Ads are owned by Actors (Player, Candidate). How do we represent this kind of hierarchal content in ECS? I've seen suggestions of a Relationship Component, though I'm not 100% on how that works.

    Also, Ads have cost and reference an Issue, so is Issue an Entity or a component? Voters own VoterIssues which are containers for results for each candidate approval on an Issue... and it all kind of spins out to where I have no idea what my components are and what my entities are in this "simple" example, let alone how to connect them. It feels like the versatility of some of my usage of things like Issues makes it difficult to understand what is the nature of it in ECS.
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    An entity is an ID, or a "key", and components are values associated with those keys. So if the data is exclusively associated with a particular key, it should be a component of that entity. Otherwise, it should be on a separate entity.

    With all that said, have you considered trying to migrate your architecture to DOTS without ECS? It may be a better stepping stone into more performance. And to clarify, that would mean converting your dictionaries into NativeHashMaps and using IJob.Run() with [BurstCompile].
     
  3. RoughSpaghetti3211

    RoughSpaghetti3211

    Joined:
    Aug 11, 2015
    Posts:
    1,709
    Someone posted this a while back, I took a snapshot since I found it super useful helping me to make sense of ECS. It’s probably over simplified but it works for me.

    A29C4001-1DFF-4390-BC53-8D748C1601E8.jpeg
     
    Per-Morten, jsmilovic and Lo-renzo like this.
  4. RecursiveEclipse

    RecursiveEclipse

    Joined:
    Sep 6, 2018
    Posts:
    298
    That was me :). It is a bit oversimplified, but it helps if starting from scratch with SQL experience to understand the architecture. Though, in SQL, tables that have the same column names with the same purpose would be bad design. In this context, you can represent a hierarchy in ECS using entities, you'd find it's similar to what you might find in basic SQL, but you can also use Blob Assets if the data/nodes are unchanging.

    In my game I have "species description" entities, over time I've been thinking about converting them entirely to blob assets, since the data doesn't change, I have tight memory considerations, and they don't need processing. I suppose this is similar to your Issues case.

    I imagine you could just have an Owner component(or Actor, whatever you want to name it) and that would reference the Entity of the candidate, which could be the player or an AI. When a voter sees the ad their opinion(could be an IBufferElementData with <Entity, Float> for each <Candidate, Opinion>) would decrease or increase.

    But if you already have the architecture, Burst + Jobs might get you close enough.
     
    Last edited: Jun 3, 2021
  5. varnon

    varnon

    Joined:
    Jan 14, 2017
    Posts:
    52
    I'll third just trying burst jobs. The performance boost is great from that alone.
     
    Krajca and jsmilovic like this.
  6. calabi

    calabi

    Joined:
    Oct 29, 2009
    Posts:
    232
    In my case I try not to think about entities until later if at all. The entity is kind of irrelevant its just a reference or id to the data its not important. I think about what the data is specifically and whether it would be best stored as an Icomponentdata or Ibuffer or even a Blob asset or maybe even none of that. I sometimes even change what type it is half way through making the program. You might think that all the data needs to be their and on that entity, or close by it but it doesn't it just needs to exist somewhere in the program and be accesible. You could have an object like a car made up of a lot of separate permanent components and some of transitory and some singletons.

    I havent really done much with heirarchies and child parent relationships yet though, or maybe I have I just look at them as references, this entity data needs a reference to this other entity data.
     
  7. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    @DreamingImLatios Ah, thank you. That does help lend some clarity! I had not considered that as an option. I did take a stab at doing it though I ran into some issues pretty quickly. I was not using structs; I was using classes, which NativeHashMaps don't support. Overall it's been very informative because it started me on a line of questions like why does the NativeHashMap require structs, understanding the memory layout that it provides and how it complements the DOTS goals of performance! I still have a lot to learn on DOTS/ECS but certainly pointing out the NativeHashMaps was a nice "pointer" :)
     
  8. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    Thanks, this has been helpful! As it stands, I do have database experience, so the metaphor is useful.

    Ah, nice, I hadn't even seen the blob, and yes reducing my memory footprint anyway possible is great. I simulate 51,000 virtual voters with all these amongst other things, so I can run very high on memory requirements!

    I think the description you've given of an Owner component that references the Entity of a candidate is helping fill in a block. I was really thinking of components and entities as decoupled to the point it didn't quite feel right to understand a component can reference an entity, but hearing it makes sense!

    The Dynamic Buffer makes sense, storing a relationship like that on a component that attaches to a Voter entity. That's really helpful. I think the thing that still trips me up a bit is the depth of the relationship. A component with a dynamic buffer of candidate/opinion makes sense, but a voter has such an opinion for each issue in practice. So I suppose I could make a dynamic buffer of <Entity, Component> where the Entity is an issue ref. The Component is itself a dynamic buffer of candidate/opinions, but am I packing too much directly in hierarchically that way, I wonder. I guess I don't really need to operate on voter opinions on issues outside of the issues themselves, so maybe that kind of nesting is ok, but these are the bits that trip me up. Is it ok to have a dynamic buffer reference components, or am I defeating the purpose here?

    Yeah, I'm making my way. I've converted all of my classes to structs and am changing the code that needs to change to function w/ structs and I'll see how the Burst + Jobs work and go from there!
     
  9. RecursiveEclipse

    RecursiveEclipse

    Joined:
    Sep 6, 2018
    Posts:
    298
    You can't store a list in a component(well, you kinda can if you use UnsafeList or pointers). This is also true for the native containers, only blittable types, usually structs that only contain structs. And you can't use nested native containers either, because the atomic safety contained in them is not blittable. You can (kinda) store a component but this is not a reference to another component on an entity, as in a reference type, IComponentData mainly only serves as a marker/safety, the underlying type is just a struct whose values are copied when assigned. A struct with IBufferElementData is technically not a list on it's own.

    More to your point, my given example isn't the only component you'd need. Imagine:

    Entity (Voter)
    • Issues (Buffer of issues(ID/Entity) the voter cares about, if this is shared by large unchanging "voter blocks" and not randomly generated you could just use an IComponentData with a blob reference that has a list of these)
    • CandidateOpinions (Buffer of <Entity, float>)

    Ad (Entity)
    • Cost (IComponentData, or maybe it's a one time fee and you don't need the state)
    • TimeRemaining (IComponentData)
    • Issue (ID/Entity)
    • ...
    Issue (I think it comes down to if there is some state about this that gets changed over the game, you should generally prefer Entities if the above is true, otherwise if this is purely unchanging data stick to blobs.)

    When you do a ForEach you'll query for Voter entities using Issues, and CandidateOpinions. In a separate query you get entities that are ads. In the foreach you'd iterate over those and compare the ads issue ID with the issue IDs the voter cares about, if there is a match they can react through their CandidateOpinions.
     
    Last edited: Jun 4, 2021
  10. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    Thanks for all the great advice! I spent a chunk of the weekend re-writing a bunch of my underlying code to be more jobs friendly. Initially, it just involved changing my classes over to structs and reworking some surrounding code, but I've been moving over to ECS for specific parts. It was really the strings that did it, realizing strings aren't blittable and therefore not going to work as they are led me down the path of FixedStrings until I just ended up making some components.

    Ah, this information was really helpful. I didn't really realize an IBufferElement and, by extension, a DynamicBuffer were essentially Component like constructs. In fact, I was creating IComponentData structs w/ DynamicBuffer props on them, then adding those to my Entities rather than defining structs implementing IBufferElementData and using AddBuffer/GetBuffer with the Entities. It was definitely one of those "Free your mind" moments :)

    Yeah, the issues don't really change. Well, they become available throughout the game as the scenario goes on but once an issue is revealed it's really just a name and functions as a "container" for topics/arguments (for hierarchical menus such as taking out ads), a grouping mechanism for opinions, things like that, in and of itself it's pretty much stateless.

    I'm thinking I can use a blob for each Issue and maybe just keep a general entity w/ an IBufferElement type containing a blob reference and I can use this general entity to keep track of what Issues are 'available'. Similarly, use that blob reference on any other Components/BufferElements require an association with an Issue.

    Wow, thanks for spelling it out, this has been really helpful for me not just to solve my immediate problem, but having such an extensive concrete example really helps me understand better how to generalize the techniques. Thank you for that!
     
  11. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    As I'm rewriting chunks of my game as Entities and Components, I'm wondering about some architectural "Best Practices" if anyone is still checking this thread :)

    So I have 51 states in my game (D.C.) and 1,000 virtual voters in each state. My initial instinct is to create an
    IBufferElementData
    of type
    Entity 
    and add it as a
    buffer 
    to each
    state
    to hold the state's 1,000
    Voter Entity references
    Well, I'm not sure if they are references or copies of the entities actually...

    At any rate, then I started wondering if I'm thinking too hierarchically, and perhaps I should be putting "Tags" (empty IComponentData) on my Voter Entities to tie them to the 51 states that way. I don't want to create 51 more classes because I'm lazy, and also, it ties the game more to the US and Federal Elections specifically than I'd prefer. So I was thinking of making Component types through reflection (e.g. {StateName}StateVoter).

    I know reflection is "slow", but my game is turn-based, and I could use some tricks, i.e., a dictionary that stores the StateTagComponents by the State name or something to keep the reflection usage down. The effort turns me away from this one a bit ( https://stackoverflow.com/a/3862241/2356770 ) but it feels right to me in terms of flexible architecture. I get the feeling tagging should be very granular since querying can be as broad or specific as you like.

    Alternatively, I could just put a flag on my voter components or an enum value or something indicating which state it's part of and just go through all 51k to find the proper ones.

    Anyway, true to the title "Struggling with Thinking in ECS," I don't feel like I know enough to make an educated decision between large entity buffers, tagging/filtering, and marking. I do like tagging, though. Otherwise, I have to tie my voters into various other Buffers, e.g., a National buffer if I want to do things on a national level. So I start leaning towards tags but hoping for a sanity check :)

    Thanks!
     
  12. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    Try shared components (ISharedComponentData) instead. They behave similar to tags, but are unique per value rather than type, which is precisely your use case.
     
    jsmilovic likes this.
  13. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    That's really helpful and definitely fills in another piece of the architecture puzzle! I had kind of skipped over that because I had some mistaken assumption that SharedComponentData was essentially "static" data shared across all Entities in an archetype. Seems like that's more what a BlobAsset is

    It was really easy to write that code. For anyone who comes across this thread
    Code (CSharp):
    1.  
    2. var stateVoterQuery = entityManager.CreateEntityQuery(typeof(VoterTagComponent), typeof(StateVoterSharedComponent));
    3.                 stateVoterQuery.AddSharedComponentFilter(new StateVoterSharedComponent() { StateCode = state.StateCode });
    4.                 var stateVoters = stateVoterQuery.ToEntityArray(Unity.Collections.Allocator.Temp);
    5.  
    was some easy code to query and filter these tagged entity types by a division within their archetype.
     
  14. RecursiveEclipse

    RecursiveEclipse

    Joined:
    Sep 6, 2018
    Posts:
    298
    It kinda is for static data by convention because of how they're handled. If you change a shared component, all the components will be copied and moved to another chunk or a new one that has the same shared component values(for all shared components), which is pretty slow. Each shared component on an entity usually means another chunk split and there is a risk of them being too small to jobify efficiently. Also there is limited/inconvenient API in a job context because they can also hold managed objects.

    ISharedComponentData's name is slightly misleading to it's purpose. It's more of a way to split/filter chunks that have the same component value and can contain managed objects. The data is stored once per chunk, hence "shared". RenderMesh is a very common shared component, I'd assume it's useful to split chunks by material and mesh for batching in rendering.

    But your case sounds like a reasonable one for it.
     
    Last edited: Jun 12, 2021
  15. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    Thanks for the clarification, and that makes a lot of sense. When I'd initially discarded Shared components I had thought Static more in the sense of a static value on a class. Set data on one shared component and all components get the value. Not so much one property whose value ought not to change, so this has all been really helpful.

    Yeah I agree I think it fixed my case quite well! So I'll tend to ISharedComponentData for segmenting an archetype but generally only if that segmentation doesn't change (often) throughout the game, and also one that's not so granular that it gets in the way of parallelization.
     
  16. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    I've worked through redoing most of my substantial simulation initialization. I didn't do my init in Systems because that just seems weird to me to put one-time code in OnUpdate or OnStartRunning and never use it again. Also just makes my startup feel clunky.

    So init is running from a single large ConvertToEntity class, which gets a bunch of data from Scriptable Objects. There is necessarily a lot of sequencing and referencing dependent data. States, Voters, Issues, Map Views, State Views, Focus Groups, etc.

    It's kind of painful though. It's single-threaded, fetching components one at a time via stuff like ToComponentDataArray<FirstNeededComponent>. I'm in situations like

    Code (CSharp):
    1.         public void CreateFuzzyMapViewFromStates(IActor actor, EntityManager entityManager)
    2.         {
    3.             var statesQuery = entityManager.CreateEntityQuery(typeof(StateComponent));
    4.             var states = statesQuery.ToComponentDataArray<StateComponent>(Unity.Collections.Allocator.Temp);
    5.  
    6.             var shuffledStates = states.Fischer();
    7.  
    8.             foreach(var state in shuffledStates)
    9.             {
    10.                     .... // Create "Fuzzy Views" of the states for each actor
    11.                     var stateIssues = issueRepository.GetIssuesByState(state); //Feels wrong! Entities.ForEach loops would let me get these buffer items at the same time of iteration and I end up needing Entity references back on my Components
    12.  
    It seems like I want to do stuff like Entities.ForEach working through Archetypes, but those only seem to be available in SystemBase, which I'm totally fine using at run time for all the repeated logic that runs through the game.

    So the question seems to be, are there better/other ways to handle one-time complex init logic that requires Entity looping/jobs, or is creating a one-run System the way to go?
     
  17. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    A system that only runs when it sees an initialization component and then destroys that component is the way to go. Such an approach is also robust to multiple games within a single application session.
     
  18. RecursiveEclipse

    RecursiveEclipse

    Joined:
    Sep 6, 2018
    Posts:
    298
    Not sure I understand your new problem. But I should mention if you use IConvertGameObjectToEntity and SubScenes, you'll only pay the conversion cost once in the editor when the SubScene is built and not when entering play mode.

    IConvertGameObjectToEntity is only local to the MonoBehaviour being converted though. If you need wider logic about all MonoBeahviours or something complex, look at GameObjectConversionSystem.

    A related component type sort of like init components is ISystemStateComponentData, which can be added after being seen once, but should be local to the system, and must be removed when the Entity is destroyed or it will stick around. I sometimes add an "IsProcessed" tag as an "I've seen it and done something" marker. Regular components with data that you remove may be better for initialization or signals/events.
     
    Last edited: Jun 13, 2021
  19. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    That makes a lot of sense in terms of leveraging it for multiple games! I'd built my game as it stood before ECS using very event-driven system, I basically never used Update and the like in Unity despite using a lot of monobehaviours, so to me it was weird to have this thing hanging around running Update every frame, but I suppose if it's doing essentially nothing it's fine. Less of a performance concern and more just a change in thinking.

    I'll give this a try it seems like it'll resolve most of my remaining issues, thanks!
     
  20. jsmilovic

    jsmilovic

    Joined:
    Jun 6, 2017
    Posts:
    13
    Ah, thanks for that info! IConvertGameObjectToEntity got me understanding more about the Conversion workflow. I was not really using Conversion workflow. Pre ECS when someone clicked New Game, a monobehaviour called a StartupService (Also Monobehaviour) which has a method that kicks off a coroutine/animation for the player and also an init method that creates all of the (substantial) game data on an async thread. Whenever the data was done generating the Animation stopped and the game UI rendered.

    Moving to ECS I just replaced what that startup service did. I grabbed the EntityManager and started making Entities and Components. Entities down the line have Component data relying upon previously created entities. i.e. loop over my previously created States and create voters to "populate" them based on data from the State. It was just really messy trying to put it all together that way without tools like Entities.ForEach with multiple components coming in together.

    I feel like I could do it with Conversion workflow but most of the data in my startup is highly coupled. I'd have to be very careful about what conversions happen in what order, and I need to loop over previously created entities/components for some Entities, which IConvertGameObjectToEntity doesn't seem to support. Currently, I'm using EntityQueries and such all over the place just for init.

    So, if I've understood that correctly, I think a System does make sense to handle all the data generation since I can do as much looping and going backwards and forwards through the datasets as needed to generate the starting Entities/Components.