Storing converted entity prefabs other than SubScenes

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?

1 Like

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.

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.

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.

6 Likes

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.

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.

public class ConvertToEntityPrefab : MonoBehaviour
{
    public SerializedEntityPrefab SerializedEntityPrefab;
}

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.

[CustomEditor(typeof(ConvertToEntityPrefab))]
public class ConvertToEntityPrefabInspector : Editor
{
    public unsafe override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("Serialize To Entity"))
        {
            using (var serializationWorld = new World("Serialization World"))
            {
                byte[] serializedEntityData;
                ReferencedUnityObjects referencedUnityObjects;

                ConvertToEntityPrefab ConvertToEntityPrefab = target as ConvertToEntityPrefab;

                GameObjectConversionUtility.ConvertGameObjectHierarchy(ConvertToEntityPrefab.gameObject, new GameObjectConversionSettings
                (
                    serializationWorld,
                    GameObjectConversionUtility.ConversionFlags.AddEntityGUID | GameObjectConversionUtility.ConversionFlags.AssignName
                ));

                using (var entityRemapping = serializationWorld.EntityManager.CreateEntityRemapArray(Unity.Collections.Allocator.Persistent))
                {
                    using (var memoryWriter = new MemoryBinaryWriter())
                    {
                        SerializeUtilityHybrid.Serialize(serializationWorld.EntityManager, memoryWriter, out referencedUnityObjects, entityRemapping); // entity remapping ????

                        serializedEntityData = new byte[memoryWriter.Length];
                        using (var memoryReader = new MemoryBinaryReader(memoryWriter.Data))
                        {
                            //TODO: compress the bytes, 16kb overhead every prefab is just yikes
                            for (int i = 0; i < memoryWriter.Length; i++)
                            {
                                serializedEntityData[i] = memoryReader.ReadByte();
                            }
                        }
                    }
                }

                if (ConvertToEntityPrefab.SerializedEntityPrefab == null)
                {
                    var asset = CreateInstance<SerializedEntityPrefab>();
                    AssetDatabase.CreateAsset(asset, AssetDatabase.GetAssetPath(ConvertToEntityPrefab).Replace(".prefab", "Serialized.asset")); //TODO: less hard-coding maybe?
                    AssetDatabase.SaveAssets();

                    ConvertToEntityPrefab.SerializedEntityPrefab = asset;
                }

                var SerializedEntityPrefab = ConvertToEntityPrefab.SerializedEntityPrefab;

                SerializedEntityPrefab.Array = referencedUnityObjects.Array;
                SerializedEntityPrefab.EntityData = serializedEntityData;

                EditorUtility.SetDirty(SerializedEntityPrefab);
            }
        }
    }
}
public class SerializedEntityPrefab : ReferencedUnityObjects
{
    [HideInInspector]
    public byte[] EntityData;

    public void LoadEntity(World dstWorld, World loadingWorld)
    {
        unsafe
        {
            fixed (byte* bytePtr = EntityData)
            {
                using (var entityDataReader = new MemoryBinaryReader(bytePtr))
                {
                    SerializeUtilityHybrid.Deserialize(loadingWorld.EntityManager, entityDataReader, this);
                }
            }
        }
        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
    }
}

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.

6 Likes

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.

I haven’t used it yet, but it seems like prefab streaming might be something that would work well for your use-case since you can’t tell upfront which things you want to preload.

https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master/ECSSamples/Assets/Advanced/EntityPrefab