Working with Objects in Unity (Classes, Variables, Instance Method, Constructors)

Classes

In this tutorial, we’ll be learning about Working with Objects, Classes, Variables, Instance Method, Constructors in Unity.

A class is a type of object. Classes are how we might depict things like “Person” or “Item” as we described before. They’re the syntax we use to say “we plan on using this data type, and it stores these members” – where the members are other data inside it (sometimes also called fields).

Fig. Classes, Variables, Instance Method, Constructors & Objects in UNITY

The class depicts what sorts of objects are stored inside it, what names we use to refer to them, and what type each one is. We declare classes to serve as something of a template for the creation of objects: declare a class, and elsewhere in your code, create instances of it. Each instance stores the same members but is its own unique set of those members – a copy of the base template, so to speak. So when we declare a class, we aren’t creating an object on the spot. We’re just defining a type of object. To declare a class, you use the “class” keyword followed by the class name and then prompt a block:

class Person
{

}

Classes can only be declared within certain kinds of blocks. You can’t declare a class within a method, for example.Looking at our code again, you may notice that all the code we’ve been writing has been inside a class block this whole time – including our Update and Start methods:

public class MyScript : MonoBehaviour
{    
   //etc.
}

There’s some extra syntax there – the “public” keyword and the “ : MonoBehaviour” bit at the end. We’ll learn more about that later.For now, let’s write a simple class and play with it. We’ll write a class resembling a simple item in a game. We’ll declare it (i.e., write the code) inside the “MyScript” class code block we just mentioned, and we’ll cut out all the code we wrote inside our Update and Start methods. You should end up with this:

public class MyScript : MonoBehaviour
{
    class Item
    {
    }
    // Start is called before the first frame update
    void Start()
    {
    }
    // Update is called once per frame
    void Update()
    {
    }
}

Now we have an Item class. We could create instances of it, but they won’t store anything yet, because the code block coming after the “class Item” line is empty, so they’ll just be empty objects.

Variables

When we declare members of a class, we are declaring variables. A variable is a named member which stores an object. Variables always have a type, depicting what kind of object they store. This type can be one of the basic types we’ve already used (int, bool, etc.) or any other class. Remember, classes, are like a template for a certain type of object. So, for example, our Item class could store another Item inside it, as a variable.Let’s add some variables to our Item declaration, and then we’ll dig into the syntax:

class Item
{
    string name = "Unnamed Item";
    int worth = 1;
    bool canBeSold = true;
}

Variables are declared as “[type] [name] = [value];”In the preceding example, we’ve declared three variables:

  • A string named “name” with the value “Unnamed Item”
  • An int named “worth” with the value 1
  • A bool named “canBeSold” with the value “true”

You can declare a variable without the “= [value]” portion – for example, we could have just written “bool canBeSold;”. If you do this, the value will be initialized to a default value. For int and float, this means 0; for strings, it’s the value null, which is what you get when an object is expected but one was never assigned; for bool, it’s always false.

These variables are considered instance variables . They exist on each instance of the class.But we can also create variables in other code blocks.

In this case, they’re called local variables . They’re declared the same way, but they’re not attached to any sort of object. We can just reference them by name when we want. However, they only exist within the block of code they were declared inside and, of course, below the variable declaration itself (not above it). For example, if we declare a local variable inside our Update method, that variable only exists in the Update method code block and in any blocks nested therein. This is why we call them local – they exist only in their local block, the one they were declared in.

Let’s exhibit the creation of an instance of our class. To play around with this, we’ll write our code in the “void Start()” method declared in our script by default. It’s similar to the Update method that we talked about before. It’s a method that the Unity engine will automatically call for us. Update is called once every frame, but Start is called just once when the script first initializes. For a script that is part of the scene, that means it will be called right when the game begins playing. If we were creating a GameObject on the fly, its scripts would have their Start called right as it’s created, before any Update calls go through.

This gives us an easy way to run our code for testing. We’ll start by writing the following code inside our Start method:

Item item = new Item();

This is a variable declaration: first, you’ll notice the type is “Item”, which is the type we declared ourselves. We name it “item”. This is a common naming convention – the names of types and methods will have a capitalized first letter (like Item), while the names of variables will have a lowercase first letter, and if they’re more than one word, then any words after the first will have a capital first letter to make them readable (as in “canBeSold”), which is known as camel case.

We then assign a value to the variable with “=”, after which we see the “new” keyword, followed by “Item()” which is much like calling a class like it’s a method. This gets us an instance of the class and is known as a constructor call.

A constructor is pretty much exactly like a method, but it’s used to return an instance of a class. In this case, we’re calling the default constructor – we didn’t declare it ourselves. Later, we’ll declare a constructor that has parameters (just like a method) for the variables in the class, which lets us assign the variables when the class is created. But let’s get into that later – for now, let’s demonstrate some interaction with our instance.

Accessing Class Members

After the local variable is declared and we’ve called the constructor with the “new Item()” syntax, we can reference the variable by its name, “item”, and, as we exhibited before, use a period to “reach into” the object and access its data. Of course, in this case, the data inside it would be those variables we declared in the class block: name, worth, and canBeSold.We can assign values to the members here by accessing them and using the “=” operator. Let’s assign some values to our new variable:

void Start()
{
    Item item = new Item();
    item.name = "Goblin Tooth";
    item.worth = 4;
    item.canBeSold = true;
}

First, we declare the local variable and create the instance; then, we have three separate statements, each one assigning a value to one of the variables inside the item. We reference those variables by using the period “ . ” to “reach into” the Item instance and access the data stored inside. Of course, these variables are declared in the Item class, and if we tried to access a name that isn’t declared in the class, it would result in an error preventing us from playing the game. And, as we went over earlier, since C# is a strongly typed language, it won’t allow us to, for example, assign a string value to “worth”, because it expects an int. It’ll throw an error in that case too.And speaking of errors, it looks like we’ve got some waiting for us now. If you’ve written the preceding code, saved, and navigated to Unity, you’ll notice the Console window is showing three errors. This leads to the next concept of object-oriented programming that we must address: access modifiers.

An access modifier is a keyword you write before a nonlocal variable declaration. There are three options: publicprotected, and private. They determine whether a member, such as the three variables we declared in the Item class, can be accessed from outside the class itself.

The public modifier means that the member can be freely accessed from outside the class block.

The protected modifier means that the member can be accessed only from within the class block or by classes which “inherit” the class – we’ll get into that concept a few chapters down the road, though, so don’t worry about it too much for now.

The private modifier means the member can only be accessed inside the class block itself.

We didn’t provide an access modifier at all when we declared our three variables (name, worth, and canBeSold). When you don’t provide an access modifier, it always defaults to private.This is the cause of our error: we can’t access the variables from outside the class block itself because they’re private, but we’re trying to.The fix is simple. Put the keyword “public” before each variable declaration in the Item class, so it looks like this:

class Item
{
    public string name = "Unnamed Item";
    public int worth = 1;
    public bool canBeSold = true;
}

Save your code and the errors should go away.Now let’s use those values in a Debug.Log call – just to see something happening and make sure the values are what we expect them to be.In our Start method, add the following line – it must be below the variable declaration and the assigning of the values:

Debug.Log("This " + item.name + " is worth " + item.worth + " golden coins!");

Here, we’re chaining many “+” operators together to add referenced values together into one string. In the end, we’ll expect to have a string saying “This Goblin Tooth is worth 4 golden coins!” And if we changed what we give to the “name” or “worth” variable, the message logged would change as a result.You may have noticed we’re mixing types here. The “item.name” is a string, so that makes sense – a string can be added to a string. But “item.worth” is an int, yet we’re trying to add it to the string. This works just as you’d expect it to – the value of the int is added as number characters to the string, and the resulting string is returned. Some base types can quietly convert to other types like this.So save and play – here’s a quick recap of what your code should look like now:

void Start()
{
    Item item = new Item();
    item.name = "Goblin Tooth";
    item.worth = 4;
    item.canBeSold = true;
    Debug.Log("This " + item.name + " is worth " + item.worth + " golden coins!");
}

You should get a message in the Console window just as you would expect.

Instance Methods

Methods can go inside classes. When you do this, you’ve created an instance method. They’re folded up inside the class itself, so that means you must reference an instance of a class, reach into it with a “ . ”, and then call the method by name.

Because the method is attached to an instance of a class like this, it can seamlessly access any of the variables that belong to the class. You can type “name” to reference the “name” variable or “worth” or “canBeSold”.

Let’s move our Debug.Log call into an instance method. First, we’ll declare the method in the Item class block. Methods have access modifiers too. We haven’t used them yet, but now that we’re declaring a method inside a class, we need to make sure we designate it as “public” so we can access it from outside the class later. It returns “void” (nothing), it will be named LogInfo, and it has no parameters, so an empty set of parentheses “()” after the name:

class Item
{
    public string name = "Unnamed Item";
    public int worth = 1;
    public bool canBeSold = true;
    public void LogInfo()
    {
        Debug.Log("This " + name + " is worth " + worth + " golden coins!");
    }
}

Now you’ll notice that we’ve typed the same Debug.Log message, except this time, we’ve taken out the “item.” before the “name” and “worth” references. Since the method is inside the class block, it must be called through an Item instance, and thus, the method has access to all the members of the Item by default. And by “members” I don’t just mean the variables – if we declared other methods inside the class, we could reference them just by their name as well, and since we’re inside the class block, we could reference them even if they were private.

Of course, this won’t change anything when we play until we actually call the method instead of running the same old Debug.Log line we had before. Replace your old Debug.Log line (in the Start method) with this:

item.LogInfo();

Save and run the game. You should see quite the same message as before. You might think, “Well, what was the point of all this, then?” After all, haven’t we just written more code than we had to and accomplished the same result? Instead of the Debug.Log line in the Start method, we now have a different line, and we declared the method in the class itself!

Well, there’s a little rule of programming called “Don’t Repeat Yourself” or “DRY.” The main point of this is that we now have the method and can call it whenever we need to do the same thing again. Say we wanted to log the same information for a different item, somewhere else in our code, and we never made our instance method. We just copy-paste the Debug.Log call over and we’re done, right? But what if we want to change what the message logs? Now we have two instances of the same code to change. What if we’d copy-pasted it 20 times already? We’ve created a bunch of extra work for ourselves.

There could be a great benefit to having a single place for the code. By creating a method for it, we’ve made sure that it exists in one place only, even if it’s called from many other places, and so if we ever want to change it, we need only change it once. This is one of the reasons why we say Don’t Repeat Yourself. If you find yourself copy-pasting code all the time, you could probably be doing something in a more efficient and clean way, and you might be setting yourself up for heartache somewhere down the road.

As well as this, it splits the code up into relevant portions. The code which logs item-related information is kept neatly folded inside the Item class itself, not inside our code which implements the class. The implementing code simply reaches into “item” and calls a method – no parameters necessary.

Let’s expand on this method and get a little more functionality into it. After all, you’ve learned enough by now to code a method that’s more than just one line, haven’t you?

We’re going to make the method log something different based on whether the item “canBeSold”. We do this with a single “if” and “else” block. Remember, since “canBeSold” is a bool, we can just type “if (canBeSold)” with no need for an “== true” operator:

public void LogInfo()
{
    if (canBeSold)
        Debug.Log("This " + name + " can be sold for " + worth + " golden coins!");
    else
        Debug.Log("This " + name + " cannot be sold.");
}

Since the “if” and “else” are both followed by a single statement only, we don’t need to write curly braces “{}” for their code blocks, as we established before.

Now, the message that’s logged will be different based on whether the item can be sold. If it can’t be sold, there’s no need to tell the user what it’s worth, right?Now, save the code and try it out. The message should be a little different now, since we changed the text in the strings. To make sure our condition is working as we expect, you can also change the “true” to “false” in the Start method when we set “item.canBeSold” and then run the code again. The message should change as expected.

Declaring Constructors

We discussed what constructors are earlier. They’re much like methods, but they’re called to generate an instance of a class. When the instance is created, the code in the declared constructor is run on that instance before the created instance gets returned. When calling a constructor (making an instance of a class), you pretty much call the class by its name, as if it were a method, and you must have the “new” keyword come before it.

It’s a good practice to use constructors to set up new instances of your class. Typically, a constructor will have a parameter for each variable in the class you expect to be set with each instance – whatever custom fields might require a different value each time the class is created. In our case, the three variables we declared ought to be parameters in a constructor – it’s silly to use three separate statements to provide the values of variables that we’re going to set every time anyway, right? Not only that, but constructors make it obvious to you, and anyone else using your code, which values are meant to be assigned when an instance of a class is created. They are orderly, and they set a standard for the usage of a class.

Constructors are declared inside the class block itself. They are as simple as “[access modifier] [class name]([parameters]) {…}”. The access modifier we desire is “public”. Private constructors can only be used from within the class itself – there are some cases where this is handy, but this is not one of them, so we need to make sure we type “public” because if we don’t, “private” will be defaulted.

This is how our constructor will look – written within the code block of “class Item”:

public Item(string name, int worth, bool canBeSold)
{
    this.name = name;
    this.worth = worth;
    this.canBeSold = canBeSold;
}

Before we get into the statements within, let’s review the declaration itself. Start with the access modifier “public” and then the name of the class, “Item”. Think of it like declaring a method that doesn’t have a name, just the access modifier and then the return type – after all, constructors always return the type of the class itself, since they’re used to create an instance of it. Then we do the same set of parentheses “()” we’re used to, with parameter declarations just as we declared for our methods.

Now, what are these three statements beginning with “this.”? It’s simple – they’re assigning the values of the parameters to the values of the variables in the class instance.

When we made instance methods just a little bit ago, we exhibited that you can simply type the name of a class variable to reference that variable. This is the same within a constructor. The class instance already exists, and the code is running on it immediately after. So within this constructor, if we type “name”, we get the value of “name” for the class instance that’s being created by the constructor. All the variables for this instance already exist.

But since the parameters are named the exact same thing as the variables themselves, “name” also refers to the parameter. We can’t just type “name = name;” and expect the computer to know what we’re talking about. That creates ambiguity that the computer cannot solve on its own – the compiler doesn’t make guesses like this. It needs us to clear the situation up. So to avoid this confusion, we use “this”, which is a keyword that always refers to the instance that the code is running for – the class instance being created. By referencing “this” first, we take the ambiguity out of the situation – the computer no longer sees it and says “Wait, what?” It sees that we mean to say “set the value of the class instance variable ‘name’ to the value of the parameter ‘name’”.

The “this” keyword can be used in instance methods as well as constructors, if you ever need a means of referencing the instance itself. A very common use case for it is to avoid name conflicts as we’ve just demonstrated.

Another solution to these name conflicts would be to simply not name the parameters the exact same thing as the class variables. For example, we could put an underscore before each parameter name and take the “this.” out:

public Item(string _name, int _worth, bool _canBeSold)
{
    name = _name;
    worth = _worth;
    canBeSold = _canBeSold;
}

But this is frowned upon. The former way (using “this.”) is the norm, because it’s clear, concise, and obvious. The parameters are being applied directly to the variables themselves, so why name them anything else?

Now that you’ve added the constructor to the Item class, it should look like this:

class Item
{
    public string name = "Unnamed Item";
    public int worth = 1;
    public bool canBeSold = true;
    public Item(string name, int worth, bool canBeSold)
    {
        this.name = name;
        this.worth = worth;
        this.canBeSold = canBeSold;
    }
    public void LogInfo()
    {
        Debug.Log("This " + name + " is worth " + worth + " golden coins!");
    }
}

Using the Constructor

Now that we’ve declared our constructor, saving the code and checking Unity will result in an error in the Console window. A class with no constructors declared will have a single, default constructor automatically provided, which takes no parameters and returns an instance of the class with all its variables at their default settings. But once you’ve declared your own constructor, this default constructor will no longer exist for the class. There is now only one way to declare the class, and it takes three parameters, but we’re still calling the constructor with no parameters at all in our Start method.

So navigate back to this code in your Start method:

Item item = new Item();
item.name = "Goblin Tooth";
item.worth = 4;
item.canBeSold = true;

This is where our error occurs. We’re trying to make an Item without giving any parameters to the constructor. You can probably guess how we’ll go about changing this to call the constructor instead. Replace the code with this:

Item item = new Item("Goblin Tooth", 4, true);

We’ve turned our repetitive four lines of code into one clean line. The parameters are provided in the constructor call, just like with our methods, and of course, we must follow the same order that the parameters were declared in the constructor: “name” first, then “worth”, and then “canBeSold”.

All the constructor code will be executed before the next line of code after we declare our “item” variable, so we know all the fields are assigned before the instance is returned to us. Everything is set up and ready for the instance to be used in a neat and consistent way.

Now if we create Item instances in a hundred different places in our game code, we can just change one block of code – the constructor declaration – if we ever need to change how items are set up when they are created.

Static Members

One last thing we’ll cover about classes is the idea of static members.

Instanced variables, like the variables we declared for our Item class, exist as separate pieces of data on each Item instance we create.

An instanced method, like the LogInfo method, operates through an instance of the Item class. You must reach into an Item instance to call the LogInfo method. Since it’s running through an instance of the Item, it can work with the instanced variables, as our method does to log the name and worth of the item.

Static variables, however, exist as one instance for the entire class. From outside the class, you must reach into the name of the class itself, like “Item”, to access its static members.

Static methods work in the same way, and since they can be called simply by reaching into the class name, this means the method cannot access instanced members since there is no specific instance tied to the call. For example, if we made our Item.LogInfo method into a static method instead, it would throw compiler errors when we try to access those instanced variables “name” and “worth”.

Let’s demonstrate. An easy example would be to count how many instances of the Item class are created. We’ll update the Item class to add the static members, marked in bold:

class Item
{
    public static int NumberOfInstances = 0;
    public string name = "Unnamed Item";
    public int worth = 1;
    public bool canBeSold = true;
    public Item(string name, int worth, bool canBeSold)
    {
        NumberOfInstances += 1;
        this.name = name;
        this.worth = worth;
        this.canBeSold = canBeSold;
    }
    public void LogInfo()
    {
        Debug.Log("This " + name + " is worth " + worth + " golden coins!");
    }
    public static void LogInstanceCount()
    {
        Debug.Log("Number of Item instances is: " + NumberOfInstances);
    }
}

First, we declare a static variable, NumberOfInstances. It’s declared just like a normal variable, but after the “public” keyword, we specify a “static” keyword as well. We start it with a default value of 0. In the constructor, we use the “+=” operator to add 1 to the NumberOfInstances every time the constructor is called – in other words, every time a new instance of Item is created.

We then declare a static method that logs the number of Item instances that exist, accessing the NumberOfInstances variable. It’s made static just like the variable, by typing “public static” instead of just “public”.

Within that method, any attempt at accessing an instanced member will result in compiler errors. We can’t call LogInfo or use our name, worth, or canBeSold variables because the static method isn’t tied to any particular instance of Item, and those variables exist on every instance. The NumberOfInstances variable, however, is tied to the class itself, so we can access it.

Let’s update our Start method to demonstrate calling the method. To demonstrate the count going up after we create our first Item, we’ll call it once before the item is created and once again after:

void Start()
{
    Item.LogInstanceCount();
    Item item = new Item("Goblin Tooth",4,true);
    Item.LogInstanceCount();
}

As you can see, we’re calling the method through a reference to the actual type name itself, “Item”, with an uppercase “I”. We aren’t referencing the instance we’re storing in our local variable, which is “item” with a lowercase “i”.

If you test this updated code, you should see two messages:

Number of Item instances is: 0

Number of Item instances is: 1

This demonstrates the NumberOfInstances going up when we create the new Item.

Now that you understand this distinction, you might realize we’ve already called a static method before: Debug.Log. “Debug” is just a class, and “Log” is a public static method declared inside it. Thus, we can access Debug.Log whenever we want.

Written by

XR Developer responsible for end-to-end development of XR solutions spanning multiple domains, by using various XR and WebXR libraries.

Leave a Reply