Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Storing converted entity prefabs other than SubScenes

Discussion in 'Entity Component System' started by yondercode, Jul 29, 2020.

  1. yondercode

    yondercode

    Joined:
    Jun 11, 2018
    Posts:
    27
    I'm trying to find a better way to store entity prefabs from converted GameObjects.

    Previously I'm using a very naive way by converting the prefab GO in runtime on scene change which is horrendously slow.

    Now I'm utilizing SubScene as a prefab container, by making a SubScene with one
    IDeclareReferencedPrefab
    and loading this subscene whenever I want to load the entity prefabs (and simply unload the subscene when I don't need them anymore). This is incredibly fast and I'm happy that it works with Addressables. However I just think that this is quite messy and I don't think SubScenes is made for something like this.

    Is there something in the current ECS library that is more suitable for pre-converting prefabs that I've missed?
     
    burningmime likes this.
  2. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    760
    Do you convert the GO every time you need an instance, or create it once and instantiate the entity?
    You could convert all the GO to entites when loading the scene or the game, so you only need o instantiate the entity prefab.
     
  3. yondercode

    yondercode

    Joined:
    Jun 11, 2018
    Posts:
    27
    I need to convert the GO in the runtime, middle of the game.

    I have this kind of "random enemy spawn events" that I can't determine on the scene/game load. I don't think it's preferable to load all possible prefabs at game load since there's quite a lot of content in total and usually only a few of those events will happen in one gaming session.

    For example in the middle of the game, a Pirate Invasion Event happened and I need to load pirate enemy prefabs, then unload it after the event has ended. Then maybe later an Alien Invasion Event happened and I need to load alien enemy prefabs and unload them afterwards. Maybe Terraria could be a reference of what kind of enemy spawning system that I want to achieve.

    With
    Addressables.LoadAssetAsync
    the asset loading itself is pretty smooth, however
    ConvertGameObjectHierarchy
    is synchronous and extremely slow.
     
  4. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    This is fairly straight forward to optimize once you figure out how sub scenes work internally. But it will take some learning on your part to figure out the specific flow. We use lower level api's for a prefabs as sub scenes paradigm which is under 200 LOC but differs from the default sub scene setup. The core of it is convert hierarchy and then EntitySerializer.Serialize and managing the storage of ReferencedUnityObjects ourselves.

    Entity serialization serializes managed objects to a ScriptableObject ReferencedUnityObjects. So you can take those and export them to asset bundles actually. We store the serialized native data locally as it's not that big but you could store that on a server also. With the native binary data and the ReferencedUnityObjects you have what EntitySerializer.Deserialize needs to deserialize into a fresh world then copy that to the default world.

    How to make this work with the default sub scene flow would differ somewhat of course from what we have, but I'm sure it's doable without too much effort it's mostly figuring out where to integrate into the flow.
     
  5. runner78

    runner78

    Joined:
    Mar 14, 2015
    Posts:
    760
    It probably annoys a player less to wait a few seconds more for the game / level load, than lags on spawning. In general, you should cache and reuse converted GO entitiy-prefabs, just converting one GO prefab per gamesession.
     
  6. yondercode

    yondercode

    Joined:
    Jun 11, 2018
    Posts:
    27
    Thanks to the hints by @snacktime I think I got it working.

    I think I'll never figured out how subscenes work lol, like what are those sub scene sections, blob retain frames, etc it's just too complex, but at least from reading the code I found the section where the serialization process is actually happening.

    So in my new system every gameobject prefab that I want to convert will be given this component.
    Code (CSharp):
    1. public class ConvertToEntityPrefab : MonoBehaviour
    2. {
    3.     public SerializedEntityPrefab SerializedEntityPrefab;
    4. }
    And that component got one button to do the conversion in the editor and save the converted entity in a generated
    SerializedEntityPrefab
    asset which is a
    ScriptableObject
    asset. I wanted this to be automatic but I don't know how to do something on asset save other than OnValidate.

    Code (CSharp):
    1. [CustomEditor(typeof(ConvertToEntityPrefab))]
    2. public class ConvertToEntityPrefabInspector : Editor
    3. {
    4.     public unsafe override void OnInspectorGUI()
    5.     {
    6.         base.OnInspectorGUI();
    7.  
    8.         if (GUILayout.Button("Serialize To Entity"))
    9.         {
    10.             using (var serializationWorld = new World("Serialization World"))
    11.             {
    12.                 byte[] serializedEntityData;
    13.                 ReferencedUnityObjects referencedUnityObjects;
    14.  
    15.                 ConvertToEntityPrefab ConvertToEntityPrefab = target as ConvertToEntityPrefab;
    16.  
    17.                 GameObjectConversionUtility.ConvertGameObjectHierarchy(ConvertToEntityPrefab.gameObject, new GameObjectConversionSettings
    18.                 (
    19.                     serializationWorld,
    20.                     GameObjectConversionUtility.ConversionFlags.AddEntityGUID | GameObjectConversionUtility.ConversionFlags.AssignName
    21.                 ));
    22.  
    23.                 using (var entityRemapping = serializationWorld.EntityManager.CreateEntityRemapArray(Unity.Collections.Allocator.Persistent))
    24.                 {
    25.                     using (var memoryWriter = new MemoryBinaryWriter())
    26.                     {
    27.                         SerializeUtilityHybrid.Serialize(serializationWorld.EntityManager, memoryWriter, out referencedUnityObjects, entityRemapping); // entity remapping ????
    28.  
    29.                         serializedEntityData = new byte[memoryWriter.Length];
    30.                         using (var memoryReader = new MemoryBinaryReader(memoryWriter.Data))
    31.                         {
    32.                             //TODO: compress the bytes, 16kb overhead every prefab is just yikes
    33.                             for (int i = 0; i < memoryWriter.Length; i++)
    34.                             {
    35.                                 serializedEntityData[i] = memoryReader.ReadByte();
    36.                             }
    37.                         }
    38.                     }
    39.                 }
    40.  
    41.                 if (ConvertToEntityPrefab.SerializedEntityPrefab == null)
    42.                 {
    43.                     var asset = CreateInstance<SerializedEntityPrefab>();
    44.                     AssetDatabase.CreateAsset(asset, AssetDatabase.GetAssetPath(ConvertToEntityPrefab).Replace(".prefab", "Serialized.asset")); //TODO: less hard-coding maybe?
    45.                     AssetDatabase.SaveAssets();
    46.  
    47.                     ConvertToEntityPrefab.SerializedEntityPrefab = asset;
    48.                 }
    49.  
    50.                 var SerializedEntityPrefab = ConvertToEntityPrefab.SerializedEntityPrefab;
    51.  
    52.                 SerializedEntityPrefab.Array = referencedUnityObjects.Array;
    53.                 SerializedEntityPrefab.EntityData = serializedEntityData;
    54.  
    55.                 EditorUtility.SetDirty(SerializedEntityPrefab);
    56.             }
    57.         }
    58.     }
    59. }
    Code (CSharp):
    1. public class SerializedEntityPrefab : ReferencedUnityObjects
    2. {
    3.     [HideInInspector]
    4.     public byte[] EntityData;
    5.  
    6.     public void LoadEntity(World dstWorld, World loadingWorld)
    7.     {
    8.         unsafe
    9.         {
    10.             fixed (byte* bytePtr = EntityData)
    11.             {
    12.                 using (var entityDataReader = new MemoryBinaryReader(bytePtr))
    13.                 {
    14.                     SerializeUtilityHybrid.Deserialize(loadingWorld.EntityManager, entityDataReader, this);
    15.                 }
    16.             }
    17.         }
    18.         dstWorld.EntityManager.MoveEntitiesFrom(loadingWorld.EntityManager); //TODO: this method has an out param, we should tag the moved entities with SCD or something, so that we can unload it easier later on
    19.     }
    20. }
    And now I only need to refer to the generated
    SerializedEntityPrefab
    without ever need to touch the original authoring GameObject on runtime. Also only the generated
    SerializedEntityPrefab
    will be included in the build as addressables. This prefab authoring process overall is a lot less janky than my previous SubScene approach.

    I'm not sure if this will work for all cases though, I only tried this in several prefabs with mesh renderers and other components and it works perfectly for those, they even got the prefab and linked entity group components automatically! Actually I'm quite surprised that ECS automagically exported my SharedComponentData as
    ReferencedUnityObjects
    and importing it back correctly. I'm not sure what will happen if I use blob asset store in the conversion process. Also I don't know what is entity mapping when serializing the world, the process works the same without it.
     
    Neiist, veliefendi, Enzi and 3 others like this.
  7. defic

    defic

    Joined:
    Apr 29, 2013
    Posts:
    18
    For
    Thanks for the scripts! For some reason the references are missing for me.
    The scriptable object looks like this:


    And after loading, it shows up like this in the entity debugger inspector:


    Any clue what could be wrong? I am not using hybrid renderer.
     
  8. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    714