Static Runtime data ECS

Hey, I can’t for the life of me figure out how to do this.

I have a bunch of ScriptableObjects that represent an item, it stores six things: Id, Name, Description, MaxStackSize, Mesh, Material.

Id is a string
Name is a string
Description is a string
MaxStackSize is an int
Mesh is a Mesh
Material is a Material

Now I have all of these ScriptableObjects tagged with Addressables as an “Item”. I am able to load them all into an IList (ItemData being the name of my Scriptable Object).

The part I am stuck at is, I have no idea where to put this data, every implementation I have tried has produced errors. I have searched so many places on this forum, but I can’t seem to find a solution for my use-case.

Things to note, all if this specific data is loaded at the start of the game, after which it will not be changing.

Things I have tried:
I tried creating a Singleton Entity, with a reference to IComponentData that stored a BlobArray<BlobAssetReference>, but this began throwing erros when I tried inspecting the entity. So I imagine this isn’t the correct way to do it.

I also just tried creating a static ItemData Dictionary, but from what I am reading using static objects is a bad idea in ECS design.

So now I am pleading to you, how would I go about solving this problem? Please let me know if you need anymore info. Thank you

Code

    public struct ItemData {
        public BlobString Id;
        public BlobString Name;
        public BlobString Description;
        public int MaxStackSize;
        public Mesh Mesh;
        public Material Material;

        public static BlobAssetReference<ItemData> BuildItem(ItemDataScript item_data) {
            using (var builder = new BlobBuilder(Allocator.Temp)) {
                var blob_builder = builder;
                ref var root = ref blob_builder.ConstructRoot<ItemData>();
                blob_builder.AllocateString(ref root.Id, item_data.Id);
                blob_builder.AllocateString(ref root.Name, item_data.Name);
                blob_builder.AllocateString(ref root.Description, item_data.Description);
                root.MaxStackSize = item_data.MaxStackSize;
                root.Mesh = item_data.Mesh;
                root.Material = item_data.Material;

                return blob_builder.CreateBlobAssetReference<ItemData>(Allocator.Persistent);
            }
        }
    }

    public struct ItemDatabase : IComponentData {
        //Cant Do this, but I don't know what I should do instead
        public NativeHashMap<NativeString64, BlobAssetReference<ItemData>> Items;
    }

    public struct Item : IComponentData {
        public BlobAssetReference<ItemData> ItemData;
    }

Currently the best approach for managed data like this is to generate an instance of a wrapper ScriptableObject at startup and put all your data into it. Then store that ScriptableObject into an ISharedComponentData on some single Entity (whose sole purpose is to only hold this ISCD). For the ISCD hashcode, use the ScriptableObject’s hashcode.

Is there no way to do this with Blobs? I really thought that would be the answer to static data.

Update:
I now create a ItemDatabase IComponentData that stores a reference to ItemDataBase blob which stores a BlobArray of shared components that I can assign to items at runtime.

This works, but I am having one issue, and I don’t know if it is an engine issue, or something caused by me

Where I create the blob:

public struct ItemDatabaseBlob {
        public BlobArray<Item> ItemData;

        public static BlobAssetReference<ItemDatabaseBlob> BuildData(IList<ItemDataScript> item_data_list) {
            using (var builder = new BlobBuilder(Allocator.Temp)) {
                ref var root = ref builder.ConstructRoot<ItemDatabaseBlob>();
                var array = builder.Allocate(ref root.ItemData, item_data_list.Count);

                for (var x = 0; x != array.Length; x++) {
                    array[x] = new Item {
                        Id = item_data_list[x].Id,
                        Name = new NativeString64(item_data_list[x].Name),
                        Description = new NativeString64(item_data_list[x].Description),
                        MaxStackSize = item_data_list[x].MaxStackSize
                    };
                }

                return builder.CreateBlobAssetReference<ItemDatabaseBlob>(Allocator.Persistent);
            }
        }
    }

    public struct ItemDatabase : IComponentData {
        public BlobAssetReference<ItemDatabaseBlob> Data;
    }

    public struct Item : ISharedComponentData, IEquatable<Item> {
        public int Id;
        public NativeString64 Name;
        public NativeString64 Description;
        public int MaxStackSize;

        public bool Equals(Item other) { return Id == other.Id && Name.Equals(other.Name) && Description.Equals(other.Description) && MaxStackSize == other.MaxStackSize; }

        public override bool Equals(object obj) { return obj is Item other && Equals(other); }

        public override int GetHashCode() {
            unchecked {
                var hashCode = Id;
                hashCode = (hashCode * 397) ^ Name.GetHashCode();
                hashCode = (hashCode * 397) ^ Description.GetHashCode();
                hashCode = (hashCode * 397) ^ MaxStackSize;
                return hashCode;
            }
        }
    }

Where I create the entity representing the item database:

        public async Task Init() {
            var items = await ScriptableObjectLoader.Load<ItemDataScript>(AddressableConstants.ITEM);

            EntityManager.CreateEntity(typeof(ItemDatabase));
            SetSingleton(new ItemDatabase {
                Data = ItemDatabaseBlob.BuildData(items)
            });
        }

I am able to create Items using GetSingleton(), but when I look at the ItemDatabase Entity in the inspector:
Error

it doesn’t give any info, and I get this error spammed in console:
Error

InvalidCastException: Specified cast is not valid.
Unity.Properties.Reflection.ReflectedMemberProperty`2[TContainer,TValue].GetValue (TContainer& container) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/Reflection/Properties/ReflectedMemberProperty.cs:24)
Unity.Properties.PropertyVisitor.VisitProperty[TProperty,TContainer,TValue] (TProperty property, TContainer& container, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:54)
Unity.Properties.Reflection.ReflectedPropertyBag`1+PropertyProxy`2[TContainer,TProperty,TValue].Accept[TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/Reflection/ReflectedPropertyBag.cs:19)
Unity.Properties.Reflection.ReflectedPropertyBag`1[TContainer].Accept[TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/Reflection/ReflectedPropertyBag.cs:58)
Unity.Properties.PropertyContainer.Visit[TContainer,TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyContainerVisit.cs:29)
Unity.Properties.PropertyVisitor.TryVisitContainerWithAdapters[TProperty,TContainer,TValue] (TProperty property, TContainer& container, TValue& value, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:127)
Unity.Properties.PropertyVisitor.VisitProperty[TProperty,TContainer,TValue] (TProperty property, TContainer& container, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:57)
Unity.Properties.Reflection.ReflectedPropertyBag`1+PropertyProxy`2[TContainer,TProperty,TValue].Accept[TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/Reflection/ReflectedPropertyBag.cs:19)
Unity.Properties.Reflection.ReflectedPropertyBag`1[TContainer].Accept[TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/Reflection/ReflectedPropertyBag.cs:58)
Unity.Properties.PropertyContainer.Visit[TContainer,TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyContainerVisit.cs:29)
Unity.Properties.PropertyVisitor.TryVisitContainerWithAdapters[TProperty,TContainer,TValue] (TProperty property, TContainer& container, TValue& value, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:127)
Unity.Properties.PropertyVisitor.VisitProperty[TProperty,TContainer,TValue] (TProperty property, TContainer& container, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:57)
Unity.Properties.Reflection.ReflectedPropertyBag`1+PropertyProxy`2[TContainer,TProperty,TValue].Accept[TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/Reflection/ReflectedPropertyBag.cs:19)
Unity.Properties.Reflection.ReflectedPropertyBag`1[TContainer].Accept[TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/Reflection/ReflectedPropertyBag.cs:58)
Unity.Properties.PropertyContainer.Visit[TContainer,TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyContainerVisit.cs:29)
Unity.Properties.PropertyVisitor.TryVisitContainerWithAdapters[TProperty,TContainer,TValue] (TProperty property, TContainer& container, TValue& value, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:127)
Unity.Properties.PropertyVisitor.VisitProperty[TProperty,TContainer,TValue] (TProperty property, TContainer& container, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:57)
Unity.Properties.VisitCollectionElementCallback`1[TContainer].VisitProperty[TElementProperty,TElement] (TElementProperty property, TContainer& container, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:17)
Unity.Entities.EntityContainerPropertyBag+ComponentsProperty+GetComponentDataCallback`1[TCallback].Invoke[TComponent] () (at Library/PackageCache/com.unity.entities@0.1.1-preview/Unity.Entities.Properties/EntityContainer.cs:65)
Unity.Properties.PropertyBag`1[TContainer].Cast[TCallback] (TCallback& callback) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/IPropertyBag.cs:38)
Unity.Entities.EntityContainerPropertyBag+ComponentsProperty.GetPropertyAtIndex[TGetter] (Unity.Entities.EntityContainer& container, System.Int32 index, Unity.Properties.ChangeTracker& changeTracker, TGetter getter) (at Library/PackageCache/com.unity.entities@0.1.1-preview/Unity.Entities.Properties/EntityContainer.cs:227)
Unity.Properties.PropertyVisitor.TryVisitCollectionWithAdapters[TProperty,TContainer,TValue] (TProperty property, TContainer& container, TValue& value, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:159)
Unity.Properties.PropertyVisitor.VisitCollectionProperty[TProperty,TContainer,TValue] (TProperty property, TContainer& container, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyVisitor.cs:87)
Unity.Entities.EntityContainerPropertyBag.Accept[TVisitor] (Unity.Entities.EntityContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.entities@0.1.1-preview/Unity.Entities.Properties/EntityContainer.cs:265)
Unity.Properties.PropertyContainer.Visit[TContainer,TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.ChangeTracker& changeTracker) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyContainerVisit.cs:29)
Unity.Properties.PropertyContainer.Visit[TContainer,TVisitor] (TContainer& container, TVisitor visitor, Unity.Properties.IVersionStorage versionStorage) (at Library/PackageCache/com.unity.properties@0.6.2-preview/Runtime/Unity.Properties/PropertyContainerVisit.cs:15)
Unity.Entities.Editor.EntitySelectionProxyEditor.OnInspectorGUI () (at Library/PackageCache/com.unity.entities@0.1.1-preview/Unity.Entities.Editor/EntityInspector/EntitySelectionProxyEditor.cs:44)
UnityEditor.UIElements.InspectorElement+<>c__DisplayClass55_0.<CreateIMGUIInspectorFromEditor>b__0 () (at <b43e6d4802d64ea8bbdaa0bf64614d3b>:0)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)

Blobs don’t work for Meshes, Materials, and other class types. Otherwise I would have recommended Blobs.

1 Like

How would I do this with blobs? I can move the Material/Mesh to an AssetReference.

The problem with the first approach you posted, is that I can’t use SetSingleton on it. I can only SetSingleton for IComponentData, not ISharedComponentData

Use AddComponentObject to add your scriptable object to the entity.
Use GetSingletonEntity on your query to get the entity and then get the component.

1 Like

You are using AssetReferences? That makes sense. I haven’t really investigated that world much, so I can’t comment about whether or not that’s a good idea. Is there a reason you are making a BlobArray of an ISharedComponentData type rather than some unmanaged struct?

But otherwise, if your game logic is working but the inspector isn’t, that either means you are missing some magic serialization attribute or Unity doesn’t support blobs yet. I haven’t figured that out yet. I typically do all my debugging using memory viewers, and it doesn’t help that my current project doesn’t have a use case for blobs yet.

I store an array of Items, each SharedComponent represents a different item in the game. then I just add the shared component from the blob array at runtime based on the item id. Apart from the error spam when I inspect the blob entity in the entity debugger at runtime it works perfectly so I think i’ll just keep that implementation. (This error spam happens no matter what even if it’s a shared component data so I have no idea what’s causing it)

Final Update: So I truly wanted to make sure that the error wasn’t something I did, so I copy and pasted the code from the BlobificationTests.cs in the Entities.test package. The error still occurs when inspecting an entity with the ComponentWithBlobData Component inside of it.

Update 2: Seeing as Blobs weren’t working, I moved over to using Buffers, works perfectly here is a code sample for anyone in the future.

Components/Buffers

public struct ItemDatabase : IComponentData {
        public static void ConstructDatabaseEntity(ComponentSystem system, IEnumerable<ItemDataScript> item_data_list) {
            var singleton = system.EntityManager.CreateEntity(typeof(ItemDatabase));
            system.SetSingleton(new ItemDatabase());
            system.EntityManager.AddBuffer<ItemData>(singleton);
            var buffer = system.EntityManager.GetBuffer<ItemData>(singleton);

            item_data_list.OrderBy((item) => item.Id).ForEach(item => {
                buffer.Add(new ItemData {
                    Id = item.Id,
                    Name = new NativeString64(item.Name),
                    Description = new NativeString64(item.Description),
                    MaxStackSize = item.MaxStackSize
                });
            });
        }
    }

    [Serializable]
    public struct ItemData : IBufferElementData {
        public int Id;
        public NativeString64 Name;
        public NativeString64 Description;
        public int MaxStackSize;
    }

    public struct Item : ISharedComponentData, IEquatable<Item> {
        public ItemData ItemData;

        public bool Equals(Item other) { return ItemData.Equals(other.ItemData); }

        public override bool Equals(object obj) { return obj is Item other && Equals(other); }

        public override int GetHashCode() { return ItemData.GetHashCode(); }
    }

System:

    [DisableAutoCreation]
    public class ItemSystem : ComponentSystem {
        private bool created;

        public async Task Init() {
            var items = await ScriptableObjectLoader.Load<ItemDataScript>(AddressableConstants.ITEM);

            ItemDatabase.ConstructDatabaseEntity(this, items);
        }

        protected override void OnCreate() { Debug.Log("Start"); }

        protected override void OnDestroy() { }

        protected void CreateItem(string id) { }

        //later
        protected override void OnUpdate() {
            if (!created) {
                var entity = EntityManager.CreateEntity(typeof(Item), typeof(Translation), typeof(LocalToWorld), typeof(RenderMesh));
                var item_data = EntityManager.GetBuffer<ItemData>(GetSingletonEntity<ItemDatabase>());

                EntityManager.SetSharedComponentData(entity, new Item {
                    ItemData = item_data[0]
                });
//                EntityManager.SetSharedComponentData(entity, item_data.RenderMesh);

                Debug.Log("Created");
                created = true;
            } else {
//                Entities.ForEach((ref Item i) => { Debug.Log(i.ItemData.Value.Id.ToString()); });
            }
        }
    }