Search Unity

  1. We are migrating the Unity Forums to Unity Discussions. On July 12, the Unity Forums will become read-only. On July 15, Unity Discussions will become read-only until July 18, when the new design and the migrated forum contents will go live. Read our full announcement for more information and let us know if you have any questions.

Question Any solution to omit Prefab conversion during runtime?

Discussion in 'Entity Component System' started by Elapotp, Jun 30, 2022.

  1. Elapotp

    Elapotp

    Joined:
    May 14, 2014
    Posts:
    98
    Hi guys,

    Like all of you, I am using prefabs to preconfigure some of my entities. After that, I wrote an EntityPrefab custom implementation to automatically convert them via the custom GameObjectConversionSystem at runtime. And it works, but I feel like that could be optimized a bit and be converted at build time (you can check the warning from the learning course DOTS Best practices (Part 2, point 8 "Runtime data is not the same as authoring data").

    So, the question is, how could I achieve that?
    Is it possible to convert those prefabs during build time (in editor it is okay to convert them in play mode, for now)?


    Here is my custom implementation to do that with GameObjectConversionSystem:

    Code (CSharp):
    1.     [CreateAssetMenu(fileName = nameof(EntityPrefab), menuName = "ECS/" + nameof(EntityPrefab), order = 0)]
    2.     public partial class EntityPrefab : SerializedScriptableObject
    3.     {
    4.         [OdinSerialize, NonSerialized] private GameObject _prefab;
    5.         [OdinSerialize, NonSerialized] private string _prefabEntityName = string.Empty;
    6.         private Entity _entity = Entity.Null;
    7.  
    8.         public GameObject Prefab => _prefab;
    9.         public string PrefabEntityName => _prefabEntityName;
    10.  
    11.         // Keep in mind, that prefab is disabled by default
    12.         public Entity Entity
    13.         {
    14.             get
    15.             {
    16.                 if (_entity != Entity.Null)
    17.                 {
    18.                     return _entity;
    19.                 }
    20.  
    21.                 Debug.LogError($"PrefabEntity is NULL. Check {nameof(EntityPrefab)}({name}) registration in your {nameof(EntityPrefabsHolder)}", this);
    22.                 return Entity.Null;
    23.             }
    24.         }
    25.  
    26.         public void SetPrefabEntity(Entity entity)
    27.         {
    28.             Assert.AreNotEqual(entity, Entity.Null);
    29.             _entity = entity;
    30.         }
    31.     }
    Code (CSharp):
    1.     [CreateAssetMenu(fileName = nameof(EntityPrefabsHolder), menuName = "ECS/" + nameof(EntityPrefabsHolder), order = 0)]
    2.     public partial class EntityPrefabsHolder : SerializedScriptableObject
    3.     {
    4.         [AssetSelector(IsUniqueList = true, ExcludeExistingValuesInList = true)]
    5.         [OdinSerialize, NonSerialized] private List<EntityPrefab> _entityPrefabs = new List<EntityPrefab>();
    6.  
    7.         public IReadOnlyList<EntityPrefab> EntityPrefabs => _entityPrefabs;
    8.  
    9.         private void OnValidate()
    10.         {
    11. #if UNITY_EDITOR
    12.             Editor_OnValidate();
    13. #endif // UNITY_EDITOR
    14.         }
    15.     }
    Code (CSharp):
    1.     [UpdateInGroup(typeof(GameObjectDeclareReferencedObjectsGroup))]
    2.     public partial class DeclareEntityPrefabsGOCS : GameObjectConversionSystem
    3.     {
    4.         private EntityPrefabsHolder _prefabsHolder;
    5.  
    6.         protected override void OnCreate()
    7.         {
    8.             base.OnCreate();
    9.  
    10.             // hack, cause could not get this conversion system in installer
    11.             _prefabsHolder = ProjectContext.Instance.Container.Resolve<EntityPrefabsHolder>();
    12.             Assert.IsNotNull(_prefabsHolder);
    13.            
    14.             var gameObjectExportGroup = World.CreateSystem<GameObjectExportGroup>();
    15.             var assignPrimaryEntityToEntityPrefabGOCS = World.CreateSystem<AssignPrimaryEntityToEntityPrefabGOCS>();
    16.            
    17.             gameObjectExportGroup.AddSystemToUpdateList(assignPrimaryEntityToEntityPrefabGOCS);
    18.         }
    19.        
    20.         protected override void OnUpdate()
    21.         {
    22.             _prefabsHolder.EntityPrefabs.ForEach(entityPrefab => DeclareReferencedPrefab(entityPrefab.Prefab));
    23.         }
    24.     }
    Code (CSharp):
    1.     public struct EntityPrefabContainer : IComponentData { }
    2.    
    3.     [DisableAutoCreation]
    4.     [UpdateInGroup(typeof(GameObjectExportGroup))]
    5.     public partial class AssignPrimaryEntityToEntityPrefabGOCS : GameObjectConversionSystem
    6.     {
    7.         private EntityPrefabsHolder _prefabsHolder; // could not Inject
    8.         private Entity _entityPrefabContainer = Entity.Null;
    9.  
    10.         protected override void OnCreate()
    11.         {
    12.             base.OnCreate();
    13.  
    14.             // hack, cause could not get this conversion system in installer
    15.             _prefabsHolder = ProjectContext.Instance.Container.Resolve<EntityPrefabsHolder>();
    16.             Assert.IsNotNull(_prefabsHolder);
    17.            
    18.             Assert.AreEqual(_entityPrefabContainer, Entity.Null);
    19.             _entityPrefabContainer = DstEntityManager.CreateEntity("EntityPrefabContainer" ,typeof(EntityPrefabContainer), typeof(Child), typeof(Prefab));
    20.         }
    21.        
    22.         protected override void OnUpdate()
    23.         {
    24.             foreach (var entityPrefab in _prefabsHolder.EntityPrefabs)
    25.             {
    26.                 var prefabEntity = GetPrimaryEntity(entityPrefab.Prefab);
    27.                 if (prefabEntity != Entity.Null)
    28.                 {
    29.                     entityPrefab.SetPrefabEntity(prefabEntity);
    30.                     if (!string.IsNullOrEmpty(entityPrefab.PrefabEntityName))
    31.                     {
    32.                         DstEntityManager.SetNameSafe(prefabEntity, entityPrefab.PrefabEntityName);
    33.                     }
    34.  
    35.                     DstEntityManager.AddComponentData(prefabEntity, new Parent { Value = _entityPrefabContainer });
    36.                     DstEntityManager.SetEnabled(prefabEntity, false);
    37.                 }
    38.                 else
    39.                 {
    40.                     Debug.LogError($"Primary entity for prefab `{entityPrefab.name}` is NULL", entityPrefab);
    41.                 }
    42.             }
    43.         }
    44.     }
     
  2. Arnold_2013

    Arnold_2013

    Joined:
    Nov 24, 2013
    Posts:
    291
    Maybe look into subscenes. These can hold 'pre-converted' entities and skip the runtime conversion.
     
  3. Elapotp

    Elapotp

    Joined:
    May 14, 2014
    Posts:
    98
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,349
    Subscenes are likely the correct answer, but I have never gotten them to work for anything other than rendering, Unity Physics, and a few IConvertGameObjectToEntity that read fully custom parameters and nothing from UnityEngine. I'm also not modifying packages like what Stunlock did, which might be part of my problem.

    By the way, I don't work for Unity.
     
  5. Elapotp

    Elapotp

    Joined:
    May 14, 2014
    Posts:
    98
    @DreamingImLatios , yeah, I know. But you develop your own framework on top of DOTS. So for sure know more and deeper than me :)
     
    DreamingImLatios likes this.
  6. Arnold_2013

    Arnold_2013

    Joined:
    Nov 24, 2013
    Posts:
    291
    For the DOTS animation package back in 0.17 it was required to load prefabs via subscene when using a build, otherwise no animation. For this purpose I created a Subscene with the animated prefabs in it. When the scene loads these prefabs make themselves known (a builder Entity with a component that stores their prefab entity and the enum value) and I store them in a NativeHashmap with the Enum as key. Then I need to instantiate an animated prefab I just make sure the spawner knows the enum value, to get the Prefab from the Hashmap.

    Now even without the animation package working I still have this system in place, even though it is more complicated than just runtime converting the prefabs. Also the Hashmap is not filled at the first frame, so code that uses this hashmap first checks if its count is higher than 0.

    Subscene are powerful for big worlds and what not... but that is too complicated for me... also when you unload a subscene all its entities disappear so you should make sure they did not follow the player :).

    To make a subscene just add it in the hierarchy with the "+" you can add an empty one and drag stuff into it, or you can select some stuff and create it from selection. You don't need to include subscenes in the build settings, its not a 'Scene'.

    Example of the builder component which is on the entity in my subscene. The "prefab" is the actual entity I will call instantiate on. When the Builder entity is processed by the PrefabProcessSystem (the "prefab" is in the hashmap at "prefab Id") the builder entity is destroyed, so it won't try to re-add the same prefab the next frame.
    upload_2022-7-3_13-40-3.png
     
  7. Elapotp

    Elapotp

    Joined:
    May 14, 2014
    Posts:
    98
    Thank you. For now, this idea feels a bit complicated.

    I would like to mimic a simple workflow with prefabs. Like, assign wherever prefab you want (or select via picker), just create EntityPrefab for the prefab (ideally, to create it at the time you assign prefab into a field). So, I would like to omit using subscenes if it's possible.

    At this stage, I am thinking about creating a master entity at editor time and serialize it on disk right away. Thus, on launch, I could load them all (even keep them loaded in the editor). The only thing is to link the loaded one to exact EntityPrefab (by path on disk or somehow).

    This is just an idea, and for sure it might be impossible to implement. That is why it would be great to receive some guidance from DOTS team. I am sure they did some research in this area (or even have some implementation and I just miss it).
     
  8. Arnold_2013

    Arnold_2013

    Joined:
    Nov 24, 2013
    Posts:
    291
    This is exactly what a subscene does. If you want to do it from a "Master Entity" its really easy. Create a component "allPrefabsComponent" with N entity fields one for each prefab you want to be able to access. Fill them in Editor with GameObject prefabs. Put the "Master Enity" GameObject with all these references into a Subscene and you are done (you need to save + close the subscene, but it has its own buttons for this).

    Now you can use the TryGetSingleton<allPrefabsComponent>() to get your master entity its allPrefabComponent and use its data to instantiate the prefabs.

    It will only help for the startup time, and I would be surprised if you notice it at all, but now there is no runtime conversion needed for your prefabs.
     
    bb8_1 likes this.
  9. Elapotp

    Elapotp

    Joined:
    May 14, 2014
    Posts:
    98
    Yeah, you are right. Subscenes do precisely that. I don't like the overall workflow with them — too many middlemen.

    Artists also work with prefabs. They should not know how to handle a prefab. Ideally, just create one (or duplicate existing) and and assign where it should be used in a scriptable object (aka config).
    Right now existing workflow is:
    1. Create prefab
    2. Via context menu select "Create EntityPrefab from Prefab"
    3. Assign newly created EntityPrefab to a reference in ScriptableObject (that's it for an artist)
    4. [dev] And in code from EntityPrefab you just use Entity property that already contains MasterEntity (simply as is)
    Not ideal, cause I would like to merge Steps 2 and 3 into one – assign a prefab (and do all technical stuff in the background). But for now, it works.


    If it's possible to do with subscenes – then it is worth trying.
    Did I get you right and this is possible?:
    1. Have a scene with a subscene and the holder for prefabs (as you suggested)
    2. Via the context menu select "Create EntityPrefab from Prefab" to add the selected prefab to the holder
    3. Save subscene (part of the context menu item)
    4. [dev] In code somehow get MasterEntity by Prefab reference (need to achieve the same experience as it was in Step 4 of existing workflow)
    NB. In my dreams I would also like to validate prefab reference for required authoring components (like RequireComponent attribute for MonoBehaviour), but that is a different story :)
     
    bb8_1 likes this.