Introduction to Game Design, Prototyping, and Development (2015)

Part II: Digital Prototyping

Chapter 22. Lists and Arrays

This chapter covers two important types of collections available to you in C#. These collections enable you to act on several things as a group. For example, you could loop over a List of GameObjects each frame to update all of their positions and states.

By the end of this chapter, you will understand how these collection types work and which to use in various situations.

C# Collections

A collection is a group of objects that are referenced by a single variable. In regular life, collections would be things like a group of people, a pride of lions, a parliament of rooks, or a murder of crows. In C#, there are two important types of collections for you to understand:

Image Array: Arrays are the most primitive but fastest collection type. Arrays can only hold data of a single type, and their length must be set when they are defined. It is possible to make both multidimensional arrays and jagged arrays (arrays of arrays), both of which are covered later in this chapter. The word array will only be capitalized when referring to the C# class Array, which is different from primitive arrays of data.

Image List: Lists are more flexible than arrays but are still strongly typed (meaning that they can only hold one type of data). Lists are flexible in length, making them useful when you don’t know exactly how many objects will be in the collection. In this book, List will be capitalized when referring to the C# type to help distinguish it from the common usage of “list.”

Because Lists are more flexible, we’ll start with them and then cover arrays. We’ll also discuss converting from one type to another, and I’ll provide a handy guide as to which collection type is best suited to any given situation.


Set Up a Project for this Chapter

Following the standard project setup procedure, create a new project in Unity. If you need a refresher on the standard project setup procedure, see Appendix A, “Standard Project Setup Procedure.”

Image Project name: Collections Project

Image Scene name: Scene_Collections

Image C# script names: ListEx and ArrayEx

Attach both C# scripts to the Main Camera in Scene_Collections.


List

The first using line at the top of each C# script gives the script knowledge of standard Unity objects (see line 1 in the following code listing). The second line, using System.Collections, gives the script knowledge of ArrayLists (a third collection type without strong typing) and how to use them; however, the List collection type is not actually part of the standard using lines we have included in previous scripts. List and other generic collections are part of the System.Collections.Generic library that is included in line 3 of the following code listing. A generic collection is one that is strongly typed to hold a collection of a single specific data type that is defined using angle brackets. Examples of this include:

Image public List<string> sList;                 –This declares a List of strings

Image public List<GameObject> goList;     –This declares a List of GameObjects

System.Collections.Generic also defines other generic data types, but they beyond the scope of this chapter. These include Dictionary and the generic versions of Queue and Stack. Unlike arrays, which are locked to a specified length, all generic collection types can adjust their length dynamically.

Double-click the ListEx C# script in the Project pane to open it in MonoDevelop and add the following bolded code (you do not need to add the // # comments on the far-right side of the code listing; these are references to explanations listed after the code):

 1 using UnityEngine;                                                   // 1
 2 using System.Collections;                                            // 2
 3 using System.Collections.Generic;                                    // 3
 4 
 5 public class ListEx : MonoBehaviour {
 6     public List<string>        sList;                                 // 4
 7
 8     void Start () {
 9         sList = new List<string>();                                   // 5
10         sList.Add( "Experience" );                                    // 6
11         sList.Add( "is" );
12         sList.Add( "what" );
13         sList.Add( "you" );
14         sList.Add( "get" );
15         sList.Add( "when" );
16         sList.Add( "you" );
17         sList.Add( "didn't" );
18         sList.Add( "get" );
19         sList.Add( "what" );
20         sList.Add( "you" );
21         sList.Add( "wanted." );
22         // This quote is from my professor, Dr. Randy Pausch (1960-2008)
23
24         print( "sList Count = "+sList.Count );                        // 7
25         print( "The 0th element is: "+sList[0] );                     // 8
26         print( "The 1st element is: "+sList[1] );
27         print( "The 3rd element is: "+sList[3] );
28         print( "The 8th element is: "+sList[8] );
29
30         string str = "";
31         foreach (string sTemp in sList) {                             // 9
32             str += sTemp+" ";
33         }
34         print( str );
35     }
36 }

The numbers in the list below refer to lines in the preceding code that are marked with // # to the right of the line.

1. The UnityEngine library enables all of the classes and types that are specific to Unity (e.g., GameObject, Renderer, Mesh). It is mandatory that it be included in any Unity C# script.

2. The System.Collections library that is at the beginning of all C# scripts enables the ArrayList type (among others). ArrayList is another type of C# collection that is similar to List except that ArrayLists are not limited to a single type of data. This enables more flexibility, but I have found it to have more detriments than benefits when compared to Lists (including a significant performance penalty).

3. The List collection type is part of the System.Collections.Generic C# library, so that library must be imported to enable the use of Lists. System.Collections.Generic enables a whole slew of generic collection types beyond just List. You can learn more about them online by searching “C# System.Collections.Generic” on the Internet.

4. This declares the List<string> sList. All generic collection data types have their name followed by angle brackets < > surrounding a specified data type. In this case, the List is a List of strings. However, the strength of generics is that they can be used for any data type. You could just as easily create a List<int>, List<GameObject>, List<Transform>, List<Vector3>, and so on. The type of the List must be assigned at the time of declaration.

5. The declaration of sList on line 6 makes sList a variable name that can hold a List of strings, but the value of sList is null (that is, it has no value) until sList is defined on line 9. Before this definition, any attempt to add elements to sList would have caused an error. The List definition must repeat the type of the List in the new statement. A newly defined List initially contains no elements and has a Count of zero.

6. A List’s Add() function adds an element to the List. This will insert the string literal "Experience" into the 0th (pronounced “zeroth”) element of the List. See the “Lists and Arrays are Zero-Indexed” sidebar for information about zero-indexed Lists.

7. A List’s Count property returns an int representing the number of elements in the List.

8. Lines 25–28 demonstrate the use of bracket access (e.g., sList[0]). Bracket access uses brackets [] and an integer to reference a specific element in a List or array. The integer between the brackets is known as the “index.”

9. foreach (introduced in the previous chapter) is often used with Lists and other collections. Just as a string is a collection of chars, List<string> sList is a collection of strings. The string sTemp variable is scoped to the foreach statement, so it will cease to exist once theforeach loop has completed. Because Lists are strongly typed (that is, C# knows that sList is a List<string>) the elements of sList can be assigned to string sTemp without requiring any kind of conversion. This is one of the major advantages of the List collection over the nontyped ArrayList type.

The console output of the preceding code will be the following lines:

sList Count = 12
The 0th element is: Experience
The 1st element is: is
The 3rd element is: you
The 8th element is: get
Experience is what you get when you didn't get what you wanted.


Lists and Arrays are Zero-Indexed

List and array collection types are zero-indexed, meaning that what you might think of as the “first” element is actually element [0]. Throughout the book, I will refer to this element as the 0th or “zeroth” element.

For these examples, we’ll consider the pseudocode collection coll. “Pseudocode” is code that is not from any specific programming language but is used to illustrate a conceptual point more easily.

coll = [ "A", "B", "C", "D", "E" ]

The count or length of coll is 5, and the valid indices for the elements would be from 0 to coll.Count-1 (that is, 0, 1, 2, 3, and 4).

print( coll.Count );  // 5

print( coll[0] );     // A
print( coll[1] );     // B
print( coll[2] );     // C
print( coll[3] );     // D
print( coll[4] );     // E

print( coll[5] );     // Index Out of Range Error!!!

If you try to use bracket access to access an index that is not in range, you will see the following runtime error:

IndexOutOfRangeException: Array index is out of range.

It is important to keep this in mind as you’re working with any collection in C#.


As always, remember to save your script in MonoDevelop when you’re done editing. Then, switch back to Unity and select the Main Camera in the Hierarchy pane. You will see that List<string> sList appears in the ListEx (Script) component of the Inspector pane. If you play the Unity scene, you can click the disclosure triangle next to sList in the Inspector and actually see the values that populate it. (Another drawback of ArrayLists is that they do not appear in the Inspector collection.)


Important List Properties and Methods

There are many, many properties and methods available for Lists, but these are the most often used. All of these method examples refer to the following List<string> sL and are noncumulative. In other words, each example starts with the List sL as it is defined in the following three lines, unmodified by the other examples.

List<string> sL = new List<string>();
sL.Add( "A" );  sL.Add( "B" );  sL.Add( "C" );  sL.Add( "D" );
// Resulting in the List: [ "A", "B", "C", "D" ]

Properties

Image sL[2] (Bracket access): Returns the element of the array at the index specified by the parameter (2). Because C is the second element of sL, this returns: C.

Image sL.Count: Returns the number of elements currently in the List. Because the length of a List can vary over time, Count is very important. The last valid index in a List is always Count-1. The value of sL.Count is 4, so the last valid index is 3.

Methods

Image sL.Add("Hello"): Adds the parameter "Hello" to the end of sL. In this case, sL becomes: [ "A", "B", "C", "D", "Hello" ].

Image sL.Clear(): Removes all existing elements from sL returning it to an empty state. sL becomes empty: [ ].

Image sL.IndexOf("A"): Finds the first instance in sL of the parameter "A" and returns the index of that element. Because "A" is the 0th element of sL, this call returns 0. If the variable does not exist in the List, a -1 is returned. This is a safe and fast method to determine whether a List contains an element.

Image sL.Insert(2, "B.5"): Inserts the second parameter ("B.5") into sL at the index specified by the first parameter (2). This shifts the subsequent elements of the List forward. In this case, this would cause sL to become [ "A", "B", "B.5", "C", "D" ]. Valid index values for the first parameter are 0 through sL.Count. Any value outside this range will cause a runtime error.

Image sL.Remove("C"): Removes the specified element from the List. If there happened to be two "C"s in the List, only the first would be removed. sL becomes [ "A", "B", "D" ].

Image sL.RemoveAt(0): Removes the element at the specified index from the List. Because "A" is the 0th element of the List, sL becomes[ "B", "C", "D" ].

Converting a List to an array

Image sL.ToArray(): Generates an array that has all the elements of sL. The new array will be of the same type as the List. Returns a new string array with the elements [ "A", "B", "C", "D" ].


To move on to learning about arrays, make sure that Unity playback is stopped and then uncheck the check box next to the name of the ListEx (Script) component in the Inspector pane to make the ListEx script inactive (as is shown in Figure 22.1).

Image

Figure 22.1 Clicking the check box to deactivate the ListEx Script component

Array

An array is the simplest collection type, which also makes it the fastest. Arrays do not require any libraries to be imported (via the using command) to work because they are built into core C#. In addition, arrays have multidimensional and jagged forms that can be very useful.

Basic Array Creation

Arrays are of a fixed length that must be determined when the array is defined. Double-click the ArrayEx C# script in the Project pane to open it in MonoDevelop and enter the following code:

 1 using UnityEngine;
 2 using System.Collections;
 3
 4 public class ArrayEx : MonoBehaviour {
 5     public string[]        sArray;                                       // 1
 6
 7     void Start () {
 8         sArray = new string[10];                                         // 2
 9
10         sArray[0] = "These";                                             // 3
11         sArray[1] = "are";
12         sArray[2] = "some";
13         sArray[3] = "words";
14
15         print( "The length of sArray is: "+sArray.Length );              // 4
16
17         string str = "";
18         foreach (string sTemp in sArray) {                               // 5
19             str += "|"+sTemp;
20         }
21         print( str );
22     }
23
24 }

1. Unlike a list, an array in C# isn’t actually a separate data type; rather, it’s a collection formed from any existing data type by adding brackets after the type name. The type of sArray in the preceding code is not declared as string; it is string[], a collection of multiplestrings. Note that while sArray is declared as an array here, its length is not yet defined.

2. Here, sArray is defined as a string[] with a length of 10. When an array is defined, its length is filled with elements of the default value for that data type. For int[] or float[], the default would be 0. For string[] and other complex data types like GameObject[], each element of the array is filled with null (which indicates no that value has been assigned).

3. Rather than using the Add() method like Lists, standard arrays use bracket access for assignment of value as well as retrieval of value from the array.

4. Rather than using Count like other C# collections, arrays use the property Length. It is important to note (as you can see from the preceding code output) that Length returns the entire length of the array, including both defined elements (for example, sArray[0] throughsArray[3] in the preceding code) and elements that are empty (that is, still their default, undefined value as are sArray[4] through sArray[9] in the previous code).

5. foreach works for arrays just as it does for other C# collections. The only difference is that the array may have empty or null elements, and foreach will iterate over them.

When you run the code, be sure to have Main Camera selected in the Hierarchy pane. This will enable you to open the disclosure triangle next to sArray in the ArrayEx (Script) component of the Inspector pane and see the elements of sArray.

The code output looks like this:

The length of sArray is: 10
|These|are|some|words||||||

Empty Elements in the Middle of an Array

One thing allowed by arrays that is not possible in Lists is an empty element in the middle of the collection. This would be useful in a game if you had something like a scoring track where each player had a marker on the track but it was possible to have empty spaces in between the markers.

Modify the previous code as follows:

10      sArray[0] = "These";
11      sArray[1] = "are";
12      sArray[3] = "some";
13      sArray[6] = "words";

The code output would look like this: |These|are||some|||words|||

As you can see from the output, sArray now has empty elements at indices 2, 4, 5, 7, 8, and 9. As long as the index (for example, 0, 1, 3, and 6 here) of the assignment is within the valid range for the array, you can use bracket access to place a value anywhere in the array, and the foreachloop will handle it gracefully.

Attempting to assign a value to an index that is outside of the defined range for the array (for example, sArray[10] = "oops!"; or sArray[99] = "error!";) will lead to this runtime error:

IndexOutOfRangeException: Array index is out of range.

Return the code back to its original state:

10      sArray[0] = "These";
11      sArray[1] = "are";
12      sArray[2] = "some";
13      sArray[3] = "words";

Empty Array Elements and foreach

Play the project again and look at the output, which has returned to its previous state:

|These|are|some|words||||||

The str += "|"+sTemp; statement concatenates (that is, adds) a pipe (|) onto str before each element of the array. Even though sArray[4] through sArray[9] are still the default value of null, they are counted by foreach and iterated over. This is a good opportunity to use a break jump statement. Modify the code as follows:

18      foreach (string sTemp in sArray) {
19          str += "|"+sTemp;
20          if (sTemp == null) break;
21      }

The new code output is as follows: |These|are|some|words|

When C# iterates over sArray[4], it will still concatenate "|"+null onto the end of str but will then check sArray[4], see that it is null, and break out of the foreach loop before iterating over sArray[5] through sArray[9]. As an exercise, think about how you could use the jump statement continue to skip empty elements in the middle of the array but not completely break out of the foreach loop.


Important Array Properties and Methods

There are also many properties and methods available for arrays. These are the most often used. All of these refer to the following array and are noncumulative.

string[] sA = new string[] { "A", "B", "C", "D" };
// Resulting in the Array: [ "A", "B", "C", "D" ]

Here you see the array initialization expression that allows the declaration, definition, and population of an array in a single line. Note that when using the array initialization expression, the Length of the array is implied by the number of elements between the braces and does not need to be specified; in fact, if you use braces to define an array, you cannot use the brackets in the array declaration to specify a length that is different from the number of elements between the braces.

Properties

Image sA[2] (bracket access): Returns the element of the array at the index specified by the parameter (2). Because "C" is the second element of sA, this returns: "C".

If the index parameter is outside of the valid range of the array (which for sA is 0 to 3), it will generate a runtime error.

Image sA[1] = "Bravo" (bracket access used for assignment): Assigns the value on the right side of the = assignment operator to the specified position in the array, replacing the previous value. sA would become [ "A", "Bravo", "C", "D" ].

If the index parameter is outside of the valid range of the array, it will generate a runtime error.

Image sA.Length: Returns the total capacity of the array. Elements will be counted regardless of whether they have been assigned or are still default values. Returns 4.

Static Methods

The static methods here are of the System.Array class and can act on arrays to give them some of the abilities of Lists.

Image System.Array.IndexOf( sA, "C" ): Finds the first instance in sA of the element "C" and returns the index of that element. Because "C" is the second element of sA, this returns 2.

If the variable does not exist in the Array, a -1 is returned. This is often used to determine whether an array contains a specific element.

Image System.Array.Resize( ref sA, 6 ): This is a C# method that adjusts the length of an array. The first parameter is a reference to the array instance (which is why the ref keyword is required), and the second parameter is the new length. sA would become [ "A", "B", "C", "D", null, null ].

If the second parameter specifies a Length that is shorter than the original array, the extra elements will be culled. System.Array.Resize( ref sA, 2 ) would cause sA to become [ "A", "B" ]. System.Array.Resize() does not work for multidimensional arrays.

Converting an array to a List

Image List<string> sL = new List<string>( sA ): This line will create a List sL that duplicates all the elements of sA.

It is also possible to use the array initialization expression to declare, define, and populate a List in one line, but it’s a little convoluted:

List<string> sL = new List<string>( new string[] { “A”, “B”, “C” } );

This declares, defines, and populates an anonymous new string[] array that is immediately passed into the new List<string>() function.


To prepare for the next example, deactivate the ArrayEx script by clicking the check box next to its name in the Inspector pane for Main Camera.

Multidimensional Arrays

It is possible—and often useful—to create multidimensional arrays that have two or more indices. This means that instead of just one index number in the brackets, the array could use two or more. This would be useful for creating a two-dimensional grid that could hold one item in each grid square.

Create a new C# script named Array2dEx and attach it to Main Camera. Open Array2dEx in MonoDevelop and enter the following code:

 1 using UnityEngine;
 2 using System.Collections;
 3
 4 public class Array2dEx : MonoBehaviour {
 5
 6     public string[,]        sArray2d;
 7
 8     void Start () {
 9         sArray2d = new string[4,4];
10
11         sArray2d[0,0] = "A";
12         sArray2d[0,3] = "B";
13         sArray2d[1,2] = "C";
14         sArray2d[3,1] = "D";
15
16         print( "The Length of sArray2d is: "+sArray2d.Length );
17     }
18 }

The code will yield the following output: The Length of sArray2d is: 16

As you can see, Length is still only a single int, even though this is a multidimensional array. Length here is now just the total number of elements in the array, so it is the coder’s responsibility to keep track of each separate dimension of the array.

Now, let’s create a nicely formatted output of the values in sArray2d. When we’re done, it should look something like this:

|A| | |B|
| | |C| |
| | | | |
| |D| | |

As you can see, the A is in the 0th row, 0th column ( [0,0] ), the B is in the 0th row, 3rd column ( [0,3] ) and so on. To implement this, add the following bolded lines to the code:

16         print( "The Length of sArray2d is: "+sArray2d.Length );
17
18         string str = "";
19         for ( int i=0; i<4; i++ ) {                              // 1
20             for ( int j=0; j<4; j++ ) {
21                 if (sArray2d[i,j] != null) {                     // 2
22                     str += "|"+sArray2d[i,j];
23                 } else {
24                     str += "|_";
25                 }
26             }
27             str += "|"+"\n";                                     // 3
28         }
29         print( str );
30     }
31 }

1. Lines 19 and 20 demonstrate the use of two nested for loops to iterate over a multidimensional array. When nested in this manner, the code will:

1. Start with i=0

2. Iterate over all j values from 0 to 3

3. Increment to i=1

4. Iterate over all j values from 0 to 3

5. Increment to i=2

6. Iterate over all j values from 0 to 3

7. Increment to i=3

8. Iterate over all j values from 0 to 3

This will guarantee that the code moves through the multidimensional array in an orderly manner. Keeping the grid example, it will move through all the elements in a row (by incrementing j from 0 to 3) and then advance to the next row by incrementing i to the next value.

2. Lines 21–25 check to see whether the string at sArray[i,j] has a value other than null. If so, it concatenates a pipe and sArray2d[i,j] onto str. If the value is null, a pipe and one space are concatenated onto str. The pipe character is found on the keyboard above the Return (or Enter) key. It is usually shift–backslash ( \ ).

3. This line occurs after all of the iterations of the j for loop but before the next iteration of the i for loop. The effect of it is to concatenate a trailing pipe and carriage return (i.e., line break) onto str, giving the output the nice formatting of a line for each iteration of the i for loop. The backslash-n (\n) is a new line character.

The code produces the following output, though you will only see the first couple of lines in the Unity Console pane.

The Length of sArray2d is: 16

|A| | |B|
| | |C| |
| | | | |
| |D| | |

Just looking at the output in the Console pane of Unity, you will only see the top two lines of the sArray2d grid array listed in the output. However, if you click that line in the Console pane, you will see that more data is revealed in the bottom half of the Console pane (see Figure 22.2).

Image

Figure 22.2 Clicking an output message in the Console causes an expanded view to appear below. Note that the first line of the most recent Console message is also shown in the lower-left corner of the Unity window.

As you can see in the figure, the fancy text formatting that we did doesn’t line up as well in the Console pane because it uses a non-monospaced font (that is, a font where an i has a different width than an m; in monospaced fonts, i and m have the same width). You can click any line in the Console pane and choose Edit > Copy from the menu bar to copy that data and then paste it into another program. This is something that I do often, and I most commonly paste into a text editor. (I prefer TextWrangler1 on the Mac or EditPad Pro2 on the PC, both of which are quite powerful.)

1 TextWrangler is available for free from BareBones Software: http://www.barebones.com.

2 EditPad Pro has a free trial available from Just Great Software: http://editpadpro.com.

You should also be aware that the Unity Inspector pane does not display multidimensional arrays. In fact, if the Inspector does not know how to properly display a variable, it will completely ignore it, so not even the name of a public multidimensional array will appear in the Inspector pane.

Stop Unity’s execution by clicking the Play button again (so that it is not blue) and then use the Main Camera Inspector to disable the Array2dEx (Script) component.

Jagged Arrays

A jagged array is an array of arrays. This is similar to the multidimensional array, but it allows the subarrays to be different lengths. We’ll create a jagged array that holds the following data:

| A | B | C | D |
| E | F | G |
| H | I |
| J |   |   | K |

As you can see, the 0th and 3rd rows each contain four elements, while rows 1 and 2 contain three and two elements respectively. Note that null elements are allowed as is shown in the 3rd row. In fact, it is also possible for an entire row to be null (though that would cause an error on line 32 in the following code listing because that code is not designed to handle null rows).

Create a new C# script named JaggedArrayEx and attach it to Main Camera. Open JaggedArrayEx in MonoDevelop and enter the following code:

 1 using UnityEngine;
 2 using System.Collections;
 3
 4 public class JaggedArrayEx : MonoBehaviour {
 5     public string[][]        jArray;                             // 1
 6
 7     void Start () {
 8         jArray = new string[4][];                                // 2
 9
10         jArray[0] = new string[4];                               // 3
11         jArray[0][0] = "A";
12         jArray[0][1] = "B";
13         jArray[0][2] = "C";
14         jArray[0][3] = "D";
15
16         // The following lines use single-line Array initialization      // 4
17         jArray[1] = new string[] { "E", "F", "G" };
18         jArray[2] = new string[] { "H", "I" };
19
20         jArray[3] = new string[4];                                       // 5
21         jArray[3][0] = "J";
22         jArray[3][3] = "K";
23
24         print( "The Length of jArray is: "+jArray.Length );              // 6
25         // Outputs: The Length of jArray is: 4
26
27         print( "The Length of jArray[1] is: "+jArray[1].Length );        // 7
28         // Outputs: The Length of jArray[1] is: 3
29
30         string str = "";
31         foreach (string[] sArray in jArray) {                            // 8
32             foreach( string sTemp in sArray ) {
33                 if (sTemp != null) {
34                     str += " | "+sTemp;                                  // 9
35                 } else {
36                     str += " |  ";                                       //10
37                 }
38             }
39             str += " | \n";
40         }
41
42         print( str );
43     }
44 }

1. Line 5 declares jArray as a jagged array (that is, an array of arrays). Where a string[] is a collection of strings, a string[][] is a collection of string arrays (or string[]s).

2. Line 8 defines jArray as a jagged array with a length of 4. Note that the second set of brackets is still empty, denoting that the subarrays can be of any length.

3. Line 10 defines the 0th element of jArray to be an array of strings with a length of 4.

4. Lines 17 and 18 use the single-line form of array definition. Because the elements of the array are defined between the braces, the length of the array does not need to be explicitly stated (hence the empty brackets in new string[]).

5. Lines 20–22 define the 3rd element of jArray to be a string[] with a length of 4 and then fill only the 0th and 3rd elements of that string[], leaving elements 1 and 2 null.

6. Line 24 outputs "The Length of jArray is: 4". Because jArray is an array of arrays (rather than a multidimensional array), jArray.Length counts only the number of elements that can be accessed via the first set of brackets.

7. Line 27 outputs "The Length of jArray[1] is: 3". Because jArray is an array of arrays, subarray Length can also now be easily determined.

8. In jagged arrays, foreach works separately on the array and sub-arrays. foreach on jArray will iterate through the four string[] (string array) elements of jArray, and foreach on any of those string[]s will iterate over the strings within. Note that sArray is astring[] (string array) and that sTemp is a string.

As was mentioned previously, line 32 would throw a null reference error if one of the rows of jArray were null. In that case, sArray would be null, and trying to run the foreach statement in line 32 on a null variable would lead to a null reference, the attempt to reference an element of something that is null. The foreach statement would be attempting to access data of sArray like sArray.Length and sArray[0]. Because null data have no elements or value, accessing things like null.Length throws an error.

9. On a keyboard, the string literal in line 34 is typed: space pipe space.

10. On a keyboard, the string literal in line 36 is typed: space pipe space space.

The code outputs the following to the Console pane:

The Length of jArray is: 4
The Length of jArray[1] is: 3
 | A | B | C | D |
 | E | F | G |
 | H | I |
 | J |   |   | K |

Using for Loops Instead of foreach for Jagged Arrays

It is also possible to use for loops based on the Length of the array and subarrays. The preceding foreach loop could be replaced with this code:

31         string str = "";
32         for (int i=0; i<jArray.Length; i++) {
33             for (int j=0; j<jArray[i].Length; j++) {
34                 str += " | "+jArray[i][j];
35             }
36             str += " | \n";
37         }

This code produces the exact same output as the foreach loops shown earlier. Whether you choose to use for or foreach will depend on the situation.

Jagged Lists

As a final note on jagged collections, it is also possible to create jagged Lists. A jagged two-dimensional list of strings would be declared List<List<string>> jaggedStringList. Just as with jagged arrays, the subLists would initially be null, so you would have to initialize them as you added them, as is shown in the following code. Just like all Lists, jagged Lists do not allow empty elements. Create a new C# script named JaggedListTest, attach it to Main Camera, and enter this code:

 1 using UnityEngine;
 2 using System.Collections.Generic;                                        // 1
 3
 4 public class JaggedListTest : MonoBehaviour {
 5
 6     public List<List<string>> jaggedList;
 7
 8     // Use this for initialization
 9     void Start () {
10         jaggedList = new List<List<string>>();
11
12         // Add a couple List<string>s to jaggedList
13         jaggedList.Add( new List<string>() );
14         jaggedList.Add( new List<string>() );
15
16         // Add two strings to jaggedList[0]
17         jaggedList[0].Add ("Hello");
18         jaggedList[0].Add ("World");
19
20         // Add a third List<string> to jaggedList, including data
21         jaggedList.Add ( new List<string>( new string[] {"complex", "initialization"} ) );                                   // 2
22
23         string str = "";
24         foreach (List<string> sL in jaggedList) {
25             foreach (string sTemp in sL) {
26                 if (sTemp != null) {
27                     str += " | "+sTemp;
28                 } else {
29                     str += " |  ";
30                 }
31             }
32             str += " | \n";
33         }
34         print( str );
35     }
36
37 }

1. Though using System.Collections; is included in all Unity C# scripts by default, it’s not actually necessary (though System.Collections.Generic is required for Lists).

2. This is one of the first instances in this book of the Image code continuation character. This is used throughout the book when a single line is longer than can fit the width of the page. You should not try to type the Image character, rather it is there to let you know to continue typing the single line as if there were no line break. With no leading tabs, line 21 would appear as follows:

jaggedList.Add( new List<string>( new string[] {"complex","initialization"} ) );

The code outputs the following to the Console pane:

| Hello | World |
|
| complex | initialization |

Whether to Use Array or List

The primary differences between the array and List collections types are as follows:

Image List has flexible length, whereas array length is more difficult to change.

Image Array is slightly faster, though it’s too little to notice most of the time.

Image Array allows multidimensional indices.

Image Array allows empty elements in the middle of the collection.

Because they are simpler to implement and take less forethought (due to their flexible length), I personally tend to use Lists much more often than arrays. This is especially true when prototyping games, since prototyping requires a lot of flexibility.

Summary

Now that you have a handle on Lists and arrays, it will be possible for you to work easily with large numbers of objects in your games. For example, you could return to the Hello World project from Chapter 18, “Hello World: Your First Program,” and add a List<GameObject> to the CubeSpawner code that had every new cube added to it at the time the cube was instantiated. This would give you a reference to each cube, allowing you to manipulate the cube after it was created.

Summary Exercise

In this exercise, we return to the Hello World project from Chapter 18 and write a script that will add each new cube created to a List<GameObject> named gameObjectList. Every frame that a cube exists, it will be scaled down to 95% of the size it was in the previous frame. Once a cube has shrunk to a scale of 0.1 or less, it will be deleted from the scene and gameObjectList.

However, if we delete an element from gameObjectList while the foreach loop is iterating over it, this will cause an error. To avoid this, the cubes that need to be deleted will be temporarily stored in another List named removeList, and then the List will be iterated over to remove them from gameObjectList. (You’ll see what I mean in the code.)

Open your Hello World project and create a new scene (File > Scene from the menu bar). Save the scene as _Scene_3. Create a new script named CubeSpawner3 and attach it to the Main Camera in the scene. Then, open CubeSpawner3 in MonoDevelop and enter the following code:

 1 using UnityEngine;
 2 using System.Collections;
 3 using System.Collections.Generic;
 4
 5 public class CubeSpawner3 : MonoBehaviour {
 6     public GameObject        cubePrefabVar;
 7     public List<GameObject>  gameObjectList; // Will hold all the  Cubes
 8     public float             scalingFactor = 0.95f;
 9     // ^ Amount that each cube will shrink each frame
10     public int                 numCubes = 0; // Total # of Cubes instantiated
11
12     // Use this for initialization
13     void Start() {
14         // This initializes the List<GameObject>
15         gameObjectList = new List<GameObject>();
16     }
17
18     // Update is called once per frame
19     void Update () {
20         numCubes++; // Add to the number of Cubes                        // 1
21
22         GameObject gObj = Instantiate( cubePrefabVar ) as GameObject;    // 2
23
24         // These lines set some values on the new Cube
25         gObj.name = "Cube "+numCubes;                                    // 3
26         Color c = new Color(Random.value, Random.value, Random.value);   // 4
27         gObj.renderer.material.color = c;
28         // ^ Gives the Cube a random color
29         gObj.transform.position = Random.insideUnitSphere;               // 5
30
31         gameObjectList.Add (gObj); // Add gObj to the List of Cubes
32
33         List<GameObject> removeList = new List<GameObject>();            // 6
34         // ^ This removeList will store information on Cubes that should be
35         //    removed from gameObjectList
36
37         // Iterate through each Cube in gameObjectList
38         foreach (GameObject goTemp in gameObjectList) {                  // 7
39
40             // Get the scale of the Cube
41             float scale = goTemp.transform.localScale.x;                 // 8
42             scale *= scalingFactor; // Shrink it by the scalingFactor
43             goTemp.transform.localScale = Vector3.one * scale;
44
45             if (scale <= 0.1f) { // If the scale is less than 0.1f...    // 9
46                 removeList.Add (goTemp); // ...then add it to the removeList
47             }
48         }
49
50         foreach (GameObject goTemp in removeList) {                      // 7
51             gameObjectList.Remove (goTemp);                              //10
52             // ^ Remove the Cube from gameObjectList
53             Destroy (goTemp);  // Destroy the Cube's GameObject
54         }
55
56     }
57 }

1. The increment operator (++) is used to increase the numCubes count of the total number of cubes that have been created.

2. An instance of cubePrefabVar is instantiated. The words "as GameObject" are necessary because Instantiate() can be used on any kind of object (meaning that C# has no way of knowing what kind of data Instantiate() will return). The "as GameObject"tells C# that this object should be treated as a GameObject.

3. The numCubes variable is used to give unique names to each cube. The first cube will be named Cube 1, the second Cube 2, and so on.

4. Lines 26 and 27 assign a random color to each cube. Colors are accessed through the material attached to the GameObject’s Renderer, as is demonstrated on line 27.

5. Random.insideUnitSphere returns a random location that is inside a sphere with a radius of 1 (centered on the point [0,0,0]). This code makes the cubes spawn at a random location near [0,0,0] rather than all at exactly the same point.

6. As is stated in the code comments, removeList will be used to store cubes that need to be removed from gameObjectList. This is necessary because C# does not allow you to remove elements from a List in the middle of a foreach loop that is iterating over the List. (That is, it is not possible to call gameObjectList.Remove() anywhere within the foreach loop on lines 38–48 that iterates over gameObjectList.)

7. This foreach loop iterates over all of the cubes in gameObjectList. Note that the temporary variable created for the foreach is goTemp. goTemp is also used in the foreach loop on line 50, so goTemp is declared on both lines 38 and 50. Because goTemp is locally scoped to the foreach loop in each case, there is no conflict caused by declaring the variable twice in the same Update() function. See“Variable Scope” in Appendix B, “Useful Concepts,” for more information.

8. Lines 41–43 get the current scale of a cube (by getting the x dimension of its transform.localScale), multiply that scale by 95%, and then set the transform.localScale to this new value. Multiplying a Vector3 by a float (as is done on line 43) multiplies each individual dimension by that same number, so [2,4,6] * 0.5f would yield [1,2,3].

9. As mentioned in the code comments, if the newly reduced scale is less than 0.1f, the cube will be added to removeList.

10. The foreach loop from lines 50–54 iterates over removeList and removes any cube that is in removeList from gameObjectList. Because the foreach is iterating over removeList, it’s perfectly fine to remove elements from gameObjectList. The removed cube GameObject still exists on screen until the Destroy() command is used to destroy it. Even then, it still exists in the computer’s memory because it is still an element of removeList. However, because removeList is a local variable scoped to the Update() function, once theUpdate() function is complete, removeList will cease to exist, and then any objects that are exclusively stored in removeList will also be deleted from memory.

Save your script and then switch back to Unity. You must assign Cube Prefab from the Project pane to the cubePrefabVar variable in the Main Camera:CubeSpawner3 (Script) component of the Main Camera Inspector if you want to actually instantiate any cubes.

After you have done this, press Play in Unity, and you should see that a number of cubes spawn in as they did in previous versions of Hello World. However, they spawn in different colors, they shrink over time, and they are eventually destroyed (instead of existing indefinitely as they did in earlier versions).

Because the CubeSpawner3 code keeps track of each cube through the gameObjectList, it is able to modify each cube’s scale every frame and then destroy each cube once it’s smaller than a scale of 0.1f. At a scalingFactor of 0.95f, it takes each cube 45 frames to shrink to a scale <= 0.1f, so what would be the 0th cube in gameObjectList is always removed and destroyed for being too small, and the Count of gameObjectList stays at 45.

Moving Forward

In the next chapter, you learn how to create and name functions other than Start() and Update().