The initial steps into any programming language are plagued with a fundamental issue – you can understand the words being typed out, but not the meaning behind them. Normally, this would be cause for a paradox, but programming is a special case.
C# is not its own language; it's written in English. The discrepancy between the words you use every day and the code in Visual Studio comes from missing context, which is something that has to be learned all over again. You know how to say and spell the words used in C#, but what you don't know is where, when, why, and, most importantly, how they make up the syntax of the language.
This chapter marks our departure from programming theory and the beginning of our journey into actual coding. We'll talk about accepted formatting, debugging techniques, and...
Writing proper C#
Lines of code function like sentences, meaning they need to have some sort of separating or ending character. Every line of C#, called a statement, MUST end with a semicolon to separate them for the code compiler to process.
However, there's a catch that you need to be aware of. Unlike the written word we're all familiar with, a C# statement doesn't technically have to be on a single line; whitespace and newlines are ignored by the code compiler. For example, a simple variable could be written like this:
public int firstName = "Harrison";
Alternatively, it could also be written as follows:
These two code snippets are both perfectly acceptable to Visual Studio, but the second option is highly discouraged in the software community as it makes code extremely hard to read. The idea is to write your programs as efficiently and clearly as possible.
Debugging your code
While we're working through practical examples, we'll need a way to print out information and feedback to the Console window in the Unity editor. The programmatic term for this is debugging, and both C# and Unity provide helper methods to make this process easier for developers. Whenever I ask you to debug or print something out, please use one of the following methods:
- For simple text or individual variables, use the standard Debug.Log() method. The text needs to be inside a set of parentheses, and variables can be used directly with no added characters; for example:
Debug.Log("Text goes here.");
- For more complex debugging, use Debug.LogFormat(). This will let you place variables inside the printed text by using placeholders. These are marked with a pair of curly brackets, each containing an index. An index is a regular number, starting at 0 and increasing sequentially by 1.
In the following example...
In the previous chapter, we saw how variables are written and touched on the high-level functionality that they provide. However, we're still missing the syntax that makes all of that possible. Variables don't just appear at the top of a C# script; they have to be declared according to certain rules and requirements. At its most basic level, a variable statement needs to satisfy the following requirements:
- The type of data the variable will store needs to be specified.
- The variable has to have a unique name.
- If there is an assigned value, it must match the specified type.
- The variable declaration needs to end with a semicolon.
The result of adhering to these rules is the following syntax:
dataType uniqueName = value;
Type and value declarations
The most common scenario for creating variables is one that has all of the required information available when the declaration is made. For instance, if we knew a player's age, storing it would be as easy as doing the following:
int currentAge = 32;
Here, all of the basic requirements have been met:
- A data type is specified, which is int (short for integer).
- A unique name is used, which is currentAge.
- 32 is an integer, which matches the specified data type.
- The statement ends with a semicolon.
However, there will be scenarios where you'll want to declare a variable without knowing its value right away. We'll talk about this topic in the following section.
Consider another scenario: you know the type of data you want a variable to store, as well as its name, but not its value. The value will be computed and assigned somewhere else, but you still need to declare the variable at the top of the script.
This situation is perfect for a type-only declaration:
Only the type (int) and unique name (currentAge) are defined, but the statement is still valid because we've followed the rules. With no assigned value, default values will be assigned according to the variable's type. In this case, currentAge will be set to 0, which matches the int type. Whenever the actual value is available, it can easily be set in a separate statement by referencing the variable name and assigning it a value:
currentAge = 32;
At this point...
Using access modifiers
Now that the basic syntax is no longer a mystery, let's get into the finer details of variable statements. Since we read code from left to right, it makes sense to begin our variable deep-dive with the keyword that traditionally comes first – an access modifier.
Take a quick look back at the variables we used in the preceding chapter in LearningCurve and you'll see they had an extra keyword at the front of their statements: public. This is the variable's access modifier. Think of it as a security setting, determining who and what can access the variable's information.
If you include a modifier, the updated syntax recipe we put together at the beginning of this chapter will look like this:
accessModifier dataType uniqueName = value;
While explicit access modifiers aren&apos...
Choosing a security level
There are four main access modifiers available in C#, but the two you'll be working with most often as a beginner are the following:
- Public: This is available to any script without restriction.
- Private: This is only available in the class they're created in (which is called the containing class). Any variable without an access modifier defaults to private.
The two advanced modifiers have the following characteristics:
- Protected: Accessible from their containing class or types derived from it
- Internal: Only available in the current assembly
There are specific use cases for each of these modifiers, but until we get to the advanced chapters, don't worry about protected and internal.
Let's try out some access modifiers of...
Time for action – making a variable private
Just like information in real life, some data needs to be protected or shared with specific people. If there's no need for a variable to be changed in the Inspector window or accessed from other scripts, it's a good candidate for a private access modifier.
Perform the following steps to update LearningCurve:
- Change the access modifier in front of currentAge from public to private and save the file.
- Go back into Unity, select the Main Camera, and take a look at what changed in the LearningCurve section.
Since currentAge is now private, it's no longer visible in the Inspector window and can only be accessed within the LearningCurve script. If we click Play, the script will still work exactly as it did before:
This is a good start on our journey into variables, but we still need to know more about what kinds of data they can store. This is where data types...
Working with types
Assigning a specific type to a variable is an important choice, one that trickles down into every interaction a variable has over its entire lifespan. Since C# is what's called a strongly-typed or type-safe language, every variable has to have a data type without exception. This means that there are specific rules when it comes to performing operations with certain types, and regulations when converting a given variable type into another.
Common built-in types
All data types in C# trickle down (or derive, in programmatic terms) from a common ancestor: System.Object. This hierarchy, called the Common Type System (CTS), means that different types have a lot of shared functionality. The following table lays out some of the most common data type options and the values they store:
In addition to specifying the kind of value a variable can store, types contain added information about themselves, including the following:
- Required storage space
- Minimum and maximum values
- Allowed operations
- Location in memory
- Accessible methods
- Base (derived) type
If this seems overwhelming, take a deep breath. Working with all of the types C# offers is a perfect example of using documentation over memorization. Pretty soon, using even the most complex custom types will feel like second nature.
Time for action – playing with different types
Go ahead and open up LearningCurve and add a new variable for each type in the preceding chart from the Common built-in types section. The names and values you use are up to you; just make sure they're marked as public so we can see them in the Inspector window. If you need inspiration, take a look at my code, which is shown in the following screenshot:
All our different variable types are now visible. Take note of the bool variable that Unity displays as a checkbox (true is checked and false is unchecked):
Before we move on to conversions, we need to touch on a common and powerful application of the string data type; namely, the creation of strings that have variables interspersed at will.
Time for action – creating interpolated strings
While number types behave as you'd expect from grade school math, strings are a different story. It's possible to insert variables and literal values directly into text by starting with a $ character, which is called string interpolation. The interpolated values are added inside curly brackets, just like using the LogFormat() method. Let's create a simple interpolated string of our own inside LearningCurve to see this in action:
Print out the interpolated string inside the Start() method directly after ComputeAge() is called:
Thanks to the curly brackets, the value of firstName is treated as a value and is printed out inside the interpolated string:
It's also possible to create interpolated strings using the + operator, which we'll get to right after we talk about type conversions.
We've already seen that variables can only hold values of their declared types, but there will be situations where you'll need to combine variables of different types. In programming terminology, these are called conversions, and they come in two main flavors:
- Implicit conversions take place automatically, usually when a smaller value will fit into another variable type without any rounding. For example, any integer can be implicitly converted into a double or float without additional code:
float implicitConversion = 3;
- Explicit conversions are needed when there is a risk of losing a variable's information during the conversion. For example, if we wanted to convert a double into an int, we would have to explicitly cast (convert) it by adding the destination type in parentheses before the value we want to convert. This tells the compiler that we are aware that data (or precision) might be lost.
In this explicit conversion, 3...
Luckily, C# can infer a variable's type from its assigned value. For example, the var keyword can let the program know that the type of the data, currentAge, needs to be determined by its value of 32, which is an integer:
var currentAge = 32;
Before we wrap up our discussion on data types and conversion, we do need to briefly touch on the idea of creating custom types, which we'll do next.
When we're talking about data types, it's important to understand early on that numbers and words (referred to as literal values) are not the only kinds of values a variable can store. For instance, a class, struct, or enumeration can be stored as variables. We will introduce these topics in Chapter 5, Working with Classes and OOP, and explore them in greater detail in Chapter 10, Revisiting Types, Methods, and Classes.
Types are complicated, and the only way to get comfortable with them is by using them. However, here are some important things to keep in mind:
- All variables need to have a specified type (be it explicit or inferred).
- Variables can only hold values of their assigned type (string can't be assigned to int).
- Each type has a set of operations that it can and can't apply (bool can't be subtracted from another value).
- If a variable needs to be assigned or combined with a variable of a different type, a conversion needs to take place (either implicit or explicit).
- The C# compiler can infer a variable's type from its value using the var keyword, but should only be used when the type isn't known when it's created.
That's a lot of nitty-gritty detail we've just jammed into a few sections, but we're not done yet. We still need to understand how naming conventions work in C#, as well as where the variables live in our scripts.
Picking names for your variables might seem like an afterthought in light of everything we've learned about access modifiers and types, but it shouldn't be a trivial choice. Clear and consistent naming conventions in your code will not only make it more readable but will also ensure that other developers on your team understand your intentions without having to ask.
The first rule when it comes to naming a variable is that the name you give it should be meaningful; the second rule is that you use camel case. Let's take a common example from games and declare a variable to store a player's health:
public int health = 100;
If you find yourself declaring a variable like this, alarm bells should be going off in your head. Whose health? Is it storing the maximum or minimum value? What other code will be affected when this value changes? These are all questions that should be easily answered by a meaningful variable name; you don't want to find yourself confused by your code in a week or a month.
With that said, let's try to make this a bit better using a camel case name:
public int maxHealth = 100;
Understanding variable scope
We're getting to the end of our dive into variables, but there's still one more important topic we need to cover: scope. Similar to access modifiers, which determine which outside classes can grab a variable's information, the variable scope is the term used to describe where a given variable exists and its access point within its containing class.
There are three main levels of variable scope in C#:
- Global scope refers to a variable that can be accessed by an entire program; in this case, a game. C# doesn't directly support global variables, but the concept is useful in certain cases, which we'll cover in Chapter 10, Revisiting Types, Methods, and Classes.
- Class or member scope refers to a variable that is accessible anywhere in its containing class.
- Local scope refers to a variable that is only accessible inside the specific block of code it's created in.
Take a look at the following screenshot. You don&apos...
Operator symbols in programming languages represent the arithmetic, assignment, relational, and logical functionality that types can perform. Arithmetic operators represent basic math functions, while assignment operators perform math and assignment functions together on a given value. Relational and logical operators evaluate conditions between multiple values, such as greater than, less than, and equal to.
At this point, it only makes sense to cover arithmetic and assignment operators, but we'll get to relational and logical functionality when it becomes relevant in the next chapter.
Arithmetic and assignments
You're already familiar with the arithmetic operator symbols from school:
- + for addition
- - for subtraction
- / for division
- * for multiplication
C# operators follow the conventional order of operations, that is, evaluating parentheses first, then exponents, then multiplication, then division, then addition, and finally subtraction (BEDMAS). For instance, the following equations will provide different results, even though they contain the same values and operators:
5 + 4 - 3 / 2 * 1 = 8
5 + (4 - 3) / 2 * 1 = 5
Assignment operators can be used as a shorthand replacement for any math operation by using any arithmetic and equals symbol together. For example, if we wanted to multiply a variable, both of the following options would produce the same result:
int currentAge = 32;
currentAge = currentAge * 2;
The second, alternative, way to do this is shown here...
Time for action – executing incorrect type operations
Let's do a little experiment: we'll try to multiply our string and float variables together, as we did earlier with our numbers:
If you look in the Console window, you'll see that we've got an error message letting us know that a string and a float can't be added. Whenever you see this type of error, go back and inspect your variable types for incompatibilities:
It's important that we clean up this example, as the compiler won't allow us to run our game at this point. Choose between a pair of backslashes (//) at the beginning of Debug.Log() on line 21, or delete it altogether.
That's as far as we need to go in terms of variables and types for the moment. Be sure to test yourself on this chapter's quiz before moving on!
In the previous chapter, we briefly touched on the role methods play in our programs; namely, that they store and execute instructions, just like variables store values. Now, we need to understand the syntax of method declarations and how they drive action and behavior in our classes.
As with variables, method declarations have their basic requirements, which are as follows:
- The type of data that will be returned by the method
- A unique name, starting with a capital letter
- A pair of parentheses following the method name
- A pair of curly brackets marking the method body (where instructions are stored)
Putting all of these rules together, we get a simple method blueprint:
Let's break down the default Start() method in LearningCurve as a practical example:
In the preceding output, we can see the following:
- The method starts with the void keyword, which is used as the method's return type if it doesn't return any data.
- The method has a unique name.
- The method has a pair of parentheses after its name to hold any potential parameters.
- The method body is defined by a set of curly brackets.
Modifiers and parameters
Methods can also have the same four access modifiers that are available to variables, as well as input parameters. Parameters are variable placeholders that can be passed into methods and accessed inside them. The number of input parameters you can use isn't limited, but each one needs to be separated by a comma, show its data type, and have a unique name.
If we apply these options, our updated blueprint will look like this:
accessModifier returnType UniqueName(parameterType parameterName)
To call a method (meaning to run or execute its instructions), we simply use its name, followed by a pair of parentheses, with or without parameters, and cap it off with a semicolon:...
Time for action – defining a simple method
One of the Time for action segments in the previous chapter had you blindly copy a method called AddNumbers into LearningCurve without you knowing what you were getting into. This time, let's purposefully create a method:
- Declare a public method with a void return type called GenerateCharacter().
- Add a simple Debug.Log() that prints out a character name from your favorite game or movie.
- Call GenerateCharacter() inside the Start() method and hit Play:
When the game starts up, Unity automatically calls Start(), which, in turn, calls our GenerateCharacter() method and prints it to the Console window.
The power of...
Like variables, methods need unique, meaningful names to distinguish them in code. Methods drive actions, so it's a good practice to name them with that in mind. For example, GenerateCharacter() sounds like a command, which reads well when you call it in a script, whereas a name such as Summary() is bland and doesn't paint a very clear picture of what the method will accomplish.
Methods always start with an uppercase letter, followed by capitalizing the first letter in any subsequent words. This is called PascalCase (a step-sibling of the CamelCase format we use with variables).
Methods are logic detours
We've seen that lines of code execute sequentially in the order they're written, but bringing methods into the picture introduces a unique situation. Calling a method tells the program to take a detour into the method instructions, run them one by one, and then resume sequential execution where the method was called.
Take a look at the following screenshot and see whether you can figure out in what order the debug logs will be printed out to the console:
These are the steps that occur:
- Choose a character prints out first because it's the first line of code.
- When GenerateCharacter() is called, the program jumps to line 23, prints out Character: Spike, then resumes execution at line 17.
- A fine choice prints out last, after all the lines in GenerateCharacter() have finished running:
Now, methods in themselves wouldn't be very useful beyond simple examples like these if we couldn't add parameter...
Chances are your methods aren't always going to be as simple as GenerateCharacter(). To pass in additional information, we'll need to define parameters that our method can accept and work with. Every method parameter is an instruction and needs to have two things:
- An explicit type
- A unique name
Does this sound familiar? Method parameters are essentially stripped-down variable declarations and perform the same function. Each parameter acts like a local variable, only accessible inside their specific method.
If parameters are the blueprint for the types of values a method can accept, then arguments are the values themselves. To break this down further, consider the following:
- The argument that's passed into a method needs to match the parameter type...
Time for action – adding method parameters
Let's update GenerateCharacter() so that it can take in two parameters:
- Add two method parameters: one for a character's name of the string type, and another for a character's level of the int type.
- Update Debug.Log() so that it uses these new parameters.
- Update the GenerateCharacter() method call in Start() with your arguments, which can be either literal values or declared variables:
Here, we defined two parameters, name (string) and level (int), and used them inside the GenerateCharacter() method, just like local variables. When we called the method inside Start(), we added argument values for each parameter with corresponding types. In the preceding screenshot, you can see that using the literal string value in quotations produced the same result as using characterLevel:
Going even further with methods, you might be wondering how we can pass values from inside the method and back out again...
Specifying return values
Aside from accepting parameters, methods can return values of any C# type. All of our previous examples have used the void type, which doesn't return anything, but being able to write instructions and pass back computed results is where methods shine.
According to our blueprints, method return types are specified after the access modifier. In addition to the type, the method needs to contain the return keyword, followed by the return value. A return value can be a variable, a literal value, or even an expression, as long as it matches the declared return type.
Let's add a return type to GenerateCharacter() and learn how to capture it in a variable.
Time for action – adding a return type
Let's update the GenerateCharacter method so that it returns an integer:
- Change the return type in the method declaration from void to int.
- Set the return value to level + 5 using the return keyword:
GenerateCharacter() will now return an integer. This is computed by adding 5 to the level argument. We haven't specified how, or if, we want to use this return value, which means that right now, the script won't do anything new.
Now, the question becomes: how do we capture and use the newly added return value? Well, we'll discuss that very topic in the following section.
Using return values
When it comes to using return values, there are two approaches available:
- Create a local variable to capture (store) the returned value.
- Use the calling method itself as a stand-in for the returned value, using it just like a variable. The calling method is the actual line of code that fires the instructions, which in our example would be GenerateCharacter("Spike", characterLevel). You can even pass a calling method into another method as an argument if need be.
Let's give this a try in our code by capturing and debugging the return value that GenerateCharacter() returns.
Time for action – capturing return values
We're going to use both ways of capturing and using return variables with two simple debug logs:
- Create a new local variable of the int type, called nextSkillLevel, and assign it to the return value of the GenerateCharacter() method call we already have in place.
- Add two debug logs, with the first printing out nextSkillLevel and the second printing out a new calling method with argument values of your choice.
- Comment out the debug log inside GenerateCharacter() with two backslashes (//) to make the console output less cluttered.
- Save the file and hit Play in Unity:
To the compiler, nextSkillLevel and the GenerateCharacter() method caller represent the same information, namely an integer, which is why both logs show the number 37:
That was a lot to take in, especially given the exponential possibilities of methods with parameters and return values. However, we'll ease off the throttle here for a minute...
Hero's trial – methods as arguments
If you're feeling brave, why not try creating a new method that takes in an int parameter and simply prints it out to the console? No return type necessary. When you've got that, call the method in Start, pass in a GenerateCharacter method call as its argument, and take a look at the output.
Dissecting common Unity methods
We're now at a point where we can realistically discuss the most common default methods that come with any new Unity C# script: Start() and Update(). Unlike the methods we define ourselves, methods belonging to the MonoBehaviour class are called automatically by the Unity engine according to their respective rules. In most cases, it's important to have at least one MonoBehaviour method in a script to kick off your code.
Just like stories, it's always a good idea to start at the beginning. So, naturally, we should take a look at every Unity script's first default method – Start().
The Start method
Unity calls this method on the first frame where a script is enabled. Since MonoBehaviour scripts are almost always attached to GameObjects in a scene, their attached scripts are enabled at the same time they are loaded when you hit Play. In our project, LearningCurve is attached to the Main Camera GameObject, which means that its Start() method runs when the Main Camera is loaded into the scene. Start() is primarily used to set up variables or perform logic that needs to happen before Update() runs for the first time.
Other than Start(), there's one other major Unity method that you'll run into by default: Update(). Let&apos...
The Update method
If you spend enough time looking at the sample code in the Unity Scripting Reference, you'll notice that a vast majority of the code is executed using the Update() method. As your game runs, the Scene window is displayed many times per second, which is called the frame rate or frames per second (FPS). After each frame is displayed, the Update() method is called by Unity, making it one of the most executed methods in your game. This makes it ideal for detecting mouse and keyboard input or running gameplay logic.
If you're curious about the FPS rating on your machine, hit Play in Unity and click the Stats tab in the upper-right corner of the Game view:
You'll be using the Start() and Update() methods in the lion's share of your beginning C# scripts, so get acquainted with them. That being said, you've reached the end of this chapter with a pocketful of the most fundamental building blocks programming has to offer...
This chapter has been a fast descent from the basic theory of programming and its building blocks into the strata of real code and C# syntax. We've seen good and bad forms of code formatting, learned how to debug information in the Unity console, and created our first variables. C# types, access modifiers, and variable scope weren't far behind, as we worked with member variables in the Inspector window and started venturing into the realm of methods and actions.
Methods helped us to understand written instructions in code, but more importantly, how to properly harness their power into useful behaviors. Input parameters, return types, and method signatures are all important topics, but the real gift they offer is the potential for new kinds of actions to be performed. You're now armed with the two fundamental building blocks of programming; almost everything you'll do from now on will be an extension or application of these two concepts.
In the next...
Pop quiz – variables and methods
- What is the proper way to write a variable name in C#?
- How do you make a variable appear in Unity's Inspector window?
- What are the four access modifiers available in C#?
- When are explicit conversions needed between types?
- What are the minimum requirements for defining a method?
- What is the purpose of the parentheses at the end of the method name?
- What does a return type of void mean in a method definition?
- How often is the Update() method called by Unity?