Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Best Practice for passing config data to an ECS System

Discussion in 'Entity Component System' started by DK_A5B, Mar 24, 2019.

  1. DK_A5B

    DK_A5B

    Joined:
    Jan 21, 2016
    Posts:
    110
    What's the Best Practice for passing "configuration" data to a System using ECS? For example, if I have a spawner System that periodically spawns Entities during the course of a game, and I want to have these Entities based on a Prefab, what is the best way to pass the Prefab to the System so that it has access to the Prefab?

    In standard Unity, my "system" would just be a MonoBehaviour, and I would have a public fields that I could pass the Prefab (and any other config data) to in the Editor. However, ECS Systems don't exist in the Scene, so I cannot set configuration data on them in the Editor.

    Reviewing the same ECS projects, the way they've handled this is by creating a "spawner" Entity which stores the Prefab (and any other config data), and then the System reads the config data off of the "spawner" Entity and destroys the Entity once this is complete. It seems like extra work to create a Component (and supporting conversion functionality) for one time use just so I can feed edit-time configuration data to a System. Is there a better way to do this that I'm not thinking of?

    Other approaches that I've considered are:
    • Attaching a "singleton" MonoBehaviour to the scene which stores the configuration data in public field. The System could then pull in the data from the public fields during OnCreateManager. This seems like an ugly hack though.
    • Using the new Addressable Asset functionality to access a ScriptableObject storing the configuration data for the System, which could also be pulled in during OnCreateManager. In this case I'd be creating a one-off ScriptableObject to store the config data, but for some reason this seems like less throwaway work that creating a one-off Component (w/ GameObject to Entity conversion logic).

    I think all of these approaches should be viable, I'm just looking for some feedback on what the Best Practice / most-ECS way of doing this would be.
     
  2. Abbrew

    Abbrew

    Joined:
    Jan 1, 2018
    Posts:
    417
    There's a method for getting singleton entities and then grabbing a Monobehaviour from it. It's not deprecated, so it should be considered good practice. Here's an example:

    Code (CSharp):
    1. public class PreloaderSystem : JobComponentSystem
    2. {
    3.     private ComponentGroup preloaderGroup;
    4.     protected override void OnCreateManager()
    5.     {
    6.         preloaderGroup = GetComponentGroup(new EntityArchetypeQuery()
    7.         {
    8.             All = new ComponentType[]
    9.             {
    10.                 ComponentType.ReadOnly<Preloader>(),
    11.             }
    12.         });
    13.     }
    14.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    15.     {
    16.         var preloader =
    17.             EntityManager.GetComponentObject<Preloader>(
    18.                 preloaderGroup.GetSingletonEntity()
    19.             );
    20.         // get data from preloader
    21.         var preloaderGridRadius = preloader.GetPreloaderGridRadius();
    22.     }
    23. }
     
  3. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,222
    It might seem like extra work, but there are a ton of benefits of this approach. For one, you can now have multiple spawners in the future with different prefabs. Maybe later you decide to add a prefab variant with slightly stronger enemies. You wouldn't even have to touch another line of code to add it. Also reading from a config IComponentData can be a lot faster than having another indirect access to a ScriptableObject.

    But if this is just an initialization system and you aren't too particularly worried about performance, then you can do the spawning in a MonoBehaviour and put the spawning data there. HelloECS 5 does this.
     
    bb8_1 and ModLunar like this.
  4. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    For assets I use conventions quite heavily. Using data classes with enum values to describe the hierarchy. Like we have an item class with sku and asset type enums. You could use integer id's in place of enums also. The idea being your data points to assets via some type of hierarchy convention much like Restful design. So the actual assets can live anywhere be anything and be loaded say via addressables.

    That's rather orthogonal to the specific mechanism of how you get it into ECS. And in some cases you have data that is purely scene specific and it does make sense to just store references directly.
     
  5. DK_A5B

    DK_A5B

    Joined:
    Jan 21, 2016
    Posts:
    110
    Yeah, I see how in the specific case of having a spawner System, where there could be multiple spawn points (each with their own prefab), having an Entity representing the spawn point makes sense (and this is how things work, more or less, in some of the HelloECS examples).

    My question is more generally about configuring global parameters on a System though. Perhaps my spawning example wasn't the best. As another example, if I had a simulation that had a System that was dependent on the acceleration due to gravity, it wouldn't really make sense to have an Entity because it is a universally applicable parameter (not spatially specific like a spawn point). I also wouldn't want to implement that System as a MonoBehaviour (like in HelloECS 5) just so I could easily tweak that parameter, because that goes against the whole idea of ECS Systems as a whole. I could hardcode a variable for the acceleration due to gravity in the System, but this is ugly and requires a code edit just to change the parameter.
     
  6. DK_A5B

    DK_A5B

    Joined:
    Jan 21, 2016
    Posts:
    110
    Thanks, but this is still a lot of extra code vs. the old way of doing it. For example, with ECS it would be something like:

    Code (CSharp):
    1. public class SimulationSystem : ComponentSystem
    2. {
    3.     // system parameters:
    4.     private float accelGravity;
    5.  
    6.     // query to get the config Entity
    7.     private ComponentGroup simulationSystemConfigGroup;
    8.    
    9.     protected override void OnCreateManager () {
    10.         // setup the query
    11.         simulationSystemConfigGroup = GetComponentGroup (new EntityArchetypeQuery () {
    12.             All = new ComponentType[] {
    13.                 ComponentType.ReadOnly<SimulationSystemConfig> ()
    14.             }
    15.         });
    16.     }
    17.  
    18.     protected override void OnUpdate () {
    19.         // execute query to get single entity
    20.         var simulationConfig = EntityManager.GetComponentObject<SimulationSystemConfig> (
    21.             simulationSystemConfigGroup.GetSingletonEntity ()
    22.         );
    23.  
    24.         // grab the acceleration due to gravity
    25.         accelGravity = simulationConfig.accelGravity;
    26.  
    27.         // NOTE: I'd probably want to destroy the entity here, so that I didn't have to get the config Entity on every Update,
    28.         // but I'm not clear on how that would work with the GetSingletonEntity() method.
    29.         // I suppose I could also leave the config Entity in place, which would allow me to update the acceleration due to
    30.         // gravity on the fly in the middle of execution, but this seems like a performance drag without any upside.
    31.  
    32.         // actually do work...
    33.     }
    34. }
    This is in contrast to the "traditional" Unity style which would be something like:

    Code (CSharp):
    1. public class SimulationSystem : MonoBehaviour {
    2.     public float accelGravity;
    3.  
    4.     void Update () {
    5.         // actually do work...
    6.     }
    7. }
    If this is really how things are supposed to be done in ECS, so be it. It's just considerably less convenient than the traditional Unity approach.
     
    deus0 likes this.
  7. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Where to store the data is no different then it was before. The scope of the data itself hasn't changed.

    The strangeness comes from the fact that the only scene specific container is a MonoBehaviour. But for the context of ECS just treat it as a data container. One that you likely want to destroy once it's data is loaded into ECS. But that's an implementation detail that can be hidden away.

    What is missing right now is good abstractions for handling a scene context. So some type of OnSceneLoaded event along with some notion of order of execution among systems to deal with dependencies. And then a uniform approach to identifying monobehaviors that you use as containers for scene data and grabbing them. But that's not really that difficult to put together.

    I have a limited version of that flow I've used for several months now that just uses FindObjectsOfType from on scene loaded events. My thinking is the bigger problem to solve is don't have scene initialization logic spread over both ECS and monobehaviors. Do it in one place or another. And I liked the ECS side better because it's more explicit about what order things happen in. I don't have to deal with script execution order settings, it's all right there in one place and I can control it via code.
     
    TheSmokingGnu likes this.
  8. SimonCVintecc

    SimonCVintecc

    Joined:
    Jun 4, 2019
    Posts:
    8
    Maybe this is what you're looking for?

    Code (CSharp):
    1. public class SimulationSystemSettings : MonoBehaviour
    2. {
    3.     public float AccelGravityOnMonoBehaviour;
    4.  
    5.     void Update ()
    6.     {
    7.         //update system value from monobehaviour value
    8.         World.Active.GetExistingSystem<SimulationSystem>().AccelGravityOnSystem = AccelGravityOnMonoBehaviour;
    9.     }
    10. }
    Code (CSharp):
    1. public class SimulationSystem : JobComponentSystem
    2. {
    3.     public float AccelGravityOnSystem;
    4.  
    5.     protected override JobHandle OnUpdate(JobHandle inputDependencies)
    6.     {
    7.         //do work using AccelGravityOnSystem
    8.     }
    9. }
     
  9. Enzi

    Enzi

    Joined:
    Jan 28, 2013
    Posts:
    954
    With system variables that could change I use static fields. Any downside to that?
    I don't see any value, storing them in a more complicated manner when it's not used for anything else. (iteration)
    Good side-effect is that the static variables can be changed from anywhere else too with 1 line.

    edit: My answer is not good for your spawner case.
    Generic spawners got waaay too complicated to handle that's why my spawner puts a SpawnComplete comp on the spawned object.

    Code (CSharp):
    1. struct SpawnJob : IJobForEachWithEntity<SpawnCommand>
    2.     {
    3.         public EntityCommandBuffer CommandBuffer;
    4.  
    5.         public void Execute(Entity entity, int index, [ReadOnly] ref SpawnCommand spawner)
    6.         {        
    7.             var instance = CommandBuffer.Instantiate(spawner.prefab);        
    8.  
    9.             CommandBuffer.SetComponent(instance, new Translation { Value = spawner.position });
    10.             CommandBuffer.SetComponent(instance, new Rotation { Value = spawner.rotation });
    11.  
    12.  
    13.             CommandBuffer.AddComponent(entity, new SpawnComplete
    14.             {
    15.                 spawnedEntity = instance,
    16.                 updateSurroundings = spawner.updateSurroundings
    17.             });
    18.  
    19.             if (spawner.updateHexCoordsComp)
    20.             {
    21.                 CommandBuffer.SetComponent(instance, new HexCoordComp
    22.                 {
    23.                     Value = HexWorld.CoordsToQR(spawner.position.x, spawner.position.z)
    24.                 });
    25.             }
    26.  
    27.             if (spawner.persist)
    28.                 CommandBuffer.RemoveComponent<SpawnCommand>(entity);
    29.             else
    30.                 CommandBuffer.DestroyEntity(entity);
    31.         }
    32.     }
    Overall it's is a pretty long chain. I have a MonoBehaviour BuildingDefinition in a proxy. That's basically the config for all the buildings. The proxy takes the BuildingDefinition and adds buffers, comps and fixed data like structureType, size, id, maxHealth, etc...
    So all those buildings are in the beginning gameobjects that are converted in a monobehaviour script at startup into entities. Those are saved in a Dictionary for easier lookup and those entities are then referenced when I want to spawn any building.
    As the proxy is not enough for all the data, because some are just known at runtime, I have the SpawnComplete comp so I can make additional operations. For example, the buildings have an input/output buffer which I can't fill in the proxy, some registering of coordinates in the grid and a healthBar.

    It's a long chain, but I have the feeling it matured over the weeks to something that's quite useful and I don't need to change much.

    What helped immensly is that there's an automatic fixup with AddComponent when the entity is just spawned, if you have ever debugged, you'll see new instantiates are counting down and start with entity id -1, -2 and so on. Not useful if you want the reference immediately, but if you put it in a comp, ECS will fix those references in the components so it can be used after the EntityCommandBuffer is done, which I use in SpawnComplete.spawnedEntity. Either the postprocess of SpawnComplete then deletes the SpawnCommand or it gets removed from the spawner itself when the persist flag is not set.
     
    Last edited: Jul 5, 2019
  10. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    I'm really wondering the same question. On the surface, it's a simple question:
    What's the preferred way to pass data to my ECS system? (inherits from
    SystemBase
    now)

    Or maybe I'm just misunderstanding the power of Components being our data.. hmm..
    Maybe what I want is to understand is.. Components form our replacement for our per-instance data.
    But what about shared data that apply for the whole system?
    I'm gonna look into ISharedComponentData (good blog post on it here) cause this might be what I'm looking for
     
    Last edited: Jul 10, 2020
  11. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    Fields in the system class?
     
  12. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Are we able to edit them in the Inspector somewhere?
     
  13. DanSuperGP

    DanSuperGP

    Joined:
    Apr 7, 2013
    Posts:
    408
    You can make a component for your settings data, put the [GenerateAuthoringComponent] on it and put it in your scene.

    Then you can edit all your settings there.

    As long as only one of that entity exists you can call GetSingleton<ComponentType>() inside your to get the entity, and then get to the data in the normal way.
     
    deus0 and ModLunar like this.
  14. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    @DanSuperGP Oh excellent! Are there restrictions about using Reference Types? Perhaps I'd like to reference an asset (a Material, ScriptableObject, or etc.)

    I know I'd ideally limit my usage of these references, to better-benefit from cache-hitting my data in a row without making the CPU jump around in memory too much, but am unaware of best-practices here. (If there's a docs page that talks about this and I'm just being blind or silly, feel free to link that as well haha. Thanks in advance! :) )
     
  15. DanSuperGP

    DanSuperGP

    Joined:
    Apr 7, 2013
    Posts:
    408
    Quoting brunocoimbra...

    "It is just a simple mechanism to use an EntityQuery with an individual entity instead of dealing with an array of entities for just one element."

    You can put any component you could normally put on a game object you are converting to an entity.

    So I think you could attach reference types to it, set them up with AddHybridComponent, and then access them with EntityManager.GetComponentObject<> when working on the main thread.

    I think... I've only been trying to learn ECS for a week...
     
    DotusX, TheSmokingGnu and ModLunar like this.
  16. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
  17. hojjat-reyhane

    hojjat-reyhane

    Joined:
    Feb 4, 2014
    Posts:
    49
    Can anyone tell me what is the best and optimized solution for my case?
    I have about 30 different materials which I use to render entities with DrawMesh().
    How can I set these materials for each entity?

    Currently I have an array of materials in a singleton monobehaviour.
    I'm setting the index of material for each entity.
    Then in my custom render system, I use that index to get the material form array.
    I know this is not good, but don't know what is the best way.
     
    deus0 likes this.
  18. nobeerleft

    nobeerleft

    Joined:
    Mar 29, 2012
    Posts:
    27
    One method I am using is to keep settings in a ScriptableObject that has an Addressables key defined for it.

    Then when my System starts it can do

    Code (CSharp):
    1. protected override void OnCreate()
    2. {
    3.     Enabled = false;
    4.     Addressables.LoadAssetAsync<GenerationSettings>("GenerationSettings")
    5.       .Completed += handle => {
    6.            Settings = handle.Result;
    7.            Enabled = True;
    8.     };
    9. }
    Being a ScriptableObject you can easily make changes outside of the code.
     
    DotusX and ModLunar like this.
  19. varnon

    varnon

    Joined:
    Jan 14, 2017
    Posts:
    52
    If I'm correct, that means the data would be accessible in any systems OnUpdate, but not OnCreate. It would be nice to have an equally convenient way to modify anything needed in OnCreate.


    Actually, this seems like a pretty good solution for that.
     
    deus0 likes this.
  20. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    256
    Thank you! With GenerateAuthoringComponent and ConvertToEntitySystem, this turned out to be the easiest solution to create settings with game objects/prefabs and then use them in ECS! I was using scriptable objects still and singleton behaviors, now I can do away with that. I am wondering if there is also a way to push Mesh/Material/AudioClip data into ecs like this (as ISharedComponentData's)?