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 like Reset and Dispose
  • 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.