using System; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using UnityEngine; namespace NativeQuadTree { // Represents an element node in the quadtree. public struct QuadElement where T : unmanaged { public float2 pos; public T element; } struct QuadNode { // Points to this node's first child index in elements public int firstChildIndex; // Number of elements in the leaf public short count; public bool isLeaf; } /// /// A QuadTree aimed to be used with Burst, supports fast bulk insertion and querying. /// /// TODO: /// - Better test coverage /// - Automated depth / bounds / max leaf elements calculation /// public unsafe partial struct NativeQuadTree : IDisposable where T : unmanaged { #if ENABLE_UNITY_COLLECTIONS_CHECKS // Safety AtomicSafetyHandle safetyHandle; [NativeSetClassTypeToNullOnSchedule] DisposeSentinel disposeSentinel; #endif // Data [NativeDisableUnsafePtrRestriction] UnsafeList>* elements; [NativeDisableUnsafePtrRestriction] UnsafeList* lookup; [NativeDisableUnsafePtrRestriction] UnsafeList* nodes; int elementsCount; int maxDepth; short maxLeafElements; AABB2D bounds; // NOTE: Currently assuming uniform /// /// Create a new QuadTree. /// - Ensure the bounds are not way bigger than needed, otherwise the buckets are very off. Probably best to calculate bounds /// - The higher the depth, the larger the overhead, it especially goes up at a depth of 7/8 /// public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int maxDepth = 6, short maxLeafElements = 16, int initialElementsCapacity = 256 ) : this() { this.bounds = bounds; this.maxDepth = maxDepth; this.maxLeafElements = maxLeafElements; elementsCount = 0; if(maxDepth > 8) { // Currently no support for higher depths, the morton code lookup tables would have to support it throw new InvalidOperationException(); } #if ENABLE_UNITY_COLLECTIONS_CHECKS // TODO: Find out what the equivalent of this is in latest entities // CollectionHelper.CheckIsUnmanaged(); DisposeSentinel.Create(out safetyHandle, out disposeSentinel, 1, allocator); #endif // Allocate memory for every depth, the nodes on all depths are stored in a single continuous array var totalSize = LookupTables.DepthSizeLookup[maxDepth+1]; lookup = UnsafeList.Create( totalSize, allocator, NativeArrayOptions.ClearMemory); nodes = UnsafeList.Create( totalSize, allocator, NativeArrayOptions.ClearMemory); elements = UnsafeList>.Create( initialElementsCapacity, allocator); } public void ClearAndBulkInsert(NativeArray> incomingElements) { // Always have to clear before bulk insert as otherwise the lookup and node allocations need to account // for existing data. Clear(); #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif // Resize if needed if(elements->Capacity < elementsCount + incomingElements.Length) { elements->Resize(math.max(incomingElements.Length, elements->Capacity*2)); } // Prepare morton codes var mortonCodes = new NativeArray(incomingElements.Length, Allocator.Temp); var depthExtentsScaling = LookupTables.DepthLookup[maxDepth] / bounds.Extents; for (var i = 0; i < incomingElements.Length; i++) { var incPos = incomingElements[i].pos; incPos -= bounds.Center; // Offset by center incPos.y = -incPos.y; // World -> array var pos = (incPos + bounds.Extents) * .5f; // Make positive // Now scale into available space that belongs to the depth pos *= depthExtentsScaling; // And interleave the bits for the morton code mortonCodes[i] = (LookupTables.MortonLookup[(int) pos.x] | (LookupTables.MortonLookup[(int) pos.y] << 1)); } // Index total child element count per node (total, so parent's counts include those of child nodes) for (var i = 0; i < mortonCodes.Length; i++) { int atIndex = 0; for (int depth = 0; depth <= maxDepth; depth++) { // Increment the node on this depth that this element is contained in (*(int*) ((IntPtr) lookup->Ptr + atIndex * sizeof (int)))++; atIndex = IncrementIndex(depth, mortonCodes, i, atIndex); } } // Prepare the tree leaf nodes RecursivePrepareLeaves(1, 1); // Add elements to leaf nodes for (var i = 0; i < incomingElements.Length; i++) { int atIndex = 0; for (int depth = 0; depth <= maxDepth; depth++) { var node = UnsafeUtility.ReadArrayElement(nodes->Ptr, atIndex); if(node.isLeaf) { // We found a leaf, add this element to it and move to the next element UnsafeUtility.WriteArrayElement(elements->Ptr, node.firstChildIndex + node.count, incomingElements[i]); node.count++; UnsafeUtility.WriteArrayElement(nodes->Ptr, atIndex, node); break; } // No leaf found, we keep going deeper until we find one atIndex = IncrementIndex(depth, mortonCodes, i, atIndex); } } mortonCodes.Dispose(); } int IncrementIndex(int depth, NativeArray mortonCodes, int i, int atIndex) { var atDepth = math.max(0, maxDepth - depth); // Shift to the right and only get the first two bits int shiftedMortonCode = (mortonCodes[i] >> ((atDepth - 1) * 2)) & 0b11; // so the index becomes that... (0,1,2,3) atIndex += LookupTables.DepthSizeLookup[atDepth] * shiftedMortonCode; atIndex++; // offset for self return atIndex; } void RecursivePrepareLeaves(int prevOffset, int depth) { for (int l = 0; l < 4; l++) { var at = prevOffset + l * LookupTables.DepthSizeLookup[maxDepth - depth+1]; var elementCount = UnsafeUtility.ReadArrayElement(lookup->Ptr, at); if(elementCount > maxLeafElements && depth < maxDepth) { // There's more elements than allowed on this node so keep going deeper RecursivePrepareLeaves(at+1, depth+1); } else if(elementCount != 0) { // We either hit max depth or there's less than the max elements on this node, make it a leaf var node = new QuadNode {firstChildIndex = elementsCount, count = 0, isLeaf = true }; UnsafeUtility.WriteArrayElement(nodes->Ptr, at, node); elementsCount += elementCount; } } } public void RangeQuery(AABB2D bounds, NativeList> results) { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); #endif new QuadTreeRangeQuery().Query(this, bounds, results); } public void Clear() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif UnsafeUtility.MemClear(lookup->Ptr, lookup->Capacity * UnsafeUtility.SizeOf()); UnsafeUtility.MemClear(nodes->Ptr, nodes->Capacity * UnsafeUtility.SizeOf()); UnsafeUtility.MemClear(elements->Ptr, elements->Capacity * UnsafeUtility.SizeOf>()); elementsCount = 0; } public void Dispose() { UnsafeList>.Destroy(elements); elements = null; UnsafeList.Destroy(lookup); lookup = null; UnsafeList.Destroy(nodes); nodes = null; #if ENABLE_UNITY_COLLECTIONS_CHECKS DisposeSentinel.Dispose(ref safetyHandle, ref disposeSentinel); #endif } } }