Process different type of dynamic buffer with same structure using single method?

For example, i have 2 buffers

public struct BufferElement1 : IBufferElementData {int value;}
public struct BufferElement2 : IBufferElementData {int value;}

If i want to sort the buffers, do i have to write a sorting method for each buffer?

You can get a NativeArray alias of the DynamicBuffer, which can then then be used with an appropriate sorting method in NativeSortExtension. The act of invoking sorting is therefore very simple (buffer.AsNativeArray().Sort()).

Like with sorting anything, you will need to define the sorting behaviour. In the case of Unity Collections, you would need to either have the type implement IComparable<T> or define a comparer type that implements IComparer<T>.

1 Like

Hi! Spy-master. thanks for the answer.
I was wondering about the efficiency of turning it into a native array. Because it would iterate all items . So for example, if i were to find a value in the buffer using binary search. It would make no sense to turn the buffer into native array first.
Plus, i’m afraid that i can’t call buffer.AsNativeArray() inside a job.

As I stated, AsNativeArray simply aliases the existing memory. There is no iteration going on, and it’s perfectly reasonable to use this when sorting or performing a binary search on a sorted buffer.

The method is usable inside jobs. Show example code and relevant errors if you see a problem.

2 Likes

Thank you! So here is one of the problem that I am having.

I have two buffers, BuildingLiftQuests_U and BuildingLiftQuests_D, to store the calls for lifts in a building entity, one for upward lifts, the other for downward lifts. They are keeped sorted in ascending order.

    public struct BuildingLiftQuests_U : IBufferElementData
    {
        public int floor;
    }
    
    public struct BuildingLiftQuests_D : IBufferElementData
    {
        public int floor;
    }

In a job iterating all building entities, i want to insert new items into the buffers:

  1. check if item is valid
  2. check if it already exists and find the insert position
  3. insert the item

I have to write two almost identical jobs just because these two buffers are different in type. For the binary search part, I already hided it in the ElementUtil.FindLiftQuest method, which has an overload for each buffer.

      #region register up
        [BurstCompile]
        partial struct JRegister_U : IJobEntity
        {
            [ReadOnly] public NativeHashMap<Entity, Tuple<int, int>> liftInfos;
            public void Execute(DynamicBuffer<BuildingLiftQuests_U> oldQuests, DynamicBuffer<BuildingLifts> lifts, DynamicBuffer<BuildingLiftQuestsBuffer_U> newQuests)
            {
                for (int i = 0; i < newQuests.Length; i++)
                {
                    if (!_checkValid(newQuests[i].floor, lifts)) continue;

                    if (ElementUtil.FindLiftQuest(oldQuests, newQuests[i].floor, out int insertPoint)) continue;

                    oldQuests.Insert(insertPoint, new() { floor = newQuests[i].floor });
                }
                newQuests.Clear();
            }

            bool _checkValid(int questFloor, DynamicBuffer<BuildingLifts> lifts)
            {
                for (int i = 0; i < lifts.Length; i++)
                {
                    if (!liftInfos.ContainsKey(lifts[i].lift)) continue;
                    
                    var liftInfo = liftInfos[lifts[i].lift];
                    if ((liftInfo.Item1 == 0 || liftInfo.Item1 == 1) && liftInfo.Item2 == questFloor)
                        return false;
                }
                return true;
            }
        }
        #endregion

        #region register down
        [BurstCompile]
        partial struct JRegister_D : IJobEntity
        {
            [ReadOnly] public NativeHashMap<Entity, Tuple<int, int>> liftInfos;
            public void Execute(DynamicBuffer<BuildingLiftQuests_D> oldQuests, DynamicBuffer<BuildingLifts> lifts, DynamicBuffer<BuildingLiftQuestsBuffer_D> newQuests)
            {
                for (int i = 0; i < newQuests.Length; i++)
                {
                    if (!_checkValid(newQuests[i].floor, lifts)) continue;

                    if (ElementUtil.FindLiftQuest(oldQuests, newQuests[i].floor, out int insertPoint)) continue;

                    oldQuests.Insert(insertPoint, new() { floor = newQuests[i].floor });
                }
                newQuests.Clear();
            }

            bool _checkValid(int questFloor, DynamicBuffer<BuildingLifts> lifts)
            {
                for (int i = 0; i < lifts.Length; i++)
                {
                    var liftInfo = liftInfos[lifts[i].lift];
                    if ((liftInfo.Item1 == 0 || liftInfo.Item1 == -1) && liftInfo.Item2 == questFloor)
                        return false;
                }
                return true;
            }
        }
        #endregion

Your buffer element has only one int field. You can reinterpet your buffer into DynamicBuffer<int> like so:
var bufferAsInt = oldQuests.Reinterpret<int>();
Then get a native array:
var dynamicBufferAsNativeArray = bufferAsInt.AsNativeArray<int>();

Modify your job so it has fields:
public NativeArray<int> oldQuests;
public NativeArray<int> newQuests;
Your job can now operate on native arrays instead of dynamic buffers of specific types.

https://docs.unity3d.com/Packages/com.unity.entities@1.3/manual/components-buffer-reinterpret.html

You have an extra check on line 23 that doesn’t exist in the other method. If there’s logic differences like this, then it would be rough going trying to unify the logic. If the difference not intentional and all your similar implementations actually do the same exact thing, you can for the most part combine things into generic methods based on interface constraints as necessary, like a single generic method that takes T of IComparable<T> for enabling direct sorting.

If you know for certain the element type will always be a wrapper struct (over a single sortable element and no custom struct size / layout), then you could just reinterpret the NativeArray as that type and sort directly without implementing your own comparer. Otherwise, you’d need an IComparable<T> implementation or a comparer type. If there are any fields you need to access in the generic method that all the types in question have in common, you can introduce accessors in an interface, like

public interface ILiftInfo
{
  public int Floor { get; set; }
}

public struct BuildingLiftQuests_U : IBufferElementData, ILiftInfo
{
        public int floor;

        int ILiftInfo.Floor
        {
            get => floor;
            set => floor = value;
        }
}

You could do something similar (interface) with methods on each element, but that could require passing more arguments to the main generic method, perhaps with even more generic type arguments, and at some point, bending over backwards to accommodate logic differences between different types to achieve a shared implementation isn’t worth it.

At the very least, implementing the binary search part as generic should be simple enough, so I would start with that. Given that Unity Collections has sorting and binary search methods ready to go, it seems to me that you could make a simple generic wrapper that exposes the same parameters for your FindLiftQuest method but uses NativeSortExtension.BinarySearch under the hood. In case you’re not familiar, for return value value, you can use value as the insertion point when value >= 0 (insert at the position of an existing element that compares the same, moving the existing element and all following elements back) or ~value if the value is negative (insert between the appropriate neighbors, or at the start / end when applicable).

I am suspicious about your use of Tuple<int, int>. I presume this is a custom struct type, as System.Tuple<T1,T2> is a class and will not be usable as the value type argument for the hash map. You can instead easily use value tuples (ValueTuple<int, int> / (int, int)) or use Unity.Mathematics.int2.

1 Like

Hi! Justyna. Thanks for answering. This approach works if I schedule jobs one building by one building. But it doesn’t work for IJobEntity, because every building has an oldQuests and a newQuests.

Hi! Spy-Master. Thanks for your answer. The extra check is unintentional. The reinterpret method which I didn’t know looks very useful. Can I use it in a job? So the code looks like this. Do I understand correctly?

public struct LiftQuest
{
    var commonFields;
}

partial struct JRegister_U : IJobEntity
{
    public void Execute(DynamicBuffer<BuildingLiftQuests_U> oldQuests_U, ...)
    {
        DynamicBuffer<LiftQuest> oldQuests = oldQuests_U.Reinterpret<LiftQuest>();
        GenericMethod(oldQuests, ...)
    }
}

partial struct JRegister_D : IJobEntity
{
    public void Execute(DynamicBuffer<BuildingLiftQuests_D> oldQuests_D, ...)
    {
        DynamicBuffer<LiftQuest> oldQuests = oldQuests_D.Reinterpret<LiftQuest>();
        GenericMethod(oldQuests, ...)
    }
}

As for the interface, did you mean it helps to process a single item in the buffer, instead of processing the whole buffer? Because you can’t pass a DynamicBuffer<LiftQuest_U> into Method(DynamicBuffer<ILiftQuest> liftQuest), right?

As for the NativeSortExtension.BinarySearch, how can I use it in jobs? It seems that the method only takes native collectors, but not dynamicBuffers.

That’s one way of going about it, yes. This requires all types in question to have the exact same data layout so they would be interchangeable in memory. This would correspond to “type A1” that I show in the code outline below, and it is indeed a valid way of doing things. You wouldn’t really be using generics then, and you can make it a shared, non-generic method.

Pretty much everything I’ve mentioned up to now is usable in jobs and Burst-compatible.

If you’re not going to reinterpret the buffer as something that’s already sortable (anything that declares IComparable<T> on itself like integer/float types or already has associated IComparer<T>), you will need to implement an IComparable<T> on the buffer element type you’re using (all buffer element types if using generics) or a IComparator<T> to specify a sorting order.

// -------- type A1: 1 type for reinterpretation, using IComparable<T>

public struct LiftQuest : IComparable<LiftQuest>
{
    // fields...
	
	public int CompareTo(LiftQuest other)
	{
	    // ...
	}
}

public static void CommonMethod(DynamicBuffer<LiftQuest> buffer)
{
    buffer.AsNativeArray().Sort();
    // ...
}

DynamicBuffer<BuildingLiftQuest_D> buffer;
CommonMethod(buffer.Reinterpret<LiftQuest>());

// -------- type A2: 1 type for reinterpretation, using IComparer<T>

public struct LiftQuest
{
    // fields...
}

public struct LiftQuestComparer : IComparer<LiftQuest>
{
    public int Compare(LiftQuest left, LiftQuest right)
	{
	    // ...
	}
}

public static void CommonMethod(DynamicBuffer<LiftQuest> buffer)
{
    buffer.AsNativeArray().Sort(new LiftQuestComparer());
    // ...
}

DynamicBuffer<BuildingLiftQuest_D> buffer;
CommonMethod(buffer.Reinterpret<LiftQuest>());

// -------- type B1: common interface, using IComparable<T>

public interface ILiftQuest
{
    // accessors...
}

public struct BuildingLiftQuest_D : IBufferElementData, ILiftQuest, IComparable<BuildingLiftQuest_D>
{
    // fields...
	
	public int CompareTo(BuildingLiftQuest_D other)
	{
	    // ...
	}
}

public static void CommonMethod<T>(DynamicBuffer<T> buffer)
    where T : unmanaged, ILiftQuest, IComparable<T>
{
    buffer.AsNativeArray().Sort();
    // ...
}

DynamicBuffer<BuildingLiftQuest_D> buffer;
CommonMethod(buffer);

// -------- type B2: common interface, using IComparer<T>

public interface ILiftQuest
{
    // accessors...
}

public struct BuildingLiftQuest_D : IBufferElementData, ILiftQuest
{
    // fields...
	
	public int CompareTo(BuildingLiftQuest_D other)
	{
	    // ...
	}
}

public struct BuildingLiftQuest_DComparer : IComparer<BuildingLiftQuest_D>
{
    
    public int Compare(BuildingLiftQuest_D left, BuildingLiftQuest_D right)
	{
	    // ...
	}
}

public static void CommonMethod<T, U>(DynamicBuffer<T> buffer, U comparer = default)
    where T : unmanaged, ILiftQuest
    where U : unmanaged, IComparer<T>
{
    buffer.AsNativeArray().Sort(comparer);
    // ...
}

DynamicBuffer<BuildingLiftQuest_D> buffer;
CommonMethod(buffer, default(BuildingLiftQuest_DComparer));

For that sample above, if you don’t need to add/remove elements in “CommonMethod” you can just use NativeArray as a parameter for the method instead of DynamicBuffer, as there’s nothing special about DynamicBuffer that you need.

Yes, using an interface is mainly to help deal with any element-specific data you need to extract in your shared implementation, like floor in your code. If you know for certain all the different buffer element types you need to handle are going to have the same memory layout, you can just use the reinterpret-to-single-type as you suggest, and if not, having an interface for the element data is the way to go.

I will say, that second bit, Method(DynamicBuffer<ILiftQuest> liftQuest) doesn’t quite make sense - the type argument needs to be an unmanaged type to meet DynamicBuffer<T>'s type constraint on T, so the method definition would more appropriately be Method<T>(DynamicBuffer<T> liftQuest) where T : unmanaged, ILiftQuest. Once you’ve defined such a generic method and LiftQuest_U implements ILiftQuest, you can then directly pass DynamicBuffer<LiftQUest_U> to the method because the type LiftQuest_U satisfies the constraint unmanaged, ILiftQuest.

You can use DynamicBuffer<T>.AsNativeArray() to convert any given DynamicBuffer<T> to a NativeArray<T> that will be usable with NativeSortExtension and anything like element read/write. See the above code for examples. The only limitation is that adding/removing/resizing/etc. (anything that requires re-allocating the underlying memory) will leave the NativeArray pointing to the old and now invalid memory. You can simply call AsNativeArray again to get the current version.

2 Likes

Hi!Spy-Master. That’s a very detailed instruction for the problem that I am facing. Truly appreciate it.

1 Like