06 - Multiple Objects
Multiple Object Workflow
If you followed along with some examples, you probably realized we do a lot of manual labor. We deal with the Unity Editor a lot. We apply all the new scripts manually to each cuboid. Then again, programming is a lazy man’s art. The less manual labor we have to do, the better.
With the concepts introduced in this chapter, we will handle lots and lots of objects simultaneously. Hence it will also allow us to create more complex sketches.
To do this conveniently in Unity, we need to look at three Concepts:
-
Prefabs and Instantiation
-
Arrays and/or Lists
-
Loops
In short, we will create objects through Instantiation. We will the use Arrays and Lists to keep track of them and loops to iterate over them to adjust their behavior.
We will also look at interaction between multiple scripts. Finally, keywords like private
and public
will hopefully make more sense.
Prefabs and Instantiation
Instantiation is the process of creating or “instantiating” objects from a script, and it is tightly intertwined with the concept of “prefabs”. Prefabs in Unity are “prefabricated” Objects we can create through the project view. In most cases these prefabs are the object we want to instantiate through code at runtime.
Creating prefabs is simple. All you have to do is drag the item from the hierarchy into the project view. Create a cuboid prefab from any of your sketches.
The tiny cube in front of the object turns blue if Unity knows about this object as a Prefab. It’s often advisable to create a specific folder for all your prefabs.
Unity will keep all the components attached to the prefab. Any behaviour we have added to a prefab will be there once we create an instance. For example, a cuboid with a script that makes it move attached to it, will lead to this same movement the moment you Instantiate it at runtime. Choose one of your moving cuboids and turn it into a prefab.
Once you have created a prefab from a GameObject, you can delete it from the hierarchy, if you don’t need it there anymore.
Instantiation
With a prefab ready, we will need to create a script to handle the creation/instantiation of all our cuboids. Because scripts in Unity can’t exist on their own, we need to create a GameObject to hold our script. Technically, any object would work, yet an “Empty Object” would be the ideal choice. Empties only contain a transform component and thus have no visual representation in the scene. Rename your Empty to something like “ScriptHolder” or “Manager”.
First, we have to get a reference to our prefabricated cuboid. In Unity, all Objects (that inherit from Monobehaviour - we’ll discuss this in Chapter 07) can be referenced as GameObject. So let’s create a variable of type GameObject
for our cuboid.
[SerializeField]
private GameObject cuboidPrefab;
Now go back to Unity and check the Inspector. You will see a field named “cuboidPrefab”. You can now drag and drop the prefab you created from the Project view into the field on the inspector.
Now the script has a reference to the prefab. This process of connecting your objects to scripts feels strange in the beginning. When writing code I tend to forget this step and run into an error. Thus when you run into a “null reference” error, check if you added your prefab to the script.
Next we need to create an actual instance at runtime:
Instantiate(cuboidPrefab, Vector3.zero, Quaternion.identity);
The Instantiate()
Method takes three arguments:
-
The object we want to instantiate
-
A
Vector3
as position in space -
Rotation passed as a quaternion.
For now, just use Quaternion.identity;
. Quaternions are scary things and Quaternion.identity
will place them in the exact state you created the prefab in. Thus this somewhat the default option.
If you run your scene now, you have one cuboid moving around, based on the script you had attached to it, when creating the prefab.
Duplicate that line and run the code again. It should give you two cuboids, and yet you only see one. It creates both cuboids in the exact same space. But you should see two instances of them in the hierarchy. They have “(Clone)” appended to their name. To be able to see both we need to position them apart. We could either change the position at which we instantiate the objects, or we could re-position them after instantiation.
To move them afterwards, we need to be able to access a GameObject after instantiation. To do that, we can just assign the created Object to a variable the moment we create it.
var cuboid = Instantiate(cuboidPrefab, Vector3.back, Quaternion.identity);
The Instantiate()
method returns the object we created. If we only use Instantiate()
method without assigning it to a variable, we just choose to ignore the returned value.
the ‘var’ Keyword
You might stumble across the ‘var’ Keyword here. You will see code that never uses ‘var’ and code which makes a lot uses of it. You should only use it when the context makes it unmistakable which type is referenced. This is mostly true for “Object stuff”. To keep it simple for now. As an Example:
GameObject myGameObject = new GameObject();
This would create a new empty GameObject. But the line of code isn’t a beauty, is it? That’s where
var
comes in:var myGameObject = new GameObject();
Don’t worry about it too much for now. This will become second nature with time.
To be more explicit about types I will not use the
var
keyword throughout this book.
Now we have a variable for our cuboid. This variable allows us to access the cuboid and all the properties of it, which are public.
cuboid.transform.position = new Vector3(0,0,0);
This is the first time private
and public
become relevant for us in a scripting sense. Try accessing some variables we created in the script attached to the cuboid. You probably can not access them, as we made almost all of them private
. The transform component though is public by default.
Now to recreate our Homage to the cuboid, we would need to duplicate the Instantiation five times and then adjust things and… nah… We will not do that! We want to simplify things!
Let’s head straight into loops!
For Loops
To handle many Objects (or any kind of repeating process) programming languages offer “loops”. Loops are blocks of code that are executed repeatedly until they meet a certain condition. Maybe the most common implementation of a loop is the “for loop”.
For loops in C# need three elements to work: initializer, condition and iterator. To think of an example, assume having a jar of cookies and you are allowed to eat 5 cookies a day. You need to check how many you have eaten so far. Thus you take a piece of paper and start a tally sheet. This tally sheet is your initializer. The initializer is just creating a variable to keep track of a number.
Then each time you want a cookie you check whether you are still allowed to eat a cookie or if you already had 5. That’s checking against the condition. Once you have eaten a cookie, you add one to your tally sheet. That’s done using the iterator.
Eating the cookie is the code in the loop. That’s wrapped in curly braces again.
for (int i = 0; i < 5; i++)
{
Debug.Log(i);
}
A for loop starts with the for
Keyword, followed by parentheses. The first thing is our tally list / initializer. It works exactly like creating any other variable in Unity, but you have to initialize it with a value. Counting in computer science almost always starts with 0. Thus we will start with int i = 0
as well. A semicolon separates the three elements of a for loop. Next up is the condition. Here we can define anything that evaluates to a boolean value. Just like if statements. Finally, the iterator determines by which value to increase or decrease your tally list. Here the i++
means, the variable i
will be increased by one after each iteration. You could also put i+1
. You can advance your initializer in any step size: i + 2
, i + 100
or even i - 1
. They all work fine.
In curly braces follows the code to run. In the above counter we just print the value of our counter i
to the console.
Incrementors and Decrementors
In for loops we use
i++
to increase a value by one. This is just a shorthand built into the language. You can also usei--
. You can even use++i
and--i
. Pre-cremented values are changed first and then read.
Arrays
Earlier, when we were discussing instantiation, we said we need a variable to create a reference to our objects. But creating five variables, each to hold a cuboid, would be a pain. Imagine having 100 cuboids! That’s why we also introduce “Arrays”. Arrays hold many things of the same type. A list in essence, except that they aren’t really list because list exist as well and… well… this is confusing. In C# Lists and Arrays are separate – yet closely related – things and we’ll look at Lists a little later.
Anyway… Arrays hold things of the same type. Like float
or int
. Or GameObject
! Creating Arrays is simple. You create them like any variable, but add []
after the type.
private GameObject[] myGameObjectArray;
Now we have declared the Array, but we didn’t initialize it and thus can’t use it. It’s essentially empty. The thing with Arrays is: they always have a fixed length. The moment you initialize the array, you need to specify how many items you want to store in your list. For our cuboid example, it’s as simple as this:
private GameObject[] cuboids = new GameObject[5];
We have to specify the length of the Array, again in brackets, after the type. But what if you wanted to do this based on a variable? You can create the variable for this on global scope and then initialize the array in Start()
or Update()
.
private GameObject[] cuboids
[SerializeField]private int arraySize = 5;
void Start()
{
cuboids = GameObject[arraySize];
}
This construct gives you some flexibility, but you can never change the length of the array once it is initialized.
To access things inside an array, we use bracket notation again.
Debug.Log(cuboids[0]);
This would print the first object in the Array to the console. As we said earlier, counting always starts with 0
! Thus myGameObject[1]
would give you the second entry and so forth.
Now let’s look at making use of this:
public GameObject cuboidPrefab;
[SerializeField] private int arraySize = 5;
private GameObject[] cuboids;
void Start()
{
cuboids = new GameObject[arraySize];
for(int i = 0; i < arraySize; i++)
{
cuboids[i] = Instantiate(cuboidPrefab, Vector3.up * i, Quaternion.identity);
}
}
This code incorporates everything we have covered so far. We first create a variable cuboidPrefab
. We then create a variable for an array of GameObjects and also create a variable for the size of the array. In the Start()
method we then initialize the array to the size of the variable. Henceforth it can hold five objects. But at the moment all slots are empty.
Next we need to create a for loop and its condition is that our initializer is smaller than our arraySize
variable. During the loop we access each slot of the list by its number, using the iterator. We also use the iterator as a multiplier for the position, thus shifting each object up by one.
As you can see, the cuboids now move happily along in sync. But they all still have same color and all of them have the same offset.
If your cuboids don’t move, you probably used a cuboid with no behavior attached as the prefab.
Script Communication
So what if we want to change the color of these? We need to access the Color component of the objects. And we have done this before in Chapter 05. But this time we access the GameObject in our array first.
//...
for(int i = 0; i < cuboids.Length; i++)
{
cuboids[i] = Instantiate(cuboidPrefab, Vector3.up * (stepSize * i), Quaternion.identity);
cuboids[i].GetComponent<Renderer>().material.color = colors[i];
}
As you can see, we can access components on other objects, if we stored a reference to the objects beforehand. Here we instantiate the object, and store it directly in a slot in our array. We can then directly use this stored reference on the next line to grab the Renderer on that object and set its color.
But how about the time offset?
This works exactly the same. Every script we create and attach to an object is a component like every other component Unity provides. We can get hold of these in the same way we did with the Renderer
. We use the GetComponent<>()
method. This is what it looks like for me:
float timeOffset = 0.25f;
for(int i = 0; i < cuboids.Length; i++)
{
cuboids[i] = Instantiate(cuboidPrefab, Vector3.up * (stepSize * i), Quaternion.identity);
cuboids[i].GetComponent<Renderer>().material.color = colors[i];
cuboids[i].GetComponent<CuboidShiftingX>().timeOffset = timeOffset * i;
}
The class name of the script I have attached to the cuboid is called “CuboidShiftingX”. In this component/script, I access the timeOffset variable and set it.
Now you should run into an error here. We have all our variables set toprivate
. This will prevent you from accessing and setting these values from outside. To make this possible, go into the script you have attached to the prefab and change the value you want to change topublic
. Now Unity will grant you access to this variable.
As you can see, I multiply this with another timeOffset
I created in the first line. This will just shift it a little for each cube and not offset it by a whole second. You can have the same variable name on different scripts. They are completely different things!
If you feel that this syntax is complicated, take the time and rebuild some sketches we built using arrays. Also, experiment with the number of cuboids you create this way. Make sure to feel comfortable with arrays and the usage of i
in this context. Arrays are important and we will leverage their power a lot from now on.
A single script
All the examples above use the idea of having the behavior attached to the prefab itself. To make this even more flexible, we could add our behavior at runtime.
using UnityEngine;
public class ComponentCuboids : MonoBehaviour
{
[SerializeField]
private GameObject cuboidPrefab;
private GameObject[] cuboids;
[SerializeField]
private int arraySize = 5;
void Start()
{
cuboids = new GameObject[arraySize];
for(int i = 0; i < arraySize; i++)
{
cuboids[i] = Instantiate(cuboidPrefab, Vector3.up * i, Quaternion.identity);
cuboids[i].AddComponent<CuboidMovingUp>();
}
}
}
Using AddComponent<>()
we can add any component Unity offers. Also any script you have created in your project is available as a component and you can add it this way. Thus one script could handle all of your cuboid variants. All you needed to do was to change the component you add.
I hope you can see how much easier this approach is. All you need is a Prefab for your cube with no behavior. We can manage everything else from a single script.
Now you could go ahead and even manage the behavior through this script. But down the line, that becomes messy. As we have the option to work in unique scripts and classes, we should make use of this. Robert C. Martin created the term “Single responsibility principal” for this. And it is a good idea to honor this principle.
Half time!
Just as a head up. Once you have reached this point of the chapter you have reached half time. And even better. Everything that follows is pretty much just more of the same. Just a little different. So if you are good with everything you’ve read so far, the rest will be a piece of cake!
First, we will cover variants of the “loop” and then we will look into Lists - essentially a variant of the Array. We will finish this chapter with a look at multi-dimensional arrays.
While Loops
The last two loops have become more uncommon over time, yet they too, still have their uses!
“While” loops are the most basic type of loop and essentially just run as long as a condition stays true. Think of the Update function. It loops each frame, but has no counter to finish after a certain amount of frames. That would be silly. Essentially, it will loop as long as the game is running. The syntax for while loops is very basic:
while(true){
//Code to run
}
All you need is the while
keyword followed by the condition in parentheses. The condition has to evaluate to a boolean. In the above example, I set this to true
. That would be an infinite loop. true
can never become false
. So this will run forever and will freeze your program. So you have to be careful how you construct your conditions with while loops.
While(1 > 0){
//Code to run
}
This again would be an infinite loop, because 1 > 0
will always equate to true.
int i = 0;
while( i < 10){
i++;
//Code to run each iteration
}
This code would work just fine. It’s recreating the for loop manually.
But you could use this in cases, when you want to base the iteration on some kind of outside variable. Just make sure to be able to make the condition become false based on operations inside the while loop.
Do-While Loops
There is one last kind of loop C# offers. The “do-while” loop. As the name suggests, its tightly entangled with the while loop. It also works on a condition you have to turn false
from inside the loop, but the evaluation of the condition is done after the first iteration of the loop. This guarantees that the code inside the loop is run at least once. This is a rather special case, but can be useful in some situations.
int i = 0;
do
{
Debug.Log(i);
i++;
} while (i < 5);
Lists
While you could say that arrays are essentially Lists, in C# they are two slightly different things. And most of the differences matter more to the advanced programmer: Arrays have a fixed size and can thus be placed as one continuous block in memory, while Lists are dynamic and can resize. This leads to both of them having advantages in one area over the other. And an excellent rule of thumb is: If you can get away with an Array of a fixed size, use it!
With technicalities out of the way, let’s look at recreating one of the sketches using lists.
private List<GameObject> cuboids = new List<GameObject>();
You create Lists using the List
Keyword followed by a type T
in angle brackets and the name you want to give to your List. You will also need to create it using the new
keyword. Mind the parentheses at the end!
To add objects to your List during runtime, you use the Add()
method provided by lists.
cuboids.Add(myGameObject));
An example using Lists and the behavior directly attached to the prefab, would look like this:
using System.Collections.Generic;
using UnityEngine;
public class MultiCuboidsLists : MonoBehaviour
{
public GameObject cuboidPrefab;
public float cuboidDistance = 0.225f;
private List<GameObject> cuboids = new List<GameObject>();
void Start()
{
for (int i = 0; i < 5; i++){
cuboids.Add(Instantiate(cuboidPrefab, Vector3.up * (cuboidDistance * i), Quaternion.identity));
}
}
}
We access items in a list in the same way we access them in arrays: Using bracket notation.
void Update(){
for (int i = 0; i < 5; i++)
{
Vector3 position = Vector3.left * Mathf.Sin(Time.time * cuboidDistance * i);
cuboids[i].transform.position = position;
}
}
As you can see, Lists and Arrays are very similar. But look at all the methods Lists supply. You can not only Add()
or Remove()
elements from them. There are many useful things, like Sort()
, Clear()
and even ToArray()
methods. These methods and their flexibility regarding length are the key feature you should look out for, when deciding between lists and arrays.
Multi-Dimensional Arrays
So what if you wanted to position your objects not just in a single axis, but in multiple axis? Now you could probably come up with some way to handle this using ifs or modulos. Or you would just use multidimensional arrays. They work and behave just like one-dimensional arrays, but you will have to nest your loops. You can create as many dimensions as you want, but we will only look at two and three dimensions.
private GameObject[,] cuboidArray = new GameObject[5, 5];
This code would create a two-dimensional Array with an array Length of 5 in each dimension. Now most likely you want to assign GameObjects to your Array using an for loop. What you will need to do now is nest two for loops into one another with a different iterator for each loop.
**
public int arraySize = 5;
public float offset = 1.5f;
void Start()
{
for (int i = 0; i < arraySize; i++)
{
for (int j = 0; j < arraySize; j++)
{
Vector3 cuboidPlacement = new Vector3(i * offset, 0, j * offset);
cuboidArray[i,j] = Instantiate(cuboidPrefab, cuboidPlacement, Quaternion.identity);
}
}
}
As you can see, all we need to do is use i
and j
in our array accessor. To spread out the cuboids, we use i
on the x axis for positioning and j
for the z axis. The offset will spread them out with some space between them.
Please be careful with these! It is easy to underestimate how many objects you create by just typing in some numbers. An 2D array of 10x10 is a 100 cuboids. An 3D array of 10x10x10 is 1000. Put in 100 and suddenly you expect Unity to create a million cuboids!
Now you might think: “A million squares isn’t that much…". Think of the way we handle these right now. There is absolutely no optimization done. We consider each of them equal with all of its components. Thus… Be careful, save your work early and often before you pump up those numbers. (And now go crash Unity, as I know you will…)
All of this works exactly the same, using three dimensions:
MultiDimensionRotationStacked.cs
Projects
Project 1
Again, play with this. Here is some inspiration.
MultidimensionalNoisePosition.cs
MultidimensionalDistancebasedSin
MultidimensionalOffsetGridScale
Project 2
Busy Orbit
You guessed it. We will stay with our spaceships. This time we will create a very busy orbit around our planet and reuse all the concepts we introduced in this chapter.
The beauty of this is mostly how little code we need to achieve this. We will do quite a few things we had to do manually before with just a line of code.
We handle the main logic in one script. We create an empty GameObject as a pivot point for each rocket and then choose a random rocket from an array of rocket prefabs and attach it to its pivot. Then we rotate the all pivots randomly.
Last, we just rotate each pivot, each frame.
You could go wild with this one, randomize colors, rotation speed, size and so forth. But I will leave that up to you.