The series continues by adding support for a major feature: arrays. These are used very frequently throughout the Unity and .NET APIs and the lack of support for them has been a big missing piece of the puzzle for most games. The GitHub project has been updated to support single- and multi-dimensional arrays. Read on to learn how this support was implemented!

Table of Contents

Conceptually, adding support for arrays sounds easy but there are a lot of little details to handle. Today we’ll walk through them one by one.

First of all, we’re talking about managed arrays today. These are the array types we have in C#, such as int[] and string[]. Though they have a similar syntax, we’re not talking about unmanaged arrays like we have in C++. There’s simply nothing to do to support unmanaged arrays, so we’ll focus on using managed arrays in the following ways:

  • Array constructors: new int[5]
  • Array properties: a.Length and a.Rank
  • Array methods: a.GetLength(5)
  • Array indexing: x = a[5] and a[5] = x
  • Arrays as parameters: void Foo(int[] a) and MyClass(int[] a)
  • Arrays as out parameters: void Foo(out int[] a)
  • Arrays as fields: class MyClass { int[] A; }
  • Arrays as properties: int[] A { get; set; }

It may seem like we already have support for all of this. After all, arrays are just a class with an overloaded array index operator, some properties, and some methods, right? Unfortunately, no. It turns out that these similarities are only superficial and there are many differences that need to be accounted for, especially syntactically. They’re so different from struct and class types that they need their own section of the code generator’s JSON config file:

{
	"Arrays": [
		{
			"Type": "System.Int32"
		},
		{
			"Type": "System.Single",
			"Ranks": [ 1, 2, 3 ]
		}
	}
}

Here we specify the types we want to expose to C++ as arrays. Optionally, we can specify the “rank” of the array to get access to 1D, 2D, 3D arrays, etc. By default, we just get a 1D array which is by far the most common. So the above JSON says that we want C++ to have access to int[], float[], float[,] (2D), and float[,,] (3D).

Next, we’ll need to establish a C++ type for arrays. We can’t use int[] because that already has a specific meaning in C++: unmanaged array. Instead, we’ll use the System.Array name on the C++ side like this:

// Base class of all arrays
// Not generic, just like in C#
// Provides basic properties: Length and Rank
struct Array : Object
{
	int32_t GetLength();
	int32_t GetRank();
};
 
// Arrays of various dimensions
template <typename TElement> Array1;
template <typename TElement> Array2;
template <typename TElement> Array3;
template <typename TElement> Array4;
template <typename TElement> Array5;
 
// Specific array types output by the code generator
template<> struct Array3<float> : System::Array
{
	// Construct with a given length, like "new float[length0, length1, length2]"
	Array3(int32_t length0, int32_t length1, int32_t length2);
 
	// Length property, uses the base Array class
	int32_t GetLength();
 
	// Get the length of a specific dimension
	int32_t GetLength(int32_t dimension);
 
	// Rank property, uses the base Array class
	int32_t GetRank();
 
	// Get an item at given indexes, like "x = array[index0, index1, index2]"
	float GetItem(int32_t index0, int32_t index1, int32_t index2);
 
	// Get an item at given indexes, like "array[index0, index1, index2] = x"
	void SetItem(int32_t index0, int32_t index1, int32_t index2, float item);
}
 
// Basic usage
Array1<String> array(10);
String elem("howdy");
array.SetItem(5, elem);
String item = array.GetItem(5);
Debug::Log(item); // prints "howdy"

This seems like a reasonable alternative to the int[] syntax we use in C#. Instead, it’ll feel more like generic collections such as List<T>. We also have access to all the Array base class functionality and polymorphism so we can pass any array type (e.g. Array1<float>) as an Array, just like in C#.

With this basis in place we can start implementing the code generator to output these array types. This is all based on .NET’s reflection functionality in System.Reflection and it’s well-equipped to handle array types. There are handy properties like Type.IsArray to identify arrays as method parameters, constructor parameters, properties, and fields. One important difference between arrays and other types (e.g. classes) is that they have different names in C# and C++. We simply have to call them int[] in C# and have to not call them int[] in C++. So when we encounter a Type.IsArray we can output one name for C# (int[]) and another for C++ (Array1<int32_t>.

The reflection functionality also provides us with Type.MakeArrayType() and Type.MakeArrayType(int) to make an array Type out of an element Type. So we can take the System.Single specified in JSON, get the Type for it, and then call Type.MakeArrayType to get the Type for float[]. That’s really handy since it allows our chosen JSON config format of specifying the type (System.Single) and the ranks [1, 2, 3].

One tricky aspect is that Type.MakeArrayType() and Type.MakeArrayType(1) are not synonymous. Consider the following snippet:

Debug.Log(typeof(float).MakeArrayType().Name); // prints "float[]"
Debug.Log(typeof(float).MakeArrayType(1).Name); // prints "float[*]"

Ther reason is that single-dimensional arrays like float[] are actually “vectors” in .NET. They get special treatment in the bytecode compared to multi-dimensional arrays that happen to have only one dimension. So float[*] indicates a multi-dimensional array that has only one dimension but that is not the same as “vector” which is what the float[] means. Unfortunately, this means that the Name here is invalid C# since we can’t write float[*].

There are more details to supporting arrays, but they’re mostly only important when working on the code generator. Instead of diving into those details, let’s look at some examples to see how to use arrays from our C++ scripts. First, let’s look at a little snippet that calls a function returning an array and then loops over it with a for loop.

// Call a function returning an array
Array1<Resolution> resolutions = Screen::GetResolutions();
 
// Loop over an array as usual
for (int32_t i = 0, len = resolutions.GetLength(); i < len; ++i)
{
	// Get the current element of the array
	Resolution resolution = resolutions.GetItem(i);
}

Now let’s try using an array as a parameter:

// Create a ray
Vector3 origin(-10, 0, 0);
Vector3 direction(1, 0, 0);
Ray ray(origin, direction);
 
// Create an array to hold the results
Array1<RaycastHit> results(10);
 
// Pass the array as a parameter
int32_t numHits = Physics::RaycastNonAlloc(ray, results);

Here’s a snippet to use an array property as both a “getter” and a “setter”:

// Create an array and set one of the elements
Array1<GradientColorKey> colors(4);
GradientColorKey colorKey;
colorKey.color.r = 1;
colorKey.color.g = 0;
colorKey.color.b = 0;
colors.SetItem(0, colorKey);
 
// Use the property "setter"
Gradient gradient;
gradient.SetColorKeys(colors);
 
// Use the property "getter"
Array1<GradientColorKey> gotColors = gradient.GetColorKeys();

Finally, let’s use a multi-dimensional array:

// Make a multi-dimensional array of floats
Array2<float> array(2, 3);
 
// Get its overall length (6) from the Length property
int32_t overallLength = array.GetLength();
 
// Get its rank (2) from the Rank property
int32_t rank = array.GetRank();
 
// Loop over the dimensions of the array
for (int32_t i = 0; i < rank; ++i)
{
	// Get the length of the current dimension (2 then 3)
	int32_t len = array.GetLength(i);
}
 
// Set the element at [1, 2]
array.SetItem(1, 2, 3.14f);
 
// Get the element at [1, 2]
float item = array.GetItem(1, 2);

That’s about all for arrays for today. There are some improvements to be added in the future, such as supporting the subscript operator, but this functionality should be enough for almost any game. The above code snippets show how we now have access to important APIs like Physics.Raycast in Unity and many more in .NET. We are, once again, one step closer to a viable C++ scripting environment. At this point, there aren’t even many major items left on the list to implement. Still, we’ll press on in next week’s article with further enhancements to the system.

Check out the GitHub project to get access to the array functionality or to read through the details of how it was implemented. If you’re interested in collaborating, please leave a comment or e-mail me.