Search Unity

Discussion The best practice for a real life large projects

Discussion in 'Entity Component System' started by andreyakladov, Mar 23, 2023.

  1. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    I struggle with ECS quite a lot and I decided to start a discussion here about the right ECS approach.
    Here is what I’ve got now:
    • GameObjects in prefabs as they have some MonoBehaviours like Animator etc. Located in Resources folder
    • Each level is a Scene file
    • Settings, Configs provided as json files (some from disk, some from network), deserialized into simple objects/structs
    • Player-Progress/Save/Stats stored in json (submitted to server or stored locally)
    With no ECS what I do (on high level) is:
    1. app starts
    2. services load the data from APIs, Resources and Disk while splash screen presented
    3. Main Menu scene loaded and reflects player progress/stats etc.
    4. Player may manage stats/characters/inventory here. Data updated and saved.
    5. Pressing Play triggers Level scene to load. Which Level scene to load controlled by player progress.
    6. Once scene loaded, level builder loads prefabs and adds GameObjects into scene, set ups corresponding values from configs, settings and player progress and establishes relationships/subscriptions/data-flows between the objects.
    7. Level starts with everything in place and ready.
    I absolutely love this approach, however when it comes to lots of enemies and projectiles etc, I have to drop some features to maintain smooth fps, so I’m looking for a way to improve performance. I already use Burst + Jobs for raycasts and some other things, however ECS seems the right approach.

    With ECS I can not understand how to make a single entry point to my Level when all the data I need loaded from various sources. So, the questions:
    1. Is there a way to control when Systems created? As I only need them after Level starts.
    2. Is there a way to inject data/configs/settings into systems? Or systems can only access objects from the inside?
    3. Is there a way to load/create/add a subscene onto a Level scene once it loaded? I tried to load a subscene additive but it replaces current screen completely for some reason. I tried adding it in other ways but it all seems invalid.
    4. Is there a way to add prefabs to a subscene from the outside or this is only possible form the inside of a Baker subclass via GetEntity(GameObject). I’ve checked BakerSystem but it seems it can only manipulate already baked data from Bakers. I do not like Bakers-based approach as I need to be able to control the order/sequence of load and access loaded objects in one place.
    As I understand, I should probably use #UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP and create World and Systems (not sure how) manually right after level scene loaded start and inject the data at this point. Do you think it worth trying? Or entire approach should be changed to be fully based on systems and entities even during app startup and stored data loading.

    Thanks a lot!
     
  2. Laicasaane

    Laicasaane

    Joined:
    Apr 15, 2015
    Posts:
    361
    When the systems are created is not that important. Only the order of Update matters.

    Since you can access
    World.DefaultGameObjectInjectionWorld
    from anywhere in your code, you can create entity by using
    World.EntityManager
    facility and add components which store your settings data.

    ECS systems will query these settings components to get the data you want.

    No, subscene must be loaded along with the normal scene.

    There is only 1 way to make Entity prefab from the outside: using the
    EntityManager.CreateEntity
    method at runtime. That means you have to manually build this prefab and set up all of its components.

    Baker is very neat and useful. I have no problem with it. I don't think I lose any control when use this facility.

    It seems you are misunderstanding baker? Baker works only at build time. Their purpose is to convert editor/authoring data into runtime data when the project is being built. So bakers will never work at runtime.

    I don't even need
    UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP
    . All I need is
    ICustomBootstrap
    to create systems in order.

    But you are thinking in OOP. In ECS systems won't update when there is no required data to operate on. You can use this characteristic to determine when and how a system will update.
     
  3. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    Thanks a lot for your answers!

    Ok then, a little more practical example as I’m not sure I actually understand the workflow. Let’s say I need to load different prefabs when user select different character.

    Old way:
    1. Read value user selected from APIcall/disk/service
    2. Load corresponding prefab and set values to it on level load and add to scene

    I assume the ECS way is something like:
    1. Create
      PlayerSpawnerConfig : IComponentData
    2. Add a GameObject with something like
      PlayerSpawner : MonoBehavior 
      and a Baker to it.
    3. PlayerSpawner should have references to every character prefab variation (set only via Editor and never at runtime, right?)
    4. During baking those prefab GameObject refenrences converted to Entitites and stored into PlayerSpawnerConfig among with some other possible values from Editor
    5. Then PlayerSpawnSystem : ISystem would
      1. Query for PlayerSpawnerConfig component, get Entities from it
      2. Query for an Entity which represents user preferences, progress, clothes and skins
    Futhermore, if I make levels as scenes, EVERY level scene MUST have a subscene created with a GameObject with a PlayerSpawner component added via editor as there is no way to add those things programmatically. Every time same set of Spawner component added manually via editor to populate the scene.

    Is this correct understanding how to do it in ECS?

    And then how PlayerSpawnSystem should know that there is a need to spawn character again cause level restarted/new one started as system lives forever no matter what scene is currently active? Should it set some flag in PlayerSpawnerConfig (like configComponent.Spawned = true). And then this value will reset to false when next Bake happend on next level scene load?
     
  4. Laicasaane

    Laicasaane

    Joined:
    Apr 15, 2015
    Posts:
    361
    You should know that you cannot store any kind of native collection inside a component at bake time. There is only BlobArray for you. For that you would have to use BlobAsset, and put a reference to that BlobAsset inside your
    PlayerSpawnerConfig
    . Please read the Entities manual to understand this part.

    But when you have ultimately converted prefab gameobjects into prefab entities, you don't have to store their references inside a container for later retrieval at runtime. You can just use EntityQuery to get the list of prefab entities at runtime.

    Code (CSharp):
    1.  
    2. using static Unity.Entities.SystemAPI;
    3.  
    4. // inside the System:
    5. private EntityQuery _prefabQuery;
    6.  
    7. // inside the OnCreate method:
    8. _prefabQuery = QueryBuilder()
    9.     .WithOptions(EntityQueryOptions.IncludePrefab)
    10.     .WithAll<YOUR_SPECIAL_COMPONENT>()
    11.     .Build();
    12.  
    13. // inside the OnUpdate method:
    14. // iterate _prefabQuery to get the entity you want and do further works
    15.  
    Unfortunately I don't have answer for this. Currently I'm doing things manually. But to ease this preparation before baking, I have some small tools to help me somewhat. You can experiment with custom editor, post processors, etc, to help you automate things.

    I suggest you dive into the Entities manual, the DOTS Best Practices guide on Unity Learn, and do some practices.

    You have to invent some data (and maybe some other systems) to give your
    PlayerSpawnSystem
    a signal to begin the work.

    For me I have some other entities to work as a signal for some system. On those entities I put an
    IEnableableComponent
    TAG. If the system finds the entities with this tag enabled, it will do further work.

    I also add another system to delete these signal entities. This clean-up system will only run after all other systems which process those signals.
     
    Last edited: Mar 23, 2023
  5. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    This part makes perfect sense for me, thanks!

    So in general I should convert everything I have loaded from various places (API/disk/resources) into entities by using EntitiesManager.CreateEntity. Probably when starting my level and probably clean/delete all those entities when leave level.
    And then access those entities via systems during gameplay.

    Regarding having to add subscene manually to level scenes - the must be a way to load/create a subscene and trigger it’s load, I will search for it.
    Or there must be a way to add a GameObject to a subscene and trigger it’s Baking at runtime.

    Anyway, thanks a lot for your answers!
     
  6. Laicasaane

    Laicasaane

    Joined:
    Apr 15, 2015
    Posts:
    361
    You don't have to do this for **everything**. We usually do this for prefab gameobjects. For other things that work as config data, we can reference to them as is in the subscene. From Entities 1.0.0-pre.65 you can save managed references in subscene without problem. Just remember that managed references can NOT be used inside Bursted code and Jobs. So if you have any data that you wish to be useable inside those Burst and Jobs, you should convert them into unmanaged data to save in subscene.

    To save managed references to ScriptableObjects, Textures, AudioClips... you have to use a class component.
     
  7. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,683
    Not only. You can use
    struct ISharedComponentData
    added through
    AddSharedComponentManaged
    in
    Baker
    and it will be serialized the same as if you add
    class IComponentData
    through
    AddComponentObject
    .
     
    Laicasaane and andreyakladov like this.
  8. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    But I load the data from internet, user may update it in menu before level scene even exists, so this is not possible to reference those things from a subscene which is part of level scene created in Editor beforehand. I assume I must create entitites and maintain updates of those.
     
  9. Laicasaane

    Laicasaane

    Joined:
    Apr 15, 2015
    Posts:
    361
  10. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    Ok, After spending some time digging into topic I ended up with the code below.

    Automatic world creation disabled
    I create World and populate it programmatically once Level scene loaded
    I use managed IComponentData components to access Animator and Transform as Animator does not work in ECS? Skinned mesh renderer break the prefab once loaded as Entity, so, I assume the only way is to use GameObject + Animator.
    I create systems manually just because I would like to see they created in one place, actually it works fine if those created automatically, except for managed one (PlayerInputSystem) which needs external PlayerInput class set as a property.

    TransformSynchronizationSystem just makes sync between GameObject.Transform and Entity's TransformAspect - ugly but fine.

    And what I really like so far is that I don't need any SubScene on my Level.scene at all.

    So far this is something I can definitely work with.

    Code (CSharp):
    1. _world = DefaultWorldInitialization.Initialize("Ordinary-World");
    2. ...
    3. var playerGameObject = _factory.InstantiatePlayer();
    4. var initialization = _world.GetExistingSystemManaged<InitializationSystemGroup>();
    5. var simulation = _world.GetExistingSystemManaged<SimulationSystemGroup>();
    6. var fixedStepSimulation = _world.GetExistingSystemManaged<FixedStepSimulationSystemGroup>();
    7.          
    8. var entityManager = _world.EntityManager;
    9. var playerEntity = entityManager.CreateEntity(
    10.                 typeof(TransformObjectData),
    11.                 typeof(AnimatorObjectData),
    12.                 typeof(Health),
    13.                 typeof(Armor),
    14.                 typeof(PlayerCharacterTag),
    15.                 typeof(LocalTransform),
    16.                 typeof(ParentTransform),
    17.                 typeof(WorldTransform));
    18. entityManager.SetComponentData(playerEntity, new Armor { Max = _playerConfig.armor, Current = _playerConfig.initialArmor});
    19. entityManager.SetComponentData(playerEntity, new Health { Max = _playerConfig.hp, Current = _playerConfig.initialHp});
    20. entityManager.SetComponentData(playerEntity, new TransformObjectData { Transform = playerGameObject.transform });
    21. entityManager.SetComponentData(playerEntity, new AnimatorObjectData { Animator = playerGameObject.GetComponent<Animator>()});
    22.          
    23. var inputSystem = _world.CreateSystemManaged<PlayerInputControlsSystem>();
    24. inputSystem.Input = _playerControls;
    25. simulation.AddSystemToUpdateList(inputSystem);
    26. fixedStepSimulation.AddSystemToUpdateList(_world.CreateSystem<PlayerMovementSystem>());
    27. fixedStepSimulation.AddSystemToUpdateList(_world.CreateSystem<TransformSynchronizationSystem>());
    28.  
    29. initialization.SortSystems();
    30. simulation.SortSystems();
    31. fixedStepSimulation.SortSystems();
    32.  
    33. inputSystem.Input.Player.Enable();
    34. Level = new Level(_world);
     
    Last edited: Mar 26, 2023
  11. Rukhanka

    Rukhanka

    Joined:
    Dec 14, 2022
    Posts:
    204
    It is true about Animator support absence in core Entites package. But there are an options: https://forum.unity.com/threads/dots-animation-options-wiki.1339196/
     
  12. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    True, but I'm migrating an existing project, so I do not really want to change animation approach. I still see lots of benefits in incorporating ECS even as hybrid as MonoBehaviours will play very specific small role fully controlled by Systems. Hybrid took no effort to employ it, so I like it.

    GameObjects loaded from prefabs via SubScene -> Authoring -> Baker -> SpawnSystem -> Entity appears all broken apart because of Skin MeshRenderer I assume.

    Did you try any of those options yourself? Can you recommend any?
     
  13. Rukhanka

    Rukhanka

    Joined:
    Dec 14, 2022
    Posts:
    204
    Everything depends from your needs. If you do not need bone positions back to entites after animation has been applied, you can use Hybrid or even GPU animation based approach. Bone attachments, IK is possible with hybrid but all this GameObject<->Entities synchronization seems not very robust nor performant options.

    I have made animation system for ECS and selling it on Asset Store, so I am not unbiased person on such advices :)
     
    andreyakladov likes this.
  14. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    983
    I was actually about to post a thread with a similar topic title when I saw this one. @andreyakladov I'm assuming this is more of an open discussion on the topic in general rather than a specific issue but if not let me know and I'll make my own thread.

    In my case I'm curious about how to transform large data sets for single entities. That is to say, I have a game where an entity can have a collection of base stats and then a collection of derived stats which are calculated on those base stats. Changing the value of the base stats of course should also change the derived stats. In my case I am easily pushing close to a hundred stats, mostly represented as floats.

    So what's the ideal way to both split the data as well as process it? Normally I'd just break up the stats categorically into their own classes and maybe hook up some INotifyChange event handlers so that derived stats get updated when the base stats do. In ECS I assume it would be better to transform the derived data by creating a system that pulls in both the base and derived components and does the necessary work during update. But what's the ideal split for the derived stats? Should I have one huge struct with loads of values (sounds doubtful to me)? Should I have a system dedicated to each and every derived stat that could be transformed (seems redundant)? Or should I split them based on logical need but them pull all of those component into for a single system update? Should I really being doing this live every frame? Base stats can change at any time but don't do so that frequently but marking the entity with some kind of 'update my stats now' component would change the archetype which also sounds costly.
     
  15. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    This is exactly what I’m trying to figure out now! I have effects, those activated and deactivated based on time or some other circumstances, so I was worndering should I just create components for each effect, and then remove or disable them. I’ve just create component called Effects with NativeArray (Allocator.Persistent) with struct Effect for each of those with ID, Duration, RemainingTime and Value. So far looks ok, but I haven’t tried to launch it yet.

    So, a IComponentData with NativeArray<Stat> and Stat itself could also carry NativeArray<Stat> Children. StatSystem will process the structure of your stats etc.
    You could aggregate changed stats somewhere, so the next frame StatSystem will pick at changes, align Stats and clean changes. At least something like this comes to my mind.
     
  16. andreyakladov

    andreyakladov

    Joined:
    Nov 11, 2016
    Posts:
    29
    I’ve just checked and there is a NativeHashMap in Unity.Collections. Since for your task ideal data structure is a Dictionary/HashMap, I think that might be a good one.
     
    Sluggy likes this.
  17. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    983
    Ah yeah, that's great to know! Makes it much easier to at least reference things by names in the editor and let the runtime just work with hashed ids. For now I've just decided to group stats categorically based on how likely they would be accessed at the same time. So for example I have max health, current health, and health recovery all under a single IComponentData struct. I think I'll just be writing one system for each derived stat that takes into account all possible combined effects including base stats as well as other more transient bonuses. That of course means a whole bunch of systems that basically do exactly the same thing just with different formulas but until someone tells me otherwise I'm just gonna go with the first idea and see what happens. Either it will work fine or I'll hit a roadblock or performance issue and learn the hard way ;)

    EDIT: Jeeze, totally forgot that you can only have one type of component of each type attached to an Entity. So yeah, this absolutely necessitates either a) grouping all derived stats into a single hard-coded struct or b) using some kind of native container to hold all of them and then somehow referencing into that container for each individual stat.
     
    Last edited: Mar 26, 2023