NativeArray2D
Unity provides exactly one collection: NativeArray<T>
. Compared to managed arrays in C#, these must be one-dimensional. So today we’re building a two-dimensional version of it: NativeArray<T>
. We’ll add this to the NativeCollections GitHub repository for easy inclusion into any project. Read on to learn more about the collection!
The goal today is to make a two-dimensional version of NativeArray<T>
so we can write code like this:
// Create a 2x3 empty array NativeArray2D<int> array = new NativeArray2D<int>(2, 3, Allocator.Temp); // Set elements of the array array[0, 1] = 123; array[1, 2] = 456; // Get elements of the array int val123 = array[0, 1]; int val456 = array[1, 2]; // Iterate over the array foreach (int val in array) { Debug.Log(val); } // Copy to a managed array int[,] managed = new int[2, 3]; array.CopyTo(managed);
As part of the NativeCollections repo, the resulting code will need the following:
- Thorough xml-doc commenting
- Unit tests of every function
- Consistent source code formatting with the other types (e.g.
NativeLinkedList<T>
) - Containing the type to a single file so it can be taken a la carte
- An example in the README
Fundamentally, only the indexing really changes compared to NativeArray<T>
. However, the subtleties of Unity’s native collections system mean that there’s actually more to do that just change the indexer.
First, we must store two lengths since we have two dimensions:
private int m_Length0; private int m_Length1;
Correspondingly, our Enumerator
type that enables foreach
loops must store two indices:
private int m_Index0; private int m_Index1;
In order to traverse one row at a time, its MoveNext
becomes a bit more complicated:
public bool MoveNext() { m_Index0++; if (m_Index0 >= m_Array.Length0) { m_Index0 = 0; m_Index1++; return m_Index1 < m_Array.Length1; } return true; }
The Current
property remains very simple:
public T Current { get { return m_Array[m_Index0, m_Index1]; } }
This calls into the indexer for NativeArray2D
, which is where the indexing ultimately happens:
public T this[int index0, int index1] { get { RequireReadAccess(); RequireIndexInBounds(index0, index1); int index = index1 * m_Length0 + index0; return UnsafeUtility.ReadArrayElement<T>(m_Buffer, index); } [WriteAccessRequired] set { RequireWriteAccess(); RequireIndexInBounds(index0, index1); int index = index1 * m_Length0 + index0; UnsafeUtility.WriteArrayElement(m_Buffer, index, value); } }
There are a number of CopyFrom
and CopyTo
functions, in addition to ToArray
, for all the permutations of managed 2D arrays (T[,]
) and NativeArray2D<T>
. They all contain the same sort of copy loop:
for (int index0 = 0; index0 < src.Length0; ++index0) { for (int index1 = 0; index1 < src.Length1; ++index1) { dest[index0, index1] = src[index0, index1]; } }
Other than those differences, there’s a ton of boilerplate:
GetEnumerator
overloads- A
NativeArray2DDebugView
type Dispose
IsCreated
Equals
GetHashCode
==
and!=
- Copy constructors
Enumerator
functions likeReset
andDispose
- A bunch of error checking and commenting
The full source code and corresponding unit tests are available in the repository. Hopefully this type will be useful when dealing with 2D data such as is common with game boards and other grids.
#1 by Renan on November 11th, 2019 ·
Awesome! Thanks for sharing this !!
#2 by Tronster on November 12th, 2019 ·
There’s nothing quite like a good 2D loop. :) Thanks for this C# version.
#3 by josue on November 12th, 2019 ·
Awesome! I need you this !!! thanks for your work
#4 by David on March 2nd, 2020 ·
This is great.
I am not sure about 2 things:
A) Why doesn’t the iterator just treat it as a 1 array similar to how it is stored?
B) Why does it explicitly clear memory rather than letting the allocator do that? (True It’s probably only a win on some platforms)
#5 by jackson on March 2nd, 2020 ·
I’m glad you like it. :)
A) The current approach focuses more on code reuse than speed, but the extra code could possibly pay off in terms of performance.
B)
UnsafeUtility.Malloc
won’t clear the memory, soUnsafeUtility.MemClear
is necessary.