Search Unity

Singleton VS variables in the System

Discussion in 'Entity Component System' started by Mockarutan, Aug 28, 2020.

  1. Mockarutan

    Mockarutan

    Joined:
    May 22, 2011
    Posts:
    159
    I recently watched the a GDC presentation where a programmer on Overwatch explained their ECS solution for the game. He talks about singletons and how they solved a bunch of stuff for them. I realized I did not rely on singletons in my Unity ECS code, but it seems like the right way to do it. So I started converting systems to use singletons to hold global data or states instead of putting it as field in the system.

    My question is simply, is there a draw back to this pattern? Or is there something else one should consider with global settings/data/states in ECS?

    Link to the talk:
     
  2. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    Not sure if there are any drawbacks, but this is how I do. I always use singletons for system states, mostly because this way I have a nice way to inspect the data through the entity debugger., but also because this makes my life easier to create save data (just serialize all entities, no need to care about specific data inside specific systems)
     
  3. msfredb7

    msfredb7

    Joined:
    Nov 1, 2012
    Posts:
    168
    From my understanding, the main drawback is that a singleton entity still takes up a full chunk of data (almost all of it being empty).

    I'm not sure what's the official Unity take on this but since they made an API for it (Get/Set/HasSingleton<T>), then I can assume they approve of this approach. Who knows, maybe they have optims planned for it like having special smaller-sized chunks.

    NB: If you have big and static singleton data, maybe consider using BlobAssets?
    NB 2: As brunocoimbra said, I would also recommend you use fields in systems to store game data only when you need to because you lose a bunch of features by not using entities (world serialization, querying the data, debuggability with Entity Debugger, etc.)
     
    Mockarutan likes this.
  4. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    They already mentioned small and big chunks (don't remember where, but was in the same thread that they mentioned about the enabled/disabled state per component)
     
    msfredb7 likes this.
  5. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Storing system state data in [Get|Set]Singleton<> has a nice advantage when it comes to serialization as well; it'll just automatically be stored with the world and you don't need custom serializers throughout all your systems.

    The only thing you have to be careful of then is creating entities in OnCreate()

    The downside is the overhead of querying and limited data types by default (which can be worked around as you really can store anything on entities, though that doesn't mean it'll all serialize).
     
    Last edited: Sep 1, 2020
    florianhanke likes this.
  6. FakeByte

    FakeByte

    Joined:
    Dec 8, 2015
    Posts:
    147
    Actually there is a GetSingletonEntity() function which removes the overhead for querying the singleton.
     
  7. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    That has overhead. Under the hood, it just does a query.

    I'm not saying the overhead is huge. But have noticed the overhead in a large project where entity counts are pushing the limit and the costs of queries become measurable.
     
    Last edited: Sep 1, 2020
  8. FakeByte

    FakeByte

    Joined:
    Dec 8, 2015
    Posts:
    147
    It actually does a call to GetSingletonEntityQueryInternal(ComponentType type) which is not the same as a normal entity query, as this function is internal I can't for sure say it's faster, but since there is a special function for querying singletons and unity added the comment "Fast path for singletons" over this function I think it is at least faster than a normal entity query.
     
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    It calls this

    Code (CSharp):
    1. internal EntityQuery GetSingletonEntityQueryInternal(ComponentType type)
    2. {
    3.     ref var handles = ref EntityQueries;
    4.  
    5.     for (var i = 0; i != handles.Length; i++)
    6.     {
    7.         var query = handles[i];
    8.         var queryData = query._GetImpl()->_QueryData;
    9.  
    10.         // EntityQueries are constructed including the Entity ID
    11.         if (2 != queryData->RequiredComponentsCount)
    12.             continue;
    13.  
    14.         if (queryData->RequiredComponents[1] != type)
    15.             continue;
    16.  
    17.         return query;
    18.     }
    19.  
    20.     var newQuery = EntityManager.CreateEntityQuery(&type, 1);
    21.  
    22.     AddReaderWriters(newQuery);
    23.     AfterQueryCreated(newQuery);
    24.  
    25.     return newQuery;
    26. }
     
    florianhanke and FakeByte like this.
  10. FakeByte

    FakeByte

    Joined:
    Dec 8, 2015
    Posts:
    147
    You are right, do you know what the second argument in the EntityManager.CreateEntityQuery(&type, 1); call is? I checked the scripting API and there is no mention about a CreateEntityQuery function with 2 arguments.
     
  11. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    If you step through a little you can see it's a count value and it leads to this.

    Code (CSharp):
    1.  
    2. public EntityQuery CreateEntityQuery(EntityDataAccess* access, ComponentType* inRequiredComponents, int inRequiredComponentsCount)
    3. {
    4.    var buffer = stackalloc byte[1024];
    5.    var scratchAllocator = new UnsafeScratchAllocator(buffer, 1024);
    6.    var archetypeQuery = CreateQuery(ref scratchAllocator, inRequiredComponents, inRequiredComponentsCount);
    7.    var outRequiredComponents = (ComponentType*)scratchAllocator.Allocate<ComponentType>(inRequiredComponentsCount + 1);
    8.    outRequiredComponents[0] = ComponentType.ReadWrite<Entity>();
    9.    for (int i = 0; i != inRequiredComponentsCount; i++)
    10.        SortingUtilities.InsertSorted(outRequiredComponents + 1, i, inRequiredComponents[i]);
    11.    var outRequiredComponentsCount = inRequiredComponentsCount + 1;
    12.    return CreateEntityQuery(access, archetypeQuery, 1, outRequiredComponents, outRequiredComponentsCount);
    13. }
    14.  
     
    FakeByte likes this.
  12. brunocoimbra

    brunocoimbra

    Joined:
    Sep 2, 2015
    Posts:
    679
    Haven't properly tested it to be sure, but always thought that the "Fast path for singletons" comment is due you being able to use it inside OnStartRunning to store the Entity in a variable and then always use Get/SetComponent inside of OnUpdate (instead of querying for the singleton every single time).

    Basically this is what I do with required singletons.

    EDIT: what I haven't tested is if using Get/SetComponent is actually faster than using Get/SetSingleton directly
     
  13. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    As an addition - there are extension methods for Get\SetSingleton which we can call through
    this.Get\SetSingleton
    which allow class component data (as regular Get\SetSingleton has struct restriction) which allow everything inside (of course it does not include proper serialization but very useful)
     
    msfredb7 likes this.