Defining Classes & Structs — Understanding reference and value types — Unity C#

Imran Momin
9 min readApr 22, 2021

Classes are reference types — that is, when they are assigned or passes to another variable, the original object is referenced, not a new copy.

Basic Syntax

Classes are created using the class keyword as follows:

accessModifier class UniqueName
{
Variables
constructors
Methods
}

Any variables or methods declared inside a class belong to that class and are accessed through its unique name.

Creating a character class

Declare a public class called Character followed by a set of curly braces.

Character is now registered as a public class blueprint. This means that any class in the project can use it to create characters. However, these are just the instructions — to create a character takes an additional step. This creational step is called instantiation.

Instantiating class objects

Instantiation is the act of creating an object from a specific set of instructions, which is called an instance. If classes are blueprints, instances are the houses built from their instructions; every new instance of Character is its object, just like two houses built from the same instructions are still two different physical structures.

Creating a new character

We declared the Character class as public, which means that a Character instance can be created in any other class.

Create a new script in Unity (I name it “Learning Curve”), let’s declare a new character type variable, called “hero”, in the Start() method.

The variable type is specified as Character, meaning that the variable is an instance of that class.

The variable is named “hero”, and it is created using the new keyword, followed by the Character class name and two parentheses. This is where the actual instance is created in the program’s memory, even if the class is empty right now.

Adding class fields

Any variables belonging to a class are created with the class instance, meaning that if there are no values assigned, they will default to zero or null. In general, choosing to set initial values comes down to what information they will store:

· If a variable needs to have the same starting value whenever a class instance is created, setting an initial value is a solid idea.

· If a variable needs to be customized in every class instance, leave its value unassigned and use a class constructor.

Fleshing out character details

Let’s incorporate two variables to hold the character’s name and the number of starting experience points:

  1. Add two public variables inside the Character class — a string variable for the name, and an integer variable for the experience points.
  2. Leave the name-value empty, but assign the experience points to 0 so that every character starts from the bottom.

3. Add a debug log in the LearningCurve script right after the Character instance was initialized. Use it to print out the new Character’s name and exp.

When the hero is initialized, the name is assigned a null value that shows up as an empty space in the debug log, with exp printing out 0. Notice that we didn’t have to attach the Character script to any GameObject in the scene; we just referenced them in LearningCurve and Unity did the rest. The console will now debug our character information.

Using constructors

Class constructors are special methods that fire automatically when a class is created, which is similar to how the Start method runs. Constructors build the class according to its blueprint:

· If a constructor is not specified, C# generates a default one. The default constructor sets any variables to their default type values — numeric values are set to zero, Booleans to false, and reference types (classes) to null.

· Custom constructors can be defined with parameters, just like any other method, and are used to set class variable values at initialization.

· A class can have multiple constructors.

Constructors are written like regular methods but with a few differences; for instance, they need to be public, have no return type, and the method name is always the class name.

As an example let’s add a basic constructor with no parameters to the Character class and set the name field to something other than null.

Run the project in Unity and you will see the hero instance using this new constructor. The debug log will show the hero’s name as “Not assigned !” instead of a null value.

Specifying starting properties

Now, the Character class is starting to behave more like a real object, but we can make this even better by adding a second constructor to take in a name at initialization and set it to the name field:

  1. Add another constructor to Character that takes in a string parameter, called name.
  2. Assign the parameter to the class’s name variable using the “this” keyword. This is called constructor overloading.

3. Create a new Character instance in LearningCurve, called heroine. Use the custom character to pass in a name when it’s initialized and print out the details in the console.

We can now choose between the basic and custom constructor when we initialize a new Character class. The Character class itself is now far more flexible when it comes to configuring different instances for different situations.

Declaring class methods

Printing out character data — Example

Our repeated debug logs are a perfect opportunity to abstract out some code directly into the Character class:

  1. Add a new public method with a void return type, called PrintStatsInfo, to the Character class.
  2. Create a debug log and change the variables to name and exp, since they can now be referenced from the class directly.

3. Replace the character debug log that we previously added to LearningCurve with method calls to PrintStatsInfo.

Now that the Character class has a method, any instance can freely access it using dot notation. Since hero and heroine are both separate objects, PrintStatsInfo debugs their respective name and exp values to the Console.

Declaring structs

Structs are similar to classes in that they are also blueprints for objects you want to create. The main difference is that they are value types — meaning they are passed by value instead of reference.

Basic syntax

Structs are declared in the same way as classes and can hold fields, methods, and constructors.

accessModifier struct UniqueName
{
Variables
constructors
Methods
}

Like classes, any variables and methods belong exclusively to the struct and are accessed by their unique name.

However, structs have a few limitations:

· Variables cannot be initialized with values inside the struct declaration unless they are marked with the static or const modifier.

· Constructors without parameters are not permitted.

· Structs come with a default constructor that will automatically set all variables to their default values according to their type.

Every character requires a good weapon, and these weapons are the perfect fit for a struct object over a class.

Creating a weapon struct — Example

  1. Create a public struct, called Weapon, in the Character script. (Add a field for the name of type string and another field for damage of type int.)

2. Declare a constructor with the name and damage parameters, and set the struct fields using this keyword.

3. Add a debug method below the constructor to print out the weapon information.

4. In LearningCurve, create a new Weapon struct using the custom constructor and the new keyword.

Even though the Weapon struct was created in the Character script, since it is outside of the actual class declaration (curly braces), it is not part of the class itself. Our new huntingBow uses the custom constructor and provides values for both fields on initialization.

Understanding reference and value types

Classes are best suited for grouping together complex actions and data that will change throughout a program; structs are a better choice for simple objects and data that will remain constant for the most part.

Besides their uses, they are fundamentally different in one key area — that is, how they are passed or assigned between variables. Classes are reference types, meaning that they are passed by reference; structs are value types, meaning that they are passed by value.

Reference types

When the instances of our Character class are initialized, the hero and the heroine variables don’t hold their class information — instead, they hold a reference to where the object is located in the program’s memory. If we assign hero or heroine to another variable, the memory reference is assigned, not to the character data. This has several implications, the most important being that if we have multiple variables storing the same memory reference, a change to one affects them all.

Creating a new hero — Example

It’s time to test that the Character class is a reference type.

  1. Declare a new character variable in LearningCurve, called hero2. Assign hero2 to hero, and use the PrintStatsInfo method to print out both sets of information.

2. Click on play and take a look at the two debug logs that show up in the console. The two debug logs will be identical because hero2 was assigned to hero when it was created. At this point, both hero2 and hero point to where hero is located in the memory.

3. Now, change the name of hero2.

4. Click on Play. You will see that hero and hero2 have the same name, even though only one of our character’s data was changed.

The reference types need to be treated carefully and that they are not copied when assigned to new variables. Any change to one reference trickles through all other variables holding the same reference.

Value types

When a struct object is created, all of its data is stored in its corresponding variable with no references or connections to its memory location. This makes structs useful for creating objects that need to be copied quickly and efficiently, while still retaining their separate identities.

Copying weapons — Example

Let’s create a new weapon object by copying huntingBow into a new variable, and updating its data to see whether the changes affect both structs:

  1. Declare a new Weapon struct in LearningCurve, and assign a huntingBow as its initial value and print out each weapon data using the debug method.

2. The way they are setup now, both huntingBow and warBow have the same debug logs, just like our two characters did before we changed any data.

3. Change the warBow.name and warBow.Damage fields to values of your choice.

4. Click on Play. The console will show that only the data relating to warBow was changed, and that huntingBow retains its original data.

Structs are easily copied and modified as their separate objects, unlike classes that retain references to an original object.

--

--

Imran Momin

A VR/AR developer, who enjoys making games and developing interactive environments using Unity’s XR integration toolkit for Oculus quest and HTC vive devices.