Looking for Feedback on Dynamic Buffers Pattern

I have come up with what I think is a neat pattern for using DynamicBuffers and am looking for some feedback before I get too carried away using it. Essentially it creates an easy and performant way to access individual values in a DynamicBuffer. Is there any performance concerns, or better ways to do something like this?

Here is a simple example of the pattern for a boolean state system.

The buffer data to be authored:

public enum StateType
{
    None = -1,
    Idle = 0,
    InCombat = 1,
}

public struct StateData : IBufferElementData 
{
    public StateType Type;
    public bool Active;
}

Abstract class to be used to author the different state types so they get added to the DynamicBuffer:

public abstract class StateAuthoring : MonoBehaviour, StateAuthoring.ICollect
{
    public interface ICollect
    {
        StateData GetState(IBaker baker);
    }
    //override in authoring implementation to return a StateData with a StateType
    public abstract StateData GetState(IBaker baker);
}

The buffer authoring class where the magic happens. It ensures that all enum values have a single entry in the DynamicBuffer. If there isn’t an associated authoring component for a state type it creates an empty placeholder to ensure enum indexes are always correct.

public class StatesAuthoring : MonoBehaviour
{
    class Baker : Baker<StatesAuthoring>
    {
        public override void Bake(StatesAuthoring authoring)
        {
            Entity entity = GetEntity(TransformUsageFlags.Dynamic);
            var statesBuffer = AddBuffer<StateData>(entity);

            var components = GetComponents<StateAuthoring>();
            var states = components.Select(c => c.GetState(this));
            foreach (StateType stateType in System.Enum.GetValues(typeof(StateType)))
            {
                if (stateType == StateType.None) continue; // Skip None
                try
                {
                    var existingState = states.First(s => s.Type == stateType);
                    if (existingState.Type == stateType)
                    {
                        //Add the stat to the buffer
                        statesBuffer.Add(existingState);
                    }
                    else
                    {
                        //Add a placeholder for the missing stat with StatType.None
                        statesBuffer.Add(new StateData
                        {
                            Type = StateType.None,
                            active = false
                        });
                    }
                }
                catch (Exception e)
                {
                    //Add a placeholder for the missing stat with StatType.None
                    statesBuffer.Add(new StateData
                    {
                        Type = StateType.None,
                        active = false
                    });
                }
            }
        }
    }
}

Simple usage example:

var combatState = stateBuffer[(int)StateType.InCombat];

//Is this a valid state in the buffer
if(combatState != StateType.None)
{
    if(combatState.Active)
    {
         //Do something that depends on the state being active
    }
}

Based on the example you provided, it could be very expensive. You either will hurt your chunk capacity, or you move your buffer out of the chunk and deal with a random pointer access on every entity you check. Plus if you are checking different element offsets in different jobs, you are always iterating over memory you don’t need. And that’s pretty expensive if most of the entities don’t match the criteria you are checking for in a given frame.

I suspect IEnableableComponent may be more efficient for what you describe.

However, if your enum values are also random and you are doing something highly data-driven, then this approach makes a lot more sense. In that case, I would strongly encourage you to try and optimize the amount of memory each element needs. Right now, you have 8 bytes per element, and it should be possible to pack that down to 1 or fewer. That can also maybe justify a non-zero internal buffer capacity if you have eight or fewer elements.

Not sure I fully understand, particularly the chunk and pointer parts. I assumed by using an index on the buffer I would not be iterating over memory, just direct access like a Dictionary. Is it because any system or job I use the buffer in puts the whole buffer into memory?

I knew that having empty value placeholders was a waste of memory, but if used sparingly wouldn’t be too much of an issue. I try to use this pattern when there won’t be many, if any unused values in the buffer. Though I am referencing different things in the dynamic buffer from several systems.

My original use of the pattern was for a stat system and my goal was to reduce the number of components with identical fields for each stat. All my stats needed a min, a max and a value. By using this pattern I was able to create a simple way to change any stat:

    [BurstCompile]
    public static void ChangeStat(in StatType statType, ref DynamicBuffer<StatData> statsBuffer, in int amount, out int value)
    {
        var stat = statsBuffer[(int)statType];
        if (stat.Type != StatType.None)
        {
            stat.Value = math.clamp(stat.Value + amount, stat.MinValue, stat.MaxValue);
            statsBuffer[(int)statType] = stat;
        }
        value = stat.Value;
    }

Maybe I am trying to force object orientedness into ECS :thinking:

Cache lines are typically 64 bytes. So it doesn’t drag in the whole buffer, nor does it drag in just the one value, but rather it drags in the value and a little bit around it. And dragging this in is slow. When you iterate linearly, it starts to guess at what to drag in in advance to speed it up.

If the stat to change isn’t hard-coded and instead a value stored in some other component that gets triggered based on other conditions, then you are entering data-driven territory in which this kind of approach makes more sense. That’s because at compile time you have no idea which element you will be accessing, and therefore don’t have a good way to ensure linear iteration of your data.

I don’t think I am doing this, each system/job that calls it uses a hardcoded StatType to change.

Sounds like I should refactor this pattern out then in my case and go back to an individual ComponentData (or IEnableableComponent for my bool example) for each. It just gets so big and messy, my object oriented brain gets overwhelmed haha :weary:

Thanks for detailed explination :slight_smile: