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?
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>.
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.
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:
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
.
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.
HiďźSpy-Master. Thatâs a very detailed instruction for the problem that I am facing. Truly appreciate it.