2
\$\begingroup\$

I'm new to ECS and am having issues finding the right data structure to use.

For context, I have a galaxy with solar systems that produce resources at a given rate (ResourceProducer component). Resources are tallied in a stockpile (ResourceStockpile) and the individual types are defined in a Resources enum.

public struct ResourceProducer : IComponentData {
    public Resources resourceType;
    public float quantity;
    public float interval;
}

(ResourceStockpile is the same but no interval)

I had originally intended to have multiple producers and stockpiles per location, one per resource type, but Unity objects to having multiple of the same component on a single entity (and I've seen some Stack Overflow posts supporting this philosophy).

So I then created a

public struct ResourceQuantity {
    public Resources resourceType;
    public float quantity;
}

and changed my stockpile to use

public ResourceQuantity[] resources;

(Planning to worry about production rate later as I can't represent it here. Also, I'd prefer a list, but an array seemed more appropriate given the error below)

But now I get an issue in my baker....

public class ResourceStockpileBaker : Baker<ResourceStockpileAuthoring> {
    public override void Bake(ResourceStockpileAuthoring authoring) {
        Entity entity = this.GetEntity(new TransformUsageFlags());
        AddComponent(entity, new ResourceStockpile {
            resources = authoring.resources
        }) ;
    }
}

AddComponent fails to compile with

The type 'ResourceStockpile' must be a non-nullable value type, along with all fields at any level of nesting, in order to use it as parameter 'T' in the generic type or method 'IBaker.AddComponent(Entity, in T)'

It feels like I may be dealing with an XY problem... I'm going down a rabbit hole without knowing it's the right one.

What's the correct way to represent an entity that produces multiple resources at different rates, to be accumulated in a stockpile for later usage?

\$\endgroup\$
2
  • \$\begingroup\$ Are your resource producers of a limited number of types (e.g. "spice mine", "laboratory", etc.) or do they get individually customised values on every producer? \$\endgroup\$ Jul 7, 2023 at 8:36
  • 1
    \$\begingroup\$ @JackAidley I was aiming for something more granular.. Systems can produce a number of elements like iron, carbon, oxygen, etc... and an number of finished components built from those. I'm currently expecting to use 8-10 elements but throw in various components and it's potentially dozens of "types". \$\endgroup\$
    – Basic
    Jul 8, 2023 at 12:46

1 Answer 1

3
\$\begingroup\$

If you want a component that doesn't have a 1:1 relationship with entities but a n:1 relationship, as in "an entity can have multiple instances of that component", then you need a Dynamic Buffer Component.

A dynamic buffer component is basically a component that is not a single structure but an array of structures.

I actually created a ECS prototype of a game that seems very similar to yours. That was about a year ago, so considering the speed with which DOTS evolves it probably no longer follows the idiomatic style of the week. But this is how I solved this back then.

This is my ResourceStorage structure. Each entity with an inventory has one per resource.

public enum ResourceType : short {
    METAL = 0,
    FOOD = 1
    // Remember to update the InternalBufferCapacity of the ResourceStorage buffer component below when adding more resources
}

[InternalBufferCapacity(2)] 
public struct ResourceStorage : IBufferElementData {
    public ResourceType type;
    public float max;
    public float current;
}

This is the code that adds an inventory to an entity:

public void SetResourceStorageData(IEnumerable<ResourceStorage> values, Entity entity) {
    EntityManager manager = World.DefaultGameObjectInjectionWorld.EntityManager;
    DynamicBuffer<ResourceStorage> buffer;
    buffer = manager.AddBuffer<ResourceStorage>(entity);

    foreach (ResourceStorage entry in values) {
        buffer.Add(entry);
    }
}

This is the resource production system that increases it:

public partial class ResourceProductionSystem : SystemBase
{
    protected override void OnUpdate()
    {

        float deltaTime = SystemAPI.Time.DeltaTime;
        
        Entities.ForEach((DynamicBuffer<model.ResourceStorage> storages) => {
            for (var i = 0; i < storages.Length; i++) {
                var storage = storages[i];
                storage.current += deltaTime;
                storage.current = math.clamp(storage.current, 0.0f, storage.max);
                storages[i] = storage;                
            }
        }).ScheduleParallel();
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ Thank you, that sounds very much like what I'm looking for. To clarify my understanding, are you effectively populating a single shared array for each entity, with each resource using a different index? I ask due to the InternalBufferCapacity decorator \$\endgroup\$
    – Basic
    Jul 8, 2023 at 12:58
  • \$\begingroup\$ The [InternalBufferCapacity] attribute sets the expected(!) maximum number of entries per entity. So in this example, every entity has its own buffer, and that buffer is expected to have up to two entries. You can add more, but that causes chaos in the memory layout which might degrade performance. The whole buffer acts as a single component. Not sure what you mean with "shared". Shared between systems? Yes. Shared between entities? No. \$\endgroup\$
    – Philipp
    Jul 8, 2023 at 13:12
  • \$\begingroup\$ I meant "Shared between components on a single entity" I was imagining that the data structure internally would be [0,0,0,0] with -say- Iron indexed to 0, Carbon indexed to 1, etc... So an entity with 2 resource components [say 50 iron & 10 carbon] would be represented in memory something like [50,100,0,0]. Doesn't matter, worst case, I can get it working and start digging through memory/profilers to see what it's doing under the covers. \$\endgroup\$
    – Basic
    Jul 8, 2023 at 13:35
  • \$\begingroup\$ Actually, the documentation on InternalBufferCapacity clarifies anwyway. It's not indexing per-se, it's just setting a "memory stride" to leave enough space for up to n of a component. \$\endgroup\$
    – Basic
    Jul 8, 2023 at 13:37
  • 1
    \$\begingroup\$ @Basic Yes, I think you understood what I was trying to say. The buffer is not "shared between components on a single entity" because each buffer is a component on a single entity (or at least behaves like one for most intents and purposes). \$\endgroup\$
    – Philipp
    Jul 8, 2023 at 14:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .