Core Concepts
Before you begin writing code for games, take some time to get comfortable with the C# language and learn some fundamental concepts that will help you when programming in any language.
Variables & Data Types
Variables are used to store information. Depending on the program requirements, you may need to store a variety of types of data including integers, decimals, text, and so on. In C#, the type of data must be specified when a variable is created, or declared.To get started, work from the previous project where you created a CubeBehaviour script that printed
Hello Worldto the console. For simplicity, remove the Update function leaving the Start function that will execute only once. Modify the code in the Start method as follows.
- Declare a variable named score that stores integer values and has an initial value of 0.
- Declare a variable named xp that stores decimal values and has an initial value of 3.5f
- Print the value that is stored in the variable named score to the console.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CubeBehaviour : MonoBehaviour { // Start is called before the first frame update void Start() { int score = 0; float xp = 3.5f; Debug.Log("Score: " + score); } }
This code may seem simple but there is a lot going on. First, in line 10 we are both declaring a variable to be of type int (meaning it will store an integer) and we are using the assignment operator (the equal sign) to assign the value of zero to the variable.
Similarly, in line 12, xp is declared as a variable of type float (meaning it will store a decimal) and the assignment operator is used to set the initial value of the variable to 3.5f. Setting an initial value for a variable is called initializing.
The orange squiggly line under the variable name on line 12 indicates a warning. Hovering over the variable name reveals the details of the warning. In the Light theme, red squiggly lines indicate errors.
You can also see code warnings and errors inside of the Unity Console. If you double-click on the error message inside of Unity, it will take you directly to the line in Visual Studio with the issue.
Warning and error messages become easier to read as you gain more programming experience. This warning is stating that the variable we created is never used. If you were not planning to expand the code further to use this variable, it would be a waste of memory to leave this line of code to store a value that is never used.
If you save your changes in Visual Studio and then press the Play button in Unity, you should see the value of zero printed to the console.
Printing the string "score" is different than printing the value of the variable named score.
Modify the print statement to print the value of xp instead of the value of score. The print function is quite versatile in that it can print strings as well as the values of variables of various types (such as int and float).
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CubeBehaviour : MonoBehaviour { // Start is called before the first frame update void Start() { int score = 0; float xp = 3.5f; Debug.Log("XP: " + xp); } }
Of course, once you begin using xp instead of score, a warning appears to indicate that score has been declared but never used.
If you save your changes in Visual Studio and then press the Play button in Unity, you should see the value of 3.5 printed to the console.
Notice, the letter f that follows the value of 3.5 in the code is not printed to the console.
You may wonder why float is the type for variables storing decimal values instead of something more meaningful, like decimal. You may also wonder why the letter f is attached at the end of values assigned to float type variables. The explanation for both of these curiosities is that there are two different types in C# that can store decimal values, the float and the double.
A float refers to a floating-point value. Just imagine an integer with a decimal point that can float forward or backward within the digits. A variable of type float (i.e. a float) requires 4 bytes of memory and can store approximately 6 to 9 digits, depending on the operating system the program is running on.
A double can be used to store decimal values with greater precision. A variable of type double (i.e. a double) requires 8 bytes of memory (that is
doublethe memory of a float) enabling it to store 15 to 17 digits. While the added precision provided by doubles is sometimes needed, often the precision afforded by floats is sufficient and therefore preferred in order to use less memory.
In C#, a decimal value is automatically stored as a double unless the letter f is appended to the value, in which case the decimal value is stored as a float. If you don't add the letter f to a number with decimals and try to store the value in a float variable, you will see this error.
Other types exist for variables that store non-numeric data. For instance, variables of type bool store Boolean values of true or false. Variables of type char store a single character such as 'a', 'B', '$' or '4'. Variables of type string store a series of characters, such as "Hector", "Games are fun!", and "007".
string firstName = "Word or a phrase"; char letter = 'h'; bool isReady = false;
Variables of the same type can also be stored in various structures, the most simple of which is called an array. Arrays are fixed in size and type with very few operations that can be run on them, but this allows them to be both small in size and flexible in their implementation.
Arrays are notated with square brackets [] and are declared as their type followed by these brackets. In instantiating an array there are two options, one for when you know what values you want in your array and one for when you simply want the array to be a set of empty slots for values to be assigned later. See the code below for an example of each.
//An int array of length 5 instantiated with specific values int[] intArray = {0, 1, 2, 3, 4}; //A char array of length 5 but without specified values char[] charArray = new char[5];
Keep in mind that the types used are arbitrary, the syntax is identical for any given data type.
To access a value inside of an array simply put the number of the entry you want inside square brackets after the variable name. Keep in mind that the array sarts counting at 0 and asking for a position not in the array will trigger an IndexOutOfRangeException.
Simply using the name without brackets refers to the array itself. This is mostly used to ask the array the number of values it can hold by referencing its length field as seen below.
Make the following edits to CubeBehaviour and run the scene.
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CubeBehaviour : MonoBehaviour { // Start is called before the first frame update void Start() { int[] arr = {0, 1, 2, 3, 4}; Debug.Log(arr.length); Debug.Log(arr[0]); arr[0] = 999; Debug.Log(arr[0]); Debug.Log(arr[arr.length]); } }
Notice that an exception occurs when it tries to access element 5 of the array, always be mindful that the indecies of an array always start at 0 and end at length - 1.
Conditional Statements
Conditional statements are used frequently in programming when the program must determine what to do based on the current value of one or more variables. Consider the following code.void Start() { string firstName = "Mario"; if(firstName == "mario") { Debug.Log("Welcome " + firstName); } }
Code snippets in this course may show only modified code. Pay attention to line numbers. For instance, the first three lines of this script are not shown because they contain the same unchanged using directives from the earlier versions of the script. Lines four through seven only contain empty space and the class declaration which remain unchanged and so are similarly excluded.
This code executes once when the Cube the script is attached to first enters the scene (i.e. the beginning of the game). The variable firstName is of type string and is initialized to the value of
Mariousing the assignment operator (a single equal sign).
Line 12 is a conditional statement used to determine if the related code block (lines 13 to 15) will execute or not. The condition that is checked is inside the parenthesis that follow the if keyword. In this case, the condition resolves to be false because the value of firstName does not equal
mario. This is because the lower-case character ‘m’ is not the same as the upper-case character ‘M’.
Notice the double equal sign is used within the conditional statement instead of a single equal sign. This is because the double equal is a Boolean operator used for comparisons while the single equals sign is the assignment operator used to change the value of a variable.
Using a single equal sign within a conditional statement will not cause a compile error (in some languages) but will change the value of the variable and result in unexpected behaviors. While using Unity and C#, this will return an error message and prevent you from running the code.
Change the string in line 12 to
Marioand run the program to ensure that the message is now being printed to the console.
Else statements are often appended after an if block to define code that should execute when the condition fails.
void Start() { string firstName = "Sonic"; if(firstName == "Mario") { Debug.Log("Welcome " + firstName); } else { Debug.Log("Who are you?"); } Debug.Log("Goodbye"); } }
The else statement should come immediately after the end of the if block.
When this code runs the conditional statement should evaluate to false because
Sonicis not equal to
Mario, causing the code in the else block to execute and print
Who are you?to the console.
The
Goodbyemessage will print as the program continues to run because it is not part of the conditional statement.
Conditionals can also be chained by combining the if and else keywords as seen below.
void Start() { string firstName = "Sonic"; if(firstName == "Mario") { Debug.Log("Welcome " + firstName); } else if(firstName == "Sonic") { Debug.Log("Hello there, " + firstName); } else { Debug.Log("Who are you?"); } Debug.Log("Goodbye"); }
Boolean Operators
There are many Boolean operators other than the double equal sign that may be used within conditional statements. Additionally, conditional statements may evaluate multiple parts using the double ampersand operator && forandand the double pipe operator || for
or. Consider the following code:
void Start() { float speed = 15f; if(speed > 5 && speed <= 20) { Debug.Log("The Speed: " + speed); } }
Here, using the && operator, both conditions must be true for the entire condition to be true. That is, speed must be greater than 5 AND speed must be less than or equal to 20 for the entire condition to be true. If either condition is false, the entire condition is false.
void Start() { float speed = 5f; if(speed <= 5 || speed > 20) { Debug.Log("The Speed: " + speed); } }
Here, using the || operator, the entire condition is true if speed is less than or equal to 5 OR speed is greater than 20. If either condition is true, the entire condition is true.
The not operator and the not equal operators are also frequently useful within conditional statements.
void Start() { int score = 0; if(!(score == 3)) { Debug.Log("Score: " + score); } }
The not operator (i.e. the exclamation mark) negates a Boolean value. That is, it changes any value of true to false and any value of false to true.
Line 11 may be read as
if not score is equal to 3.
void Start() { int score = 0; if(score != 3) { Debug.Log("Score: " + score); } }
The not equal operator (i.e. the exclamation followed immediately by the equal sign) is a more elegant way to write the same line of code.
Consider the use of Boolean variables within conditional statements.
void Start() { bool hasKey = true; if(hasKey == true) { Debug.Log("You have a key!"); } }
Here, the hasKey variable is defined as a bool so it may store only values of true or false.
Of course, in a full game the initialization of the variable would likely be in the Start method, but the conditional statement would occur elsewhere, at the moment the player tries to open the door.
The conditional statement on line 11 is redundant and may be written more elegantly.
void Start() { bool hasKey = true; if(hasKey) { Debug.Log("You have a key!"); } }
Now, the value of hasKey is immediately used to determine if the conditional statement succeeds or fails.
This approach requires less code and improves readability as long as the Boolean variable is well named.
The same shortcut applies when checking if a Boolean is false. The three conditional statements below are equivalent, but the statement on line 11 is preferred by most experienced programmers.
void Start() { bool hasKey = false; if (!hasKey) { Debug.Log("You Don't Have a Key"); } }
Notice that the conditions of the if statements are followed immediately by a command instead of by a block of code wrapped with curly braces. This is acceptable when only a single command is needed to run when the condition is true. Curly braces are only necessary if multiple commands need to run when the condition is true. Still, many programmers prefer to always using curly braces, in part because they feel it improves readability.
Variable Scope
All variables defined in examples up to this point have been local variables. That is, they were declared within a function (such as the Start or Update function) and could only be used within the function in which they were declared. Local variables have limited scope. Additionally, each time the function runs, the local variables are declared again in new places in memory and with new initial values. The values stored from previous executions of the function are no longer accessible.Often, you will need to use class variables which have a greater scope. Class variables are accessible by all functions defined in the class (including the Start and Update functions). Class variables also retain their values when a function finishes running. Class variables will be used in the next section.
Mathematical Operators
A powerful feature of variables is that their values can change. A variable score of type int may begin with a value of 0 and then change to any other integer value while the program runs. In addition to using the assignment operator to change the value of variables, many mathematical operators exist for changing numeric values. Consider the following script that increases the value of numSpacePressed every time the space bar is pressed.public class CubeBehaviour : MonoBehaviour { int numSpacePressed = 0; void Update() { if(Input.GetKeyDown("space")) { numSpacePressed = numSpacePressed + 1; Debug.Log(numSpacePressed); } } }
Here, the numSpacePressed variable is declared within the CubeBehaviour class but outside of any one function. This makes it a class variable that can be used by all functions defined within the class, including the Update function. It also means that the value of the variable is retained even when the function is finished running.
Typically, all class variables are defined above the class functions.
Line 11 utilizes the Input.GetKeyDown function that is provided with the Unity core library. This function returns true if the key specified was just pressed.
In the Update method (which runs repeatedly every frame) the value of numSpacePressed is increased using the + operator if the space bar is pressed down. Line 13 can be read as
let numSpacePressed be equal to the current value of numSpacePressed plus one more.
Running this script should result in the value of the variable increasing and being printed to the console every time the spacebar is pressed.
The += operator can be used to accomplish the same thing in a more concise manner.
public class CubeBehaviour : MonoBehaviour { int numSpacePressed = 0; void Update() { if(Input.GetKeyDown("space")) { numSpacePressed += 1; Debug.Log(numSpacePressed); } } }
Now line 13 can be read as
increase the value of numSpacePressed by 1.
The += operator allows you to increase a numeric variable by any value. However, to increase by one there is an even more concise method.
public class CubeBehaviour : MonoBehaviour { int numSpacePressed = 0; void Update() { if(Input.GetKeyDown("space")) { numSpacePressed++; Debug.Log(numSpacePressed); } }
The ++ operator was introduced because of how often programmers need to increase variables by one. Writing +=1 is just too much extra effort!
Just as the +, ++, and += operators are used to increase numeric values; the -, -= and -- operators exist for decreasing numeric values. Other common math operators include the asterisk * for multiplication and the forward slash / for division.
Loops
Often, when coding you will find that you need to run a certain section of code multiple times. This concept is called iteration and is essential to create efficient and compact code. Generally speaking there are two key types of iteration, recursion and loops, however for this lesson we will focus on the latter.Loops are blocks of code that can be set to run multiple times, either using a set number of runs or by using a condition. Inside these code blocks, just as with conditionals, you may code whatever behaviors you want, including other loops (called nesting).
The most kind of loop is called a for loop. This is because it uses the for keyword, which is then followed by a series of three fields seperated by semicolons. ;
The first field is the declaration and assignment of an index variable typically named i. The second is the stop condition for the loop as a boolean expression. Lastly is the index operation, a simple expression that modifies the index variable.
public class CubeBehaviour : MonoBehaviour { void Start() { for(int i = 0; i <= 10; i++) { Debug.Log(i); } }
Loops are also an excellent way of running a set of opperations across all of the elements of an array. when doing so we can use i to index the array and using the length value of the array dynamically set our stopping condition. Please note that we want to only run our loop when i is less than the length of the array, or else we will incur an IndexOutOfRangeException.
public class CubeBehaviour : MonoBehaviour { private int[] arr = {10, 11, 12}; void Start() { for(int i = 0; i < arr.length; i++) { Debug.Log(arr[i]); } }
Overloading Operators
Operators may be overloaded, which means it will know what to do even with different variable types. For instance, the + operator has been overloaded in C# to behave differently when placed between strings. The behavior of combining the two strings together into one string is called concatenation. For example, concatenation is often used within print statements.string name = "Mario"; int number = 3; void Start() { Debug.Log("This is " + name + " the number is " + number); }
Another example of overriding an operator involves Vector3 objects. In Unity, a Vector3 is an object that contains three float values. In fact, the position and scale of every Transform is stored as a Vector3. Please note that rotation is not stored as a Vector3 but rather as a Quaternion, which are much less usable by default.
The Vector3 for the scale is defined by the float values that represent the scale along the x, y, and z axis.
There is no default behavior for adding or subtracting Vector3 objects in C#. Unity has provided the Vector3 class for us to create Vector3 objects and overridden the +, -, +=, and -= operators to allow us to add and subtract vectors.
Consider the following script that increases and decreases the size of the cube when the up and down arrow keys are pressed.
public class CubeBehaviour : MonoBehaviour { Vector3 deltaScale; void Start() { deltaScale = new Vector3(0.02f, 0.02f, 0.02f); } void Update() { if (Input.GetKey("up")) { transform.localScale += deltaScale; } else if (Input.GetKey("down")) { transform.localScale -= deltaScale; } } }
Lines 16 and 20 utilize the Input.GetKey function that is provided with the Unity core library. Unlike the Input.GetKeyDown function previously demonstrated, this function returns true if the key specified is currently being held down.
Run the code and hold down the up and down arrows to cause the cube to grow and shrink.
Line 7 of the script defines the class variable deltaScale of type Vector3. A Vector3 is not a primitive type (such as int, float, bool) so cannot be simply assigned a single value. Instead, a Vector3 is a class provided by Unity that may be used to create Vector3 objects using three float values. The new keyword is used to create a new instance of a Vector3 with initial float values.
The delta in the variable name is from the Greek letter Δ (delta) and is commonly used in mathematics to mean
change in. The deltaScale variable therefore represents the
change in scalethat will be made on the cube each frame that the up or down arrow is being held down. Adding deltaScale to the scale vector of any object will increase that object's size on the x, y, and z axis by 0.02. If the initial scale is 1 on every axis, 0.02 represents 2% of that initial size.
Line 18 and 22 use the deltaScale to increase and decrease the cube from its current size. The transform.localScale property references the Vector3 object for the cube's scale. For instance, in line 18 the values in deltaScale (0.02, 0.02, and 0.02) are added to the current x, y, and z values of the cube's scale because of how the += operator was overloaded in the Vector3 class. You can see these values change in the inspector as the cube grows and shrinks.
Access Modifiers
Variables declared inside a class without an access modifier default to private. Private variables may only be accessed by code within the class they are defined. Adding the public access modifier to a variable allows it to be accessed by other classes. Imagine a player not having access to the health of an enemy and therefore being unable to deal any damage!In Unity, adding the public access modifier has another use. On line 7, add the public keyword in front of the declaration of the deltaScale variable.
public class CubeBehaviour : MonoBehaviour { public Vector3 deltaScale; void Start() { deltaScale = new Vector3(0.02f, 0.02f, 0.02f); } void Update() { if (Input.GetKey("up")) { transform.localScale += deltaScale; } else if (Input.GetKey("down")) { transform.localScale -= deltaScale; } } }
Save the code and return back to Unity. You may need to wait for a few seconds for the Inspector window to update, but you should see your public variable appear under your script name. When you play the game, you will see the values change from 0 to 0.02, since that's what the script does during the Start function.
Notice that our variable was named deltaScale but is displayed in the Inspector window as
Delta Scale. Unity chooses to display variable names in this way for readability. Rest assured that they refer to the same variable.
To be consistent with other programmers and the expectations of the Unity Engine, use the camel-case naming convention where all variable names beginning in lower-case and then an upper-case letter (a hump) is used for each new word within the variable name. For example, a variable to store a player's first name could be firstName in code and, if it was made public, would appear in the Inspector Window as
First Name.
Consider the benefits of having properties (i.e. class variables) appear in the Inspector Window. This script can be attached to multiple GameObjects to give them the same behavior. The values in the Inspector Window could be set differently for each GameObject the script is attached to, and this can be done without ever editing the code. Very powerful indeed.
Alternative Solutions
When programming, it is usually worthwhile to consider all options for adding functionality or solving a problem. For instance, if we wanted the same scale for the x, y, and z values so the object could not be stretched more in one direction, we would only need a single value for setting the change in scale to appear in the Inspector. Consider the following changes.public class CubeBehaviour : MonoBehaviour { public float scaleSpeed = 0.02f; public Vector3 deltaScale; void Start() { deltaScale = new Vector3(scaleSpeed, scaleSpeed, scaleSpeed); } void Update() { if (Input.GetKey("up")) { transform.localScale += deltaScale; } else if (Input.GetKey("down")) { transform.localScale -= deltaScale; } } }
On line 7, scaleSpeed is declared as a public float and given a default value. On line 8, deltaScale is declared as a class variable so it may be used in both the Start and Update methods, but it is not made public. On line 12 within the Start method, deltaScale is initialized to a Vector3 with float values all set to the same value as scaleSpeed.
Save the code and return to Unity.
The Inspector should update to show only the scaleSpeed property with its default value. The value can be changed prior to playing the game and will be used in the first frame to set the value of deltaScale.
Functions
Where variables (i.e. properties) are useful for saving data, functions (i.e. methods) are used to group together commands that will complete an action. For example, the print function is used to print text to the console.Programmers are not limited to functions that are provided to them. They may create their own functions to complete any action they wish. Consider the following code.
public class CubeBehaviour : MonoBehaviour { void Start() { printPasswords(); } private void printPasswords() { Debug.Log("Passwords:"); Debug.Log("Mario"); Debug.Log("Sonic"); } }
On lines 12 through 17, the custom printPasswords method is defined. This function definition provides the list of commands that should execute when the function is called.
Line 9 is a function call. Without a function call, the code in the function definition would never execute.
Unity automatically calls some functions without the need of writing a function call. This is how the Start and Update method get called.
There are many reasons why functions are useful. They provide more readable code by condensing many commands into a single function call with a meaningful name. Functions also reduce code redundancy. Imagine you needed to print the passwords several times through your program. If the commands were not in a function, you would have to repeat them several times. This approach could be tedious if there are many commands that need repeated. Additionally, maintaining the code would quickly become more difficult as changes to the commands would need to be made in several places. If the commands are in a function, you may call that function as many times as needed from anywhere in your code which has access to the function.
Notice the words private and void preceding the function name on line 12. The private keyword makes the function visible only to other functions within the CubeBehaviour class—those defined between lines 6 and 18. Code outside of the class would not have access to use this function. The void keyword is included to define the type of value that will be returned by the function. A function with a return type of void will return no values.
Sometimes you want a function to return a value. Consider the following code.
public class CubeBehaviour : MonoBehaviour { void Start() { int x = sum(); Debug.Log(x); } int sum() { return 15 + 237; } }
On lines 13 through 16, the custom method named sum is defined with int as the type of value that should be returned.
On line 15, an integer value (the sum of 15 and 237) is returned using the return keyword. The return statement is typically placed in the last line of a function.
On line 9, the function call is made and the return value is stored in the variable x. The value of x is then printed out via the print command on line 10.
The return value of a function can be stored into a variable for later use as per the previous example, or used immediately without storing the value as in the following command.
void Start() { Debug.Log(sum()); }
For function definitions and function calls in C#, the name of the function is always followed by parenthesis. Sometimes the parentheses contain parameters (i.e. values) to be sent to and used by the function and other times they are left empty with no parameters.
Functions can receive data (via parameters) and return data (via a return value). Let's expand the sum function to make it more useful.
public class CubeBehaviour : MonoBehaviour { void Start() { int x = sum(15, 237); Debug.Log(x); } int sum(int a, int b) { return a + b; } }
On line 13, the function is now defined to expect two values of type int which will be stored in local variables a and b. The sum of these two values are returned on line 15.
The function call on line 9 must now include two integers as parameters.
The function is much more useful now because it can be used to add any two numbers.
The example was unnecessary as the plus operator accomplishes the same behavior. However, this provides a very simple example of how values could be passed to a function. Consider an example you might see in a game. You may call dealDamage() to deal damage to the player. However, with no parameters, you would not be able to specify how much damage should be dealt. It would likely be more useful to call dealDamage(4) to deal 4 damage (or any amount of damage specified) to the player.
Given how common randomness is in games, let's take a look at the Random.Range function.
public class CubeBehaviour : MonoBehaviour { void Update() { if(Input.GetKeyDown(KeyCode.Space)) { int x = Random.Range(3, 10); Debug.Log(x); } } }
First, notice the Update method is being used instead of the Start method so that the code will repeat continuously.
Each frame, if the space bar is pressed, lines 11 and 12 are executed.
The Range method of the Random object expects two parameters (a minimum and maximum value) and returns a randomly generated value that lies between those two values. The definition for the Range function must be public as we are allowed to use it from outside of the Random class where it is defined. If you run the code, a new integer value is printed to the console every time the space bar is pressed.
Save the code and play to see the values that are printed to the console when the space bar is pressed. Notice that all the values are integers.
Also, note that the value
10is never returned. The Range function returns values up to, but not including, the maximum value you specify.
Interestingly, this function has been overloaded, meaning there are multiple definitions for the same function. The function that is used depends on the parameters that are sent. Use the overloaded function that expects float values instead of int values.
public class CubeBehaviour : MonoBehaviour { void Update() { if(Input.GetKeyDown(KeyCode.Space)) { float x = Random.Range(3f,10f); Debug.Log(x); } } }
Now a different version of the Range function is being used, one that takes two float values as parameters and returns a float value that lies between them.
When you play the game now, pressing the space bar will cause random decimal values, instead of random integer values, to be printed to the console.
Overloading functions is very common. Consider how many definitions exist for the print statement. You can send it a string, and int value, a float value, and more. This is possible due to overloading!