text:
Please confirm that you want to add Learn to Code with John M. Quick (C# Unity Game Development) to your Wishlist.
Have you ever dreamed of making your own games? Your quest to become a heroic coder begins today.
Governments around the world are calling for citizens to learn to code. Indeed, computer programming is rapidly becoming a key skill for people of all kinds. Meanwhile, game development is one of the most rewarding crafts of modern times. Not only is creating games a wonderful lifelong hobby, but employment opportunities exist across many levels, from small independent developers to massive game studios.
If you want to learn to code, then making games is an excellent place to start. John M. Quick presents a novel approach to coding for the complete beginner. You will explore game development through a practical, hands-on method and begin to think of code as a problem-solving technique. You will be challenged to code real game components in and create a game that you can be proud of. After completing this course, you will have a solid foundation in both computer programming (C# language) and game development fundamentals (Unity game engine).
Have you ever dreamed of making your own games? Your quest to become a heroic coder begins today.
Governments around the world are calling for citizens to learn to code. Indeed, computer programming is rapidly becoming a key skill for people of all kinds. Meanwhile, game development is one of the most rewarding crafts of modern times. Not only is creating games a wonderful lifelong hobby, but employment opportunities exist across many levels, from small independent developers to massive game studios.
If you want to learn to code, then making games is an excellent place to start. My name is John Quick. I will guide you through your coding journey. You will experience a novel approach to coding for the complete beginner. You will explore game development through a practical, hands-on method and begin to think of code as a problem-solving technique. You will be challenged to code real game components as you create a game that you can be proud of. After completing this course, you will have a solid foundation in both computer programming and game development fundamentals.
Hi. I'm John Quick. I will be your game master for this course. I have created independent games for the web, desktop, and mobile devices. In addition, I've published books that help people of all kinds learn to code and develop games. Furthermore, I've helped many university students learn to code and develop games, which has prepared them for careers in the game industry and related fields. My goal is to help you learn to code and create outstanding video games. Let's begin this journey together.
As your game master, I'm here to guide you through the learning process. In a typical course, you would watch the instructor do things on the computer, such as click here, click there, then follow along and do it yourself. That is not the most effective way to learn, especially if you want to be able to think for yourself and solve the problems of the future.
Our approach will be different. Ironically enough, I call it the Learn to Code method. You will engage in a unique learning experience that requires you to create solutions to coding challenges while developing your own game. By rising to the challenge and creating personalized solutions, you will prepare yourself to succeed today and into the future. Trends and technologies change rapidly. Unity and C# are great today, but we don't know what will become of them in the future. However, if you're prepared, the uncertain future doesn't matter. You're going to build a strong coding foundation. You're going to learn how to think logically and use code to create meaningful solutions. These abilities will serve you throughout your career, no matter what the future holds.
Here's a look at the game you will create in this course. It's a 2D platformer called Lana on Ice. As you can see, we have our hero, baby Lana. She's jumping around icy platforms. Along the way, she can collect sippy cups and make sure to avoid those slippery penguins. You will put this game together piece by piece by designing your own solutions to coding challenges. Along the way, you will learn the fundamentals of coding and game development.
In this course, you will:
Solve computer coding problems using logical PseudoMapping techniques
Code in the C# programming language using variables, conditional statements, loops, collections, functions, and more
Build a game in the Unity engine that includes player controls, obstacles, collisions, levels, state management, scores and more
To prepare yourself for this course, make sure to:
Install the Unity game engine. You can download it from http://unity3d.com. Choose a code editor. You can use any code editor you like, such as Unity's built-in MonoDevelop, Visual Studio, Notepad++, or any text editor.
This course is made up of a series of challenges. The challenges require you to implement game components in your own special way. Hints are provided along the way to guide you towards a solution. After you have successfully created your own solution, you are provided with a complete example solution. That helps you learn even more by comparing your work to another successful implementation.
All of the necessary files for each lesson are provided to you. In the Software folder, you will find Challenge and Solution projects. Since this course is primarily about learning to code and not how to use Unity, some things have been provided for you. Start each challenge by making a copy of the Challenge project. From there, you can proceed to work on implementing your solution. Meanwhile, the Solution project represents a complete, working implementation. Once you complete your own solution, you can review the example solution to see how it was done. Furthermore, you can play through the example solution to see how your game should function once the challenge is complete. Don't peek ahead at the code though, since you will learn more by implementing your own solution first. Meanwhile, the Video folder contains all of the lesson's videos. Be sure to watch the videos in order as you work through the lesson. Take time to work on your implementation between each video. The better you apply what you learn in the videos to your own implementation before proceeding to the example solution, the more you will learn. Lastly, in the Resources folder, you will find supplementary materials, such as articles, quizzes, and example scripts. Let's get to work.
Your first challenge involves getting a character to move within the game world.
Navigate to the Lesson 1 > Software folder. There you will find the Challenge folder. Inside the Challenge folder, click on Assets > Scenes. Here you will find the Game.unity file. Double-click on the file to open your project in Unity.
Once the project opens, have a look around. In the scene window, you will see that we have a game character, some platforms, and other objects. This character is the star of our game. Her name is Lana. Notice above her head that there are some instructions telling us that Lana can move left or right using the A and D keys.
Click on the play button found at the top-center of the Unity interface. Notice that the game begins to run in the Game window below. Try moving Lana using the A and D keys. She's stuck! That's because we have not yet written the code necessary to make Lana move. Press the play button again to stop running the game.
Take a look at your Hierarchy window. Inside is the Player object. This object represents Lana. Click on the Player object in the Hierarchy and turn your attention to the Inspector window. Towards the bottom, you will see that a script called UserMove is attached. You will need to modify the code in this script to help Lana move.
Find the Project window towards the bottom of the Unity interface. This is where all the folders, files, and assets for your game are stored. Open the Scripts folder and locate the UserMove script.
Double-click on the UserMove script to open it in your code editor. There is quite a lot of code in this script, but for this challenge, you only need to focus one a specific place. Scroll down until you find the MoveLeft() and MoveRight() functions. When you press the A key, the MoveLeft() function is triggered. When you press the D key, the MoveRight() function is triggered.
Find the challenge text within each function. As indicated, your challenge is to update the dir, or direction, variable. Right now, the value of the direction variable is 0. Therefore, you need to change the value of the direction variable to ensure that Lana moves in the appropriate direction when a key is pressed.
Think about what potential values might make Lana move left or right in the game world. For additional assistance with solving this problem, proceed to the hints.
Start by visualizing the problem.
Use pencil and paper to:
1. Draw a rectangle to represent your screen.
2. Draw a square in the center to represent your character.
3. Draw a horizontal line to represent your x axis.
4. Draw a vertical line to represent your y axis.
You have just created a two-dimensional (2D) coordinate plane and placed your character at the position (0, 0).
Reconsider the challenge at hand and ask yourself these questions:
1. How will I know if the character is moving left or right?
2. How will the x and y coordinate values change as the character moves?
See if you can come up with a solution based on this information.
We are working with a two-dimensional, or 2D, game world. Therefore, the locations of all objects are represented by x and y coordinates.
Follow these steps to demonstrate how positioning works in Unity:
1. Open your Unity project.
2. Click on the Player object inside the Hierarchy window.
3. Look to the Inspector window to find the Transform component.
4. Inside the Transform component, you will see the Position variable, which stores the object's x and y position.
5. Now, look to your Scene window and find the Player object.
6. Click and drag to move the Player object around your scene.
7. Notice that x and y position values change as the object moves.
In Unity, every object has a Transform component. Inside the Transform component is a Vector3 variable that stores the object's position.
Reconsider the challenge at hand and ask yourself these questions:
1. How do the x and y coordinate values change as the object moves around the screen? 2. What happens to the object's x value when it moves left or right?
It is important to have a clear vision for what you want to code. PseudoMapping involves two techniques that you can use to solve coding problems effectively.
Pseudocode is a technique that involves writing a logical solution to a coding problem in a human-readable format. For example, consider this pseudocode for the problem "the character moves right when the player presses the D key." Notice the use of key words, such as IF, THEN, IS, and HAS. Also note that this example is quite similar to human language. Alternatively, someone else might write pseudocode for the same solution that appears closer to computer language. There are no strict formatting requirements for pseudocode. The same logic can be represented in a variety of ways. What matters is that you can use pseudocode effectively to organize your logic.
Process Mapping is a technique that involves using a visual diagram to demonstrate how a system functions. Consider this process map for the problem "the character moves right when the player presses the D key." Notice the use of standard symbols, such as rectangles to represent states, diamonds to represent decision points, and arrows to represent the flow of information. As with pseudocode, process mapping styles vary, so use whatever format helps you to organize your thoughts.
While pseudocode focuses on code-like instructions, process mapping demonstrates the overall state and flow of information. Both techniques help you identify the logic behind your solution prior to coding it. When you know what you want to do before you begin coding, your work is more efficient and effective. I encourage you to use PseudoMapping to organize your logic for each and every challenge you face in this course.
At this point, you should have come up with a working solution of your own. Let's look at the example solution for comparison purposes. *Remember that the example solutions in this course do not present the correct or best way to solve challenges. Instead, they each demonstrate one successful solution that you can compare to your own solution to learn even more.
Navigate to the Lesson 1 > Software folder. There you will find the Solution folder. Inside, click on Assets > Scenes. Double-click on the Game.unity file to open your project.
Once the project opens, press the play button. Use the A and D keys to move Lana around the screen. At last, she moves! How did we make this happen?
In the Project window, double-click on your UserMove script to open it in your code editor. Scroll down to the MoveLeft() function. Notice that the value of the direction variable is negative 1. Similarly, look to the MoveRight() function and note that the direction variable is positive 1.
Recall that we conceptualized the game world as a 2D coordinate plane. In our game world, Lana is moving left and right along the x axis. When she moves left, her x position value decreases. Thus, leftward movement is represented by a negative value. In contrast, when Lana moves right, her x position value increases. Thus, rightward movement is represented by a positive value.
That's all. Congratulations on succeeding in your first coding challenge.
Here's a bonus challenge for you to consider on your own. Imagine that Lana were also moving up and down in the game world. What values along what axis would you use to represent Lana's movement in the up and down directions?
You are beginning to explore the concept of variables. A variable is one way that a computer stores information. For instance, information such as a name, date, or number could be stored in a variable. Computers store different kinds of information in different ways. When creating a variable, a data type tells the computer what kind of information will be stored in that variable.
Navigate to the Lesson 02 > Software > Challenge > Stats folder and open the LanaStats script in your code editor. Notice that several variables are present. However, their data types are missing.
In this challenge, you will choose the appropriate data type for each variable. For each variable, delete the comment text and insert your chosen data type. We will focus on the most common data types, including Boolean, integer, floating point, and string. Examine the variables now and think about what kind of information best represents each one. Ask yourself whether they should contain true or false values, whole numbers, decimal numbers, or text. Once you've identified a theoretical type for each variable, proceed to learn more about coding data types in the hints.
It's time to consider how we can code specific data types. We will focus on the most common data types. These include the Boolean or bool, integer or int, floating point or float, and the string, also known as string. Let's examine each data type.
The Boolean data type identifies values that are either true or false. You apply this data type to a variable using the bool keyword. For example, we can make use a Boolean to store whether a character can jump. Or, we can indicate whether or not our video has ended. Notice that all Boolean variables store information that can be considered true or false.
The integer data type identifies values that are whole numbers. For example, 0, 1, -45, 123 can all be represented as integers. In fact, any whole number from about negative 2 billion to positive 2 billion can be represented by an integer. However, integers cannot use fractional or decimal values. Therefore, numbers like the price of a late-night infomercial miracle product or 0.25 are not integers. You apply this data type to a variable using the int keyword. For instance, we might want to keep track of the game's current level.
Consider this. The Boolean data type gives us only two choices: true or false. Meanwhile, the integer allows us to use a huge range of whole numbers. Therefore, we might use a Boolean when only two choices are needed, but an integer when we need to represent more possible states. As the complexity of the information that we are representing increases, we choose a data type that suits our needs.
The floating point data type can represent almost any value up to 7 digits long. This equates to an unspeakably large range of values. For example, 10.0 and 9.876543 (2, 1, blastoff) can be represented as floating points. You apply this data type to a variable using the float keyword. For instance, we might store the x position of our character in a float. Notice again that our data types changes as our requirements change. Floating points allow a larger range of more complex information to be represented than integers.
Interestingly, the string data type stores information that represents human language, such as letters, words, and numbers. A string is composed of individual units, called characters. For example, the letter "a" or number "0" can be stored as a character. By putting together a series of characters, we can form a string, such as "I have coded 16 lines!" You apply this data type to a variable using the string keyword. For instance, we can use a string to store what someone said. While the string is clearly a flexible data type, we will only apply it in certain circumstances.
Let's review our most common data types. Recall that the Boolean data type represents things that can be true or false. An integer is a whole number. A floating point is a number that can have up to 7 digits, including decimals. A string is a collection of characters that can flexibly represent a variety of information, including human language.
Note that many other data types exist. You will use bool, int, float, and string extensively throughout your coding career. In the future, you may learn about even more data types and run into specialized situations that require them. However, we will focus on using the most common data types in this course.
Recall that a Boolean variable can only have two values, either true or false. Similarly, at the most fundamental level, computers only understand two values. This is known as a base 2 or binary system. All information stored in a computer is ultimatley represented by a value of 0 or 1. Each value is called a binary digit, or a bit. If we put 8 bits together, we have a byte, which can represent a simple value such as the letter "z" or the number 64. If we put many bytes together, we can begin to represent more complex information such as words. "Wow, this is cool!" you said to yourself -- we could represent that using 18 bytes.
Imagine yourself being inside a massive warehouse. The walls are lined with billions of tiny switches. Every time you take the slightest action, such as breathing, taking a step, or saying a word, many of the switches flip to reveal a new unique pattern. Using these switches, any state of the world can be uniquely represented for an instant. Then, something else happens that updates the state once again. This is similar to how a computer's memory works. Every time you load a webpage, save a file, or press a button, you are rearranging bits to represent the present state of the computer's memory.
Although we can do so many amazing things, a computer truly only understands 0 and 1. We could write code directly in the 0 and 1 values that computers understand. However, this would be nearly impossible for humans, since we have trouble comprehending information in this format. Hence, computer code is a compromise that allows humans and computers to work together. Code bridges the gap between human language and computer language. Computers cannot do anything without humans. As a coder, you must tell a computer what to do through code. Therefore, your challenge is to become better at communicating with a computer so you can get it to do what you want it to do. When you are proficient at coding, you are able to transform a computer into a powerful tool for achieving your goals.
Since computers and humans understand the world differently, part of being an effective coder involves selecting appropriate data types. Generally speaking, computers are more efficient at processing data that represents 0s and 1s than data that represents human language. Hence, Boolean data is quite efficient to process, although it is less like human language.
Meanwhile, string data is less efficient to process, but easier for humans to understand. Your data types represent the compromise between computer and human language. A good strategy is to use the simplest data type necessary to represent your information. For instance, use a Boolean if you can. If you need more than 2 values, use an integer. If you require decimal values, then a floating point is acceptable. If you need to represent text or other data that cannot be represented in the preceding types, use a string.
We won't obsess over optimizing the efficiency of code in this course. The kinds of games you are learning to make can function well if you make generally wise choices about your code. However, be aware that you might go onto code hardware-intensive applications in the future that require more detailed optimization strategies.
Take a look at your LanaStats script. Here are some example data types for the variables. Review these data types and compare them to your own. Note that you may have used different data types. Any data type you selected is fine, so long as you have a logical justification for your choice.
You may have noticed that our game world contains a few objects besides Lana. For instance, our game world also has a sippy cup and a penguin.
Navigate to the Challenge > Stats folder. Open the CupStats and PenguinStats scripts in your code editor. Notice that these scripts are empty.
Your challenge is to design 8-10 variables for each of these objects. This time around, you are going to fully define the variables yourself. Similar to your LanaStats script, your CupStats and PenguinStats scripts should contain variables that describe the properties of these objects.
In your LanaStats script, you may have noticed that each variable started with public or private. These keywords identify the access level of the variable. An access level determines the extent to which a variable can be referenced and modified by external code. For example, suppose that script A wants to ask script B for information. If script B's variable is private, script A will not be able to access it. However, if script B's variable is public, script A will be able to access it.
Essentially, a private variable can only be accessed within its own script. Meanwhile, a public variable can be accessed by other scripts. For our purposes, think of private as the default. Most of the time, variables only need to be accessed within their own script. However, you will use public variables at times to share information across scripts. Note that more complex access levels exist, but are not necessary for our purposes. We will focus on public and private access levels and later encounter situations where both are needed.
For now, think about whether the variables you are creating contain information that should be publicly known or privately held. For instance, someone's name may be public information, whereas their bank account number would not.
The choice of a variable's name is an important one. Not only are there specific rules that you must follow, but you also want to ensure that your names clearly identify the purpose of your variables.
Here are the rules:
1. You must use alphanumeric values, that's the letters A-Z and numbers 0-9.
2. However, note that a number may not be the first character in a variable name.
3. Underscores may be used, but not other special characters.
4. Beyond that, make sure your variable names clearly represent the information contained within them, as well as their purpose.
Here are some different naming techniques. Camel Case starts with a lowercase letter. Any new word in the name uses an uppercase letter. Pascal Case starts with an uppercase letter. Any new word in the name uses an uppercase letter. Some people append a letter to the start of each variable to indicate its type. That's b for Boolean, i for integer, f for float, or s for string. Similarly, you might use signal prefix words that help people understand the data type and purpose of your variables. Another common style is to prefix private variables with an underscore, while public variables have no underscore, to identify their access levels.
Feel free to apply these guidelines, since they represent common coding practices. At the same time, note that everyone has a personal coding style which may vary to some degree from the common practices. Ultimately, you want your code to be easy to understand for yourself, as well as others.
Try reading these sets of variables. One reflects good style and one reflects poor style. Take note of which is easier and faster to interpret.
You may have noticed descriptive text that has been placed in various portions of our scripts. These are called comments. Their purpose is to further describe our code using human language. Since human and computer language are not the same, and humans understand their own language better than computer code, we use comments to help describe what we are doing with our code.
Comments can be written on a single line using a double forward slash. Anything following the slashes is ignored by the computer, which allows us to explain whatever we need to. Comments can also be written across multiple lines. To start a comment block, use a forward slash and asterisk. To close the comment block, use an asterisk and forward slash. Anything written between these opening and closing symbols will be ignored by the computer. Hence, we can use this type of comment to explain complex operations that require more than a single line of text.
Although comments do not change the function of our code, writing comments is one of the most important skills of an excellent coder. I suggest that you being using them effectively now and continue throughout your life as a coder.
Basically, all of your code should be commented. For example, above every variable you declare, you might add a comment that explains the purpose of the variable. As you can see throughout the scripts we use in this course, I have used comments extensively to describe specific code.
You should do the same. Not only do comments help others interpret your code, but they actually help you remember your own code! You will often revisit code to revise it or to reuse it at different times. Comments really help you get the most out of your code by making it memorable and reusable.
Take a look at these sets of variables. One has comments and one does not. Think about which set is easier to understand.
At last, it is time to declare variables in full. The general format for declaring a variable follows:
1. Start with an access level, such as public or private.
2. Next, include a data type, such as bool, int, float, or string.
3. Then, name your variable according to guidelines, such as camel case or Pascal case.
4. Last, finish with a semicolon.
In fact, all lines of code you write end with a semicolon, so you will become very familiar with that symbol. The semicolon is like a period at the end of a sentence. It completes a single instruction to the computer.
In addition, it is recommended that you add a meaningful comment above each variable that describes its purpose. By now, you should know everything necessary to complete this challenge. Return to your scripts and define variables for the objects in our game.
It was up to you to define about 10 variables for the sippy cup and for the penguin objects in our game. Each of your variables should have an access level, data type, and name, followed by a semicolon.
If you didn't include comments describing each variable, make sure to write them now. As long as your variables follow these requirements and describe meaningful characteristics of these objects, they are acceptable.
For comparison, take a look at the provided CupStats and PenguinStats scripts. Each script contains variables that represent the effective use of access levels, data types, naming, and comments.
You have defined variables for several game objects, including Lana, the sippy cup, and the penguin. In this challenge, you'll give each variable a reasonable starting value. This process is called initialization.
To begin, find the Start() function inside each script. It is currently empty. What you need to do inside the Start() function is to provide a starting value for each variable in the script. In addition, add a comment for each variable that explains the value you selected.
When you complete this challenge, you will have scripts that contain variables that have been declared and initialized. As always, hints are available to assist.
To initialize a variable is to give it a starting value. Before a variable is used anywhere else in your code, make sure that you initialize it. Depending on the language, should you use a variable that hasn't been initialized, it will either contain a default value chosen for you, random information, or no information. Needless to say, this can cause a lot of trouble in the form of errors and bugs in your code. You always want to be in total control of your code. Therefore, you must initialize your variables with relevant values.
To initialize a variable:
1. First, type the name of the variable.
2. Second, add an equal signal.
3. Third, type a value that matches the data type of the variable.
4. Fourth, end the line with a semicolon.
Remember to describe your initial value with a comment as well. Here are a few initialization examples.
Start() is a special function in Unity. It is used to control the time at which specific code is executed. Notably, the Start() function is executed only one time, just after Unity enables your script, but before any other functions.
Think about our variables. We need to initialize them before they are used in our code. Furthermore, we only need to initialize them a single time. Thus, the Start() function is an excellent place for us to initialize our variables. Hence, you will want to initialize your variables within the Start() function of each script. A Start() function has already been provided for you, so place your code inside.
At this point, your scripts should have fully initialized values inside their Start() functions. Compare your values with the provided example scripts. Make sure that the initial values you selected are logical and consistent with the data types of the variables
In this challenge, you will explore the usage of constants both as a contrast to variables and as a method for improving the quality of your code. Navigate to the Challenge > Software > Constants folder. Inside, browse to Assets > Scenes. Double-click on the Game file to open your project. Run your game to verify that it works as expected.
In the project window, notice that the Constants script is present. Double-click on the Constants script to open it in your code editor. Examine the content of this script. You should find that it is very similar to the variables that you worked with previously. In this challenge, you will briefly review how to apply constants in your code. Proceed to the hints.
Let's compare variables and constants. The value of variables can change over time. They can be assigned one value only to be assigned another value later. This can happen many times during the exciting life of a variable. On the other hand, constants never change. Once they are declared with an initial value, they can never be changed. We use variables abundantly, especially as our code is updated moment by moment throughout the game. We use constants sparingly, often to reference a few values that will never change, but may appear in at various points in our code.
Constants are declared in a format very similar to variables. Constants almost always have a public access level, since we want to be able to refer to them from a variety of other scripts. One major difference is that constants make use of the const keyword. This keyword is placed after the access level. The data type and naming requirements are the same as variables. Last, since a constant can only have one value, we typically assign it at declaration. This is accomplished the same way as initializing a variable. Just add an equal sign, the value, and a semicolon.
Take a look at the examples provided in the Constants script. We have stored some enduring values that will be reused in several calculations throughout our game. These include the screen width, screen height, tile size, and pixels to units conversion ratio, all of which will be detailed at the appropriate time.
Return to your project. Inside the Project window, open the UserMove script in your code editor. Look for the mysterious value of 100, which can be found in a few places throughout the script. This is often referred to as a magic number or a hard-coded value. Generally, we want to avoid writing code like this. Among other reasons, it is difficult to interpret what this number means, you definitely won't remember what the number means when you look back at your code in the future, and this number may make it difficult for you to reuse your otherwise suitable code in future projects.
Constants are one way to alleviate these issues. Instead of directly typing a hard-coded value into our script, we can reference our stored constant. In this case, we can replace the number 100 with a reference to the pixels to units variable inside our Constants script. To do so, delete the number 100 and type constants dot pixels to units. Do this for all instances of the number 100 in the UserMove script.
Afterwards, run your game and notice that it works the same way as before. All we have done is replaced a hard-coded value of 100 with a reference to a constant value stored in our Constants script. This makes our code easier to understand, since the constant value has a meaningful name and description in our code. Furthermore, it makes our code easier to reuse in the future, since all we have to do is update the pixels to units variable one time in our Constants script to change our calculations throughout all scripts that make reference to it.
The final version of your UserMove script should have all hard-coded values of 100 removed. Those hard-coded values should be replaced with references to the pixels to units value from the Constants script.
Run your game to observe that it operates as normal.
You now have explored the use of both variables and constants to store important information in your game. Let's proceed to learn more about coding and game development.
Open the challenge project. Run your game and move Lana around the screen. Notice that she can move off of the screen. Needless to say, it is undesirable for our game character to leave the player's view.
In this challenge, you are going to fix this situation. Instead of letting Lana move off screen, you are going to check for boundary collisions. If the player tries to move Lana past the edges of the screen, we will stop Lana's movement so she remains visible at all times.
Now is a good time to put your PseudoMapping skills to the test. Create pseudocode or a process map that captures the logic behind your solution. How will you know when Lana moves past the edge of the screen? How will you prevent her from leaving the screen?
After you have thought through the problem, open the UserMove script. Find the CheckBounds() function that has been provided for you. As you can see, you have been provided with Lana's x position and the width of the screen. You also have an incomplete conditional statement. Your task is to complete the conditional statement so you can detect when Lana would leave the screen. Furthermore, you should make the necessary adjustments to Lana's position to prevent her from leaving the screen. In essence, your challenge is to implement boundary collisions to ensure that Lana is always visible on the screen during play.
We are working with a two-dimensional, or 2D, game world. Therefore, the locations of all objects are represented by x and y coordinates. Recall that every object in Unity has a Transform component with a Position variable.
Again, we can visualize our game world with a drawing. You have a character and a screen. You know the x position of the character and the width of the screen. The basic principle behind collisions in a 2D world is this: we know two objects collide when they overlap one another. In other words, their positions intersect one another.
How will you know when the character moves outside the boundaries?
What will you do to prevent the character from moving outside the boundaries?
Operators allow us to make various calculations in our code. There are many types of operators, but we will focus on two that are of immediate importance.
Math operators are probably familiar to you from ...math. A computer is like a high-powered calculator that can perform many operations quickly. We can add, subtract, multiply, and divide using these symbols. Here are some examples. In addition, the modulus operator takes the remainder from a calculation. Math operators can be used for a variety of purposes. One example could be adjusting the position of your character once it collides with the boundary of the screen.
Equality operators help us compare values. Here are the most common equality operators along with examples demonstrating how they are used. Note that a single equal sign is used to set the value of a variable. It is known as the assignment operator. However, a double equal sign is used to determine whether two values are equal. Equality operators are often used in conditional statements to determine whether or not certain code should be executed. For example, you could compare the position of your character to the edge of the screen to determine whether a collision occurs.
Similar to math, computer expressions contain a collection of values, variables, and operators that can be evaluated. You already know about variables, and operators. When you combine these together to calculate, modify, or compare values in your code, you create an expression. It is important to note that expressions are evaluated in a particular order. For example, multiplication occurs before addition and items in parentheses are always evaluated first. In the case of a tie, expressions are evaluated from left to right. Here are the precedence rules for evaluating expressions. See if you can work out these examples.
Open your project. Proceed to the UserMove script's CheckBounds() function. Here is an example for what your code might look like.
To determine whether Lana exits the left side of the screen, we ask whether her x position is less than the negative screen width. If so, we set her x position equal to the negative screen width, which prevents her from moving beyond that point. Similarly, for the right side of the screen, we ask whether Lana's x position is greater than the screen width. If so, we set the x position equal to the screen width.
Run your game. Move Lana towards the edges of the screen. It looks like she is still moving out of the player's view. Zoom out and focus on the Scene window. There you will see that Lana does indeed stop eventually, but it is not where we might expect.
Although our logic is sound, we aren't quite finished with our boundary collision calculations. We need to make some more adjustments to our code to produce the desired behavior. You will work on these adjustments in the next challenge.
Currently, our code does not result in the exact behavior that we want. Although Lana is stopped eventually, it isn't at the visual edge of the screen as we expected. We need to take some additional factors into consideration, such as the origin points of our character and the game world. Your challenge is to ensure that Lana remains fully visible on screen and is stopped just at the edge of the screen when she tries to move outside of it.
In Unity, both objects and the game world have a center origin point. This means that their position is determined at the very center, rather than another point, such as a corner or edge. Hence, when making calculations, we must adjust for the origin points of our objects.
Let's visualize the situation. Suppose we want to find the right edge of our character. As you can see, we must add half the width of the character to the origin point to get there. On the other hand, to find the left edge, we subtract half of the width from the origin point.
Likewise, the game world itself has a center origin point. Thus, the point (0, 0) is located at the very center of our screen. Therefore, you need to adjust for this in your boundary collision calculations as well. For instance, the right edge of the screen is actually located at the origin point plus one-half of the screen width. Meanwhile, the left edge of the screen is located at the origin point minus one-half of the screen width.
You will make adjustments like these for nearly all position and collision calculations in our game. That's because we often want to control our objects according to their edges, while Unity places them according to their center points. These adjustments ensure consistency between our calculations and the requirements of the game engine.
Now that you are aware of the origin points of our objects and the game world, consider how you will adjust the code in your UserMove script to accommodate this information.
Take a look at your Constants script. Conveniently, there are a few constant values stored that can help you make the calculations required in this challenge.
The TILE SIZE represents the size of our character. All of the objects in our game are composed of 32 by 32 pixel images. Hence, Lana's width is 32 pixels and her x position is at center origin point. How will you calculate her left and right edges?
The SCREEN WIDTH represents the size of our screen, which is 1024 pixels wide. Again, you know the center of the screen is found at 0. How can you calculate the left and right edges of the screen?
Once you determine the logic behind these adjustments, make sure to implement them in your UserMove script. Test your game after each change to make sure that your code works as expected. Leverage the values in the Constants script to adjust your calculations.
Open your project. Proceed to the UserMove script's CheckBounds() function. Here is example code that takes the center origin of objects into account.
Notice that our code now uses half of the object and screen width in the calculations. Since our objects have a center origin point, we need to add or subtract half of their width to arrive at their edges. Ultimately, the edges are needed to determine our boundary collisions.
For example, consider the calculations that determine whether Lana moves past the left edge of the screen. We need to find Lana's left edge by taking her x position, which is at her center, and subtracting half of her width. Similarly, we need to find the left edge of the screen by taking its origin point, which happens to be 0, and subtracting half of its width. If Lana's left edge is less than the screen's left edge, we know that she is moving outside the boundaries. Therefore, we set her left edge exactly equal to the left edge of the screen. This ensures that she remains completely visible on screen at all times. The calculation for the right boundary of the screen is nearly identical. See if you can reason out the code yourself.
Afterwards, run your game to test it out. As you can see, Lana now stops at the very edges of the screen, but remains visible to the player at all times. Congratulations on implementing your first boundary collisions. This is something you will do in nearly all games that you create.
We have looked at some fundamentals, such as getting our character to move and collide with the edges of the screen. Now, let's give her something really interesting to do.
How about giving Lana the magical power of invisibility? Not only will this add some excitement to our game, but it will help you learn about things like the game loop, conditional statements, and function calls.
Open the challenge project. Right now, our character can move, but not much else. Take a look at the UserInvis script in your code editor. You have been provided with a few variables and empty functions. Your task in this challenge is to utilize the variables and complete the functions to grant Lana the power of invisibility. Specifically, here are the requirements for your solution.
1. Lana becomes invisible when the player presses a key.
2. After a specific amount of time, Lana becomes visible again.
3. Once visible, Lana must wait a specific amount of time before using her power.
4. Afterwards, Lana may become invisible again.
Design a logical system to accomplish these requirements. Then, use your code to implement it. When you're finished, Lana will have the power to turn herself invisible.
To assist you with this task, these variables are provided in the UserInvis script:
1. _canInvis indicates whether the power can be used.
2. _isInvis tells us whether Lana is currently invisible.
3. _duration is how long the power lasts once activated.
4. _cooldown is how long the player must wait before reactivating the power after it has been used.
5. _startTime marks when the ability was used. For example, we might take note of when the power was activated, so we can measure how long it should last. Alternatively, we might take note of when the power was deactivated, so we can tell when it can be reactivated.
In addition, several empty functions are provided, including Start(), Update(), CheckUserInput(), and CheckInvis(). You will place code inside these functions to implement your system. You do not need to create any additional functions or variables for this challenge.
Think about how you want your invisibility system to work. Use the hints to help guide you in applying the provided variables and functions, as well as your own code, to implement your solution.
Update() is a special function in Unity. Unlike the Start() function, which is run a single time, the Update() function runs continuously. We use Start() to accomplish one-time setup steps such as initializing variables. However, we use Update() to execute code that needs to be repeated throughout our game.
If you are familiar with movies or animations, you may have heard of frames. A frame is like a single still image. If you take individual frames with slight differences between them and flip through them quickly, you will perceive motion. This is basically how movies and animations work. Put lots of images together and move through them rapidly to create a motion picture.
The rate at which these images are swapped is known as the frame rate. In movies and animations, a frame rate of 24 frames per second, or FPS is common. For games, you often hear of 30 or 60 FPS to describe how smoothly the graphics update during play. The actual framerate achieved depends upon various factors, such as how much computing power the game requires and what hardware each individual player uses.
In our code, we are concerned about more than just visuals. We can think of a frame as capturing the entire state of our game, including all objects and variables, at a moment in time. As we put multiple frames together, we create an entire living game world. The game loop is a term used to describe this phenomenon. The game loop represents all of the events that take place in our game in a single instant. This loop is repeated over and over to produce our game.
Hence, we can use our Update() function to control our game loop. The Update() function runs many times per second. Thus, you can think of each cycle as a frame. When you put the frames together, you have your game. When designing your game systems, it is helpful to think of what would occur during a single frame.
For example, think of our character's movement. In one frame, we might only move our character one pixel to the right. To do so, we might write code that detects the player's key press and adds 1 to the character's x position. If we repeat this code over a series of frames, we will see the character moving in action.
Think about your current challenge. What occurs during a single frame of your system? What needs to be repeated over and over to produce your complete system? What order do these events need to occur in?
When you call someone's name, like "hey, Yoshi," you expect them to respond to you by looking in your direction or saying something back. Similarly, when you call a function in a computer language, you use its name to make something happen in your code. For instance, you might provide information to the function, ask the function to do something, or receive information back from the function.
To call a function, you type its name followed by parentheses. Whenever your computer program is running and a function call is encountered, the code inside the function is immediately executed. If code is inside a function, but that function is never called, then its code will never be executed. Thus, you can use function calls to control when the code inside your functions is executed.
In the current challenge, you can make calls to your CheckUserInput() and CheckInvis() functions to execute your invisibility system. If these functions need to be executed as part of your game loop, then you may need to call them from your Update() function.
Conditional statements are one of the most fundamental and important aspects of coding. They help you control when and whether your code is executed. The most basic conditional statement is the IF statement. Here is its structure. We type the IF keyword, followed by parentheses. Inside the parentheses, we place a condition. Then come brackets. Within the brackets, we place our code. This code is only executed if the condition is true. Otherwise, if the condition is false, the code is skipped.
You can extend an IF statement to include multiple conditions. To add another condition, use the ELSE IF keywords followed by the same format. Once you do this, your code will first evaluate the original IF condition. If that happens to be false, it will check the first ELSE IF condition. If it is true, then the code contained within the condition is executed. Otherwise, it is not. You can add any number of ELSE IF conditions in this manner. Each condition will be checked from top to bottom until either a true condition is found or all of the conditions have been determined false.
Interestingly, when checking boundary collisions in the previous challenge, you already specified conditions inside an if statement. Let's look back at the conditional statement from our prior solution. As you can see, we first checked whether our character moved past the left edge of the screen in an IF condition. If so, we stopped her movement by setting her x position. If not, we checked whether she moved past the right edge of the screen in an ELSE IF condition. If so, we stopped her movement. If not, our conditional statement would end with none of its code executed because none of its conditions were true. In other words, since our character never moved outside the boundaries of the screen, we allowed her position to be updated normally.
One other keyword you can use with your IF statement is the solo ELSE. You may add a single ELSE condition to the end of any IF statement. There are no parentheses for an ELSE condition. You simply put ELSE and proceed directly to the brackets. The code inside the ELSE condition executes only if none of your other conditions are true. Hence, if you want to make sure that something - anything - happens at the end of your IF statement, you can use ELSE. Think of it as a default action that is taken when none of your special circumstances occur.
An alternative to the IF statement is the SWITCH statement. Most of the time, SWITCH is functionally identical to IF, but you may prefer to use them at different times to improve the structure and readability of your code. The SWITCH statement checks a variable against multiple mutually exclusive values, which are called cases. If a case is true, then the code within that case is executed. Here is the basic structure.
Begin with the SWITCH keyword. Place a variable within parentheses. Follow with brackets. Inside the brackets, type the CASE keyword, followed by a valid potential value for the variable, and a colon. Then type any unique code that you would like to execute in the event that the case is true. End the case with the BREAK keyword and a semicolon. The BREAK keyword tells the SWITCH statement to stop executing and allows us to proceed to subsequent code that lies outside of our conditional statement. Hence, once any case is determined to be true, its code is executed and the SWITCH statement ends. Repeat this process to add any number of unique cases. Once all of the cases are included, finish with the DEFAULT keyword and a colon. Again, place any code that belongs to the case inside, then finish with BREAK and a semicolon. Similar to ELSE, the default case only executes if none of the other cases are true. Thus, it is like a backup action that can be taken when none of the special conditions occur.
Here's an example SWITCH statement we might use within our invisibility system. It contains cases that ask whether the _isInvis variable is true or false. Based on the current value of the _isInvis variable, we might add code within those cases that change the state of our game.
In this challenge, you'll practice creating your own complete conditional statements. Try using both the IF structure and the SWITCH structure in your code as you create your invisibility system.
Unity has convenient built-in functions that help us check for user input. For keyboard input, we can use the Input.GetKey(), Input.GetKeyDown(), and Input.GetKeyUp() functions. GetKey() is called every frame that a key is held. GetKeyDown() is triggered one time at the moment a key is pressed down. GetKeyUp() is triggered one time at the moment a key is released. With these functions, we can customize the controls available to players of our game.
Inside the parentheses of these input functions, we must identify what key we are looking for. We do so by typing KeyCode followed by a dot and the key. All of the keys are already defined for us in Unity. For instance, to check for the spacebar, we'd use KeyCode.Space.
Putting it all together, we'd end up with code like this to detect the moment when the player presses the spacebar. Ultimately, this code gives us a value of true or false. If the key press occurs, it's true. If not, it's false.
Therefore, we are in a perfect position to combine our input code with conditional statements. Indeed, this is a common technique for handling user input in Unity. Think about our current challenge. Suppose we want to check for a key press inside our CheckUserInput() function. We might use an IF statement and input code like this.
As you can see, we check whether the I key is pressed down. If so, we can write additional code that makes this happen. Try including an IF statement like this one in your CheckUserInput() function for this challenge. Expand and modify your function as necessary to implement your invisibility system.
Remember that we have math operators, smooth operators, and equality operators, among many others. Boolean operators allow us to augment our logic. They are especially useful within conditional statements.
The conditional AND, represented by a double ampersand, asks whether all of the provided conditions are true. For example, the code inside this IF statement is only executed if both x = 0 and y = 1. If either x or y has any other value, then the condition is false. All of the portions must be true for the condition to be true.
Meanwhile, the conditional OR, represented by two vertical bars, asks whether any of the given conditions are true. For example, the code inside this IF statement is executed if either x = 2 or y = 3. If x = 2, but y != 3, the condition is still true. As long as any portion is true, the entire condition is true.
Another related operator is the logical NOT, which is surprisingly represented by an exclamation point. When placed in front of a Boolean value, the logical NOT operator takes the opposite of that value. For instance, consider our _isInvis Boolean variable, which indicates whether our character is currently invisible. Assuming our character is invisible at this moment, the value of _isInvis is true. If we put the logical NOT operator before _isInvis, we would get a false value. On the other hand, if _isInvis is false and we put the logical NOT operator in front of it, we'd get a value of true. As you can see, the logical NOT operator takes the opposite of a Boolean value.
Furthermore, the logical NOT operator can be combined with equality operators as well. Recall that a double equals sign asks whether two values are equal. As shown, we can use a not equals sign to ask whether two values are not equal to one another.
For practice, consider whether these examples are true or false. Assume that x has a value of 0 and y has a value of 1.
In our current challenge, _isInvis and _canInvis demonstrate a useful feature of Boolean variables. Since Boolean a variable can only be true or false at any given time, they can be used to represent states in a system. With one variable, you can represent two states. With two variables, you can represent four states. When we track states in this manner, we often say that we are using Boolean flags, or simply flags, or bflags. For example, _isInvis indicates whether the character is or is not invisible. In addition, _canInvis indicates whether the character can or cannot become invisible. These two flags yield four important possible character states in our invisibility system. Depending on the character's state at a given time, our code will take dramatically different actions. Hence, these flags help us keep track of and control the state of our system throughout the game.
Think about how you can apply these Boolean flags and Boolean operators to implement your invisibility system. For instance, you might allow or disallow certain user input based on whether the character can become invisible. Or, you might show or hide the character on screen based on whether the character is currently invisible.
In Unity, the Time.time command returns the time, in seconds, since the game started running. Logically, you can think of Time.time as being the time right now. Thus, whenever you need to know the time, use Time.time.
Recall the requirements for your invisibility system. Once activated, the invisibility power should last for a specifc amount of time. Once deactivated, the player must wait a specific amount of time before using the power again. Therefore, you need a way to keep track of how long the invisibility effect has remained active or idle. Based on this timing, you can control the state of your system to allow or disallow the player from triggering the invisibility power.
You were provided with a _startTime variable. This can be used to mark the time at which the character's invisibility power was used by setting it equal to Time.time. Later, you can calculate a duration by subtracting Time.time from your stored _startTime. In this manner, you can determine how long the player has been invisible. Likewise, after the power is deactivated, you can make a similar calculation to determine how long the player must wait until the power may be used again.
Along with your Boolean flags and conditional statements, you can use timing to control the state of your invisibility system.
You have created and used many variables thus far. All of these have been GLOBAL variables. Global variables are defined in the CLASS section of a script, which lies outside any specific function. Since they don't belong to any specific function, they can be accessed by all functions throughout the script. In contrast, LOCAL variables are created inside specific functions. A local variable can only be used inside the specific function where it was created.
Consider this example script, which demonstrates the difference between global and local variables. Recall that global variables have an access level, such as public or private, which indicates whether the variable can be accessed by outside scripts. Local variables have no access level. That's because they can only ever be accessed within the function where they were created. Other than that, local variables follow the same definition and usage rules that we are already familiar with.
You may be wondering when to use global and local variables. An effective guideline is to use local variables whenever possible and global variables only when you truly need them. Most of the time, you can accomplish your calculations inside the body of a function using local variables. This is preferred, since local variables only live as long as the code of a function is being run, which is a very brief period of time. Meanwhile, global variables exist for the entire duration of our script, which can be the entire time that our game is running. Therefore, global variables are generally more resource-intensive than local variables. Yet, there are times when we need to store information outside of a specific function. For instance, sometimes we have variables that need to be accessed by multiple functions or shared with other scripts. In these cases, we would use global variables. Get into the habit of using local variables whenever possible to perform momentary calculations, but recognize that global variables have a place in storing a few pieces of key information as well.
The time element of our invisibility system gives us an excellent opportunity to compare global and local variables. Our _startTime variable is global. It records the moment at which the player became invisible. If we set our _startTime to Time.time inside a local variable of a function, it would get updated every frame and always equal the current time. There would be no way for us to tell how long the character has been invisible. Thus, we set the _startTime variable to Time.time only one time, just at the moment when the character becomes invisible. In contrast, when we want to know how long the character has been invisible in total, we can use a local variable. For instance, we might create a variable called DURATION inside our CheckInvis() function. We can calculate how long the character has been invisible by taking Time.time and subtracting the _startTime. Hence, every frame that the CheckInvis() is called, we calculate the most up-to-date duration of the invisibility effect. With this information, we can modify the state of the system. For example, if we find that the character has been invisible for longer than the allowed amount of time, we can update the state to make the character visible again.
At that point, we could update the _startTime variable again and start calculating the duration of time that the character has been visible. Once enough time has passed, we can update our state to allow the character to use her power once again.
Note that the _startTime variable is global. Each time its value is updated, it remains available for our use and doesn't change unless we change it. Since we need to refer to the _startTime over the lifetime of our script, it makes sense to store it globally. Meanwhile, our duration variable is local inside the CheckInvis() function. Every time CheckInvis() is run, the duration variable is created, calculated, utilized, and destroyed, all within an instant. Since we only need to know the duration in that instant to update our invisibility system, it is much preferred to store it as a local variable.
The Unity game engine applies a component-based structure. This means that the various objects in our game, such as our characters, are composed of a collection of individual components. These components represent characteristics of our objects, such as the ability to move or collide.
Take a look at the Player GameObject in our scene. Notice that it has various components, including Transform, SpriteRenderer, and the UserMove script. The Transform component gives our character a position in the game world. The SpriteRenderer component displays our character's image so it is visible on screen. The UserMove script let's us move our character around the game world.
As you can see, our UserInvis script is also a component. When we attach this script to the Player object, we are saying that we want our Player to have the features described within that component. By putting collections of individual components together, we are able to create unique objects, such as a character that can move and turn invisible.
Since all objects in Unity follow this basic formula, we have some convenient built-in ways to access our objects, as well as their individual components, from inside our code.
Whenever you type the gameObject command from inside a script, you are accessing the object to which that script is attached. For instance, when you type gameObject inside your UserInvis script, you are accessing the Player object in our scene.
Furthermore, you can access specific components attached to an object using the GetComponent command. After typing GetComponent, you place the type of component inside less than and greater than symbols. Afterwards, add a pair of parentheses. For example, if you put this code inside your UserInvis script, it would retrieves the Transform component from the Player GameObject.
Moreover, you can also access properties inside a component using dot notation. In other words, once you retrieve a component, you can add a period followed by the name of a property. At that point you can refer to, modify, or set the value of the property. For example, this code accesses the x position of our Player GameObject using dot notation.
As demonstrated, you can retrieve objects, components, and properties from inside your code using the gameObject, GetComponent, and dot notation commands. You will become very familiar manipulating these features as you create your game.
Start practicing now by retrieving Lana's SpriteRenderer component from inside your UserInvis script. Then, use it to modify the visibility of Lana on screen at the appropriate times.
All Unity components have an ENABLED property. This property flags whether the component is or is not active.
A SpriteRenderer component is used to display a 2D image, such as our character. If you look at the Player GameObject in your project, you will see that a SpriteRenderer is attached. If this component is enabled, the character is visible. If this component is disabled, the character is invisible.
In your code, you can set the value of the enabled property. Access the enabled property from the player's SpriteRenderer component like so. Set enabled to true to make the character visible or false to make the character invisible. Accordingly, the on-screen visuals shown to the player are updated when you enable or disable the SpriteRenderer component.
By now, the logic behind your invisibility system should be in place. Taking this final step to enable or disable the SpriteRenderer at the appropriate times will make your invisibility system apparent to the player by changing the visuals on screen.
Debugging is a process of iteratively revising and improving your code to get it to function the way you want it to. Although you may not have realized it, you have already been debugging since the day you started this course. Every time you type code, run your game to test it, then go back and make adjustments, you are engaging in the debugging process.
One simple technique that can dramatically improve the effectiveness of your debugging process is to print messages to the console window. You can find the console window in the Unity interface. In this window, different messages, such as errors and warnings, will appear at times. If you haven't seen them already, I can guarantee that you will. It's always good to read those messages carefully, since they can help you figure out how to improve your code.
However, we also have the opportunity to take a proactive approach to debugging our code. By using the Debug.Log() command, we can print specific messages in the console window on demand.
To print a message in the console window, type Debug.Log followed by parentheses. Inside the parentheses, type a string. The contents of that string will be printed to the console window when the code is executed.
You can use Debug.Log() to keep track of when certain events occur in your game. For instance, consider this code, which checks for the spacebar to be pressed. If the key is pressed, Debug.Log() is called and a message is printed in the console window. I'll run the game now. Each time I press the spacebar, a message appears up in the console window.
Similarly, you can place Debug.Log() messages in critical portions of your scripts. These messages can help you keep track of the order in which events occur and the state of your system at various times. Now and in the future, using these messages can help improve the efficiency of your debugging process. Start using them now by incorporating Debug.Log() into your UserInvis script. Try printing a message each time the state is updated or user input is received in your script. Then, test your game and see what is printed to the console window.
Your task in this challenge was to grant Lana the power of invisibility. Let's review the requirements.
1. Lana becomes invisible when the player presses a key.
2. After a specific amount of time, Lana becomes visible again.
3. Once visible, Lana must wait a specific amount of time before using her power.
4. Afterwards, Lana may become invisible again.
As long as the solution you implemented in your UserInvis script meets these requirements, you have succeeded in this challenge. As with all challenges in this course, the solutions involve complex systems that can be created in an infinite variety of ways. Therefore, your solution may differ from the example. Take it as an opportunity to compare and contrast different approaches.
We'll break down the solution piece by piece. Recall that you were provided with these variables to start. In addition, you had empty Start(), Update(), CheckUserInput(), and CheckInvis() functions. Using just these variables and functions, you can implement a fully working invisibility system.
As always, we want to initialize our private global variables in the Start() function. These values assume that our scene starts with the character visible and able to become invisible. The duration of the power is set to 1 second, while the cooldown period is set to 2 seconds. The start time is initialized and can be updated once the ability is triggered.
Remember that Update() represents our game loop. The game loop runs continuously and updates the state of our game each frame. Inside Update(), we make calls to our CheckUserInput() and CheckInvis() functions. This means that we are repeatedly checking for user input and updating the character's invisibility status throughout the game. Thus, we are ready to activate or deactivate the invisibility power at any time. Calling out functions in Update() is what ensures they are checked repeatedly as our game loop runs.
In contrast, recall that we can write our functions in the context of a single frame. We only need to focus on what needs to happen in one frame. Once that frame is repeated by our game loop, the individual actions yield a complete working system over time.
Look at the CheckUserInput() function. The goal of this function is to accept input from the player and update the state of our system. An IF statement is used to check the necessary input and states.
If the I key is pressed and the character is able to become invisible, we update our state. Now the character is made invisible and therefore no longer able to become invisible. At the same time, we disable the SpriteRenderer component to make sure the character is not visible on screen. Then, we update the _startTime so we can keep track of how long the character has been invisible.
On the other hand, suppose that the I key is pressed, but the character cannot become invisible. This is a case where we wouldn't want to update the state. The character is already invisible and we don't want to allow the power to be activated again until a few seconds after it has been deactivated. This prevents the player from using the ability continuously, which is not the intended design. When this occurs, we might ignore the input or give the player some feedback, such as a negative sound to indicate that the input was disregarded.
Lastly, suppose that neither condition is true. We can include an ELSE condition to capture this. For example, this condition might be triggered if a different key was pressed or no key was pressed at all. That concludes the CheckUserInput() function, which takes input from the player and updates the invisibility state.
Let's turn to our final function, which is CheckInvis(). The purpose of this function is to manage the invisibility states to determine if the character is invisible and whether the character can become invisible.
Since our system leverages a time-based system, we immediately calculate the present duration of the state. The state may be either visible or invisible. Either way, we kept track of the last time the state was updated in the _startTime variable. Therefore, we can calculate the duration of the current state by taking Time.time minus _startTime.
Here, we use a SWITCH statement to check the value of _isInvis.
In the case the character is invisible, we check the duration of the current state. If that exceeds the amount of time stored in our _duration variable, then we know we need to update the character's state. We set _isInvis to false, make the SpriteRenderer visible on screen, and update our _startTime variable once again to keep track of our new cycle.
On the other hand, suppose that we hadn't exceeded the duration of the power. In that case, nothing would happen and the character would be allowed to continue being invisible. We only update the character's state under the conditions that we specified.
Next, consider that the character may be visible when this function is run. We enter the false case. Here, we check whether the duration of the current state exceeds the specified cooldown period. If it does, we want to allow the player to trigger the ability again, so we set _canInvis to true. On the other hand, if the cooldown period has not yet completed, then we don't change anything.
That concludes the example solution for the UserInvis script. Review the script now and make sure that you understand how it works. It may be especially helpful to consider how information flows through the script and how the states are updated.
For example, imagine that the game is started and the player presses the I key. Walk yourself through the script from CheckUserInput() to CheckInvis() over a single frame. Then consider the game loop established by the Update() function. Determine what happens in your script over multiple frames. How do the states change? What happens when the player presses the I key? When is the character visible or invisible? When is the character allowed to become invisible?
As an additional exercise, you can write Debug.Log() statements throughout your script. Any time a state is changed, input is detected, or a condition is true, you could print a statement to the console describing the situation. This will help you visualize how your script works and what happens over time. Indeed, printing statements to the console is a useful technique for debugging any system that you implement.
Another great thing about the system that you just implemented is that it can be applied to any variety of character abilities. While you made a character invisible this time around, you could easily repurpose this system to match any time-based ability, such as sprinting at super speed or temporary invincibility.
Collectable objects appear in some form in nearly all games. Once you know how to create collectable objects, you can be prepared to include them in your future games.
In our game, the sippy cup represents a collectable object. As Lana moves through the game world, we will place sippy cups at various positions and challenge the player to collect them.
To get started, we have to write the code necessary to detect collisions between our character and collectable objects. Here are the requirements for this challenge:
1. The Collectable script is attached to every collectable object. This script should continuously check for collisions.
2. The CheckCollisions() function retrieves our character's position and uses it to check for collisions with the collectable object.
3. If a collision is detected, destroy the collectable object.
Open the challenge project. Look to the Hierarchy window. Inside the Collectables object are 4 collectable objects. Notice that each individual object has a Collectable script attached to it. When you code your Collectable script, think in terms of what happens when Lana collides with a single collectable. Subsequently, your complete script can be attached to any collectable in the game world. Also recall that the Player object represents Lana. This is the object whose position you want to check for collisions with the collectable objects. Turn to the scene window. Notice that a line of collectable objects has been placed just in front of our character. Although we might not design a finalized game level like this, it is extremely convenient to position these objects next to our player while we are developing our Collectable script. That's because we can rapidly run our game and collide with the objects to determine whether our script is working. Since you need to iteratively revise and test your script to make sure it is working as intended, putting the collectables so close to our player is a great time saver. Beyond this challenge, think about how you can arrange your scene to make your development process more efficient in future challenges as well.
Open the Collectable script. This is where you will code your solution. You have been given empty Update() and CheckCollisions() functions. Remember to add any code inside Update() that needs to repeat as part of your game loop. Meanwhile, CheckCollisions() should be used to check Lana's position against the collectable object to determine whether there is a collision. Try applying local variables exclusively in this challenge, since global variables are not necessary. Furthermore, recall that you implemented boundary collisions in a previous challenge. You should be able to leverage that knowledge to help you implement collectable collisions. Lastly, try to work out the logic behind your solution using PseudoMapping before you attempt to code it.
Thus far, you have used mostly primitive data types, such as bool, int, and float. Primitive data types cannot be broken down to any simpler data type than they already are. In contrast, composite data types are composed of collections of simpler data types. For example, recall that a string is composed of individual characters. Each character has the char data type. By adding several characters together, we form a string. Hence, the string data type is a composite of multiple characters.
Furthermore, you can think of the objects in our game as complex composites. For instance, our player has a Transform component. The Transform is composed of multiple variables, including position, rotation, and scale. In turn, those are each composed of primitive float values representing x, y, and z. As you can see, the combination of primitive data types can generate sophisticated objects that are composed of many levels of variables.
This process of creating complex objects through the combination of simpler ones is known as composition. With composition, we are no longer limited to using primitive data types in our scripts. We can access entire objects and their components, as well as the individual variables associated with them. Moreover, we can assemble a combination of variables to represent anything we want in our game world.
In the current challenge, you need to make use of composite and primitive data types. The Player object in our scene represents our character and is of the GameObject type, which is a special container that Unity uses to represent many different kinds of objects. Note that every GameObject has a Transform component, which has a position variable that tells us where the object is located in our game world. To write your CheckCollisions() function, you need to know the position of our character and the collectable.
The position variable is of the Vector3 type. Basically, a Vector3 is a composite data type that stores 3 primitive float values named x, y, and z. Hence, the position Vector3 gives our objects an x an y position in the game world. Since we're working in 2D instead of 3D, we won't worry about the z axis at this time. Instead, focus on retrieving the x and y coordinates from the Player GameObject in your scene. Likewise, you can retrieve the x and y coordinates of any collectable object. Once you have the position of the Player and the collectable, you can proceed to check for collisions between them.
Here are some suggested local variables that you can define inside your CheckCollisions() function. As you can see, we aim to retrieve the objects themselves, their positions, and their sizes. Think back to the boundary collisions that you implemented previously. Think about the calculations you needed to make in regards to the position, size, and movement of our character. This should help you see why these variables are relevant to include in our current script.
Recall that the gameObject command can be used to access the object to which our script is attached. Hence, typing gameObject into our Collectable script accesses the collectable object our script is attached to. However, to check for collisions, we also need to access the Player object in our scene, which is not attached to our script. Thankfully, Unity has a tag system that allows us to access any object from any script.
Open your project. Look to the Hierarchy window. Note that the Player game object has been given a tag named Player. We can use that tag to access the Player from inside our Collectable script.
On a side note, see that there are many tags available to choose from. You can change the tag attached to any object using the dropbox. Furthermore, you can define your own tags by selecting Add Tag..., clicking on the plus sign, and providing a name. Afterwards, click on any object and use the dropbox to apply your custom tag. Using tags to identify your objects gives you a convenient way to access them in your code. You will likely do this often when working in Unity.
Returning to our code, we can access any tagged object using the GameObject.FindWithTag() command. Inside the parentheses, type in a string that matches the name of the tag. For example, here's how we can retrieve the Player object from inside the CheckCollisions() function of our Collectable script. In addition, we can use our familiar GetComponent() and dot notation commands to initialize the remaining local variables. At this point, we have the positions and sizes of our character and collectable. That's exactly what we need to be able to determine if our objects are colliding.
At this point, you are prepared to write your collision detection code. Similar to boundary collisions, you need to calculate the character's top, bottom, left, and right edges. Compare these values against the collectable's edges to determine if the objects overlap in our 2D space. In other words, determine if they collide.
Note that the objects in our game are treated as rectangles for the purposes of collision detection. Regardless of what visual image is shown on screen, we can imagine that a box surrounds every object. This is known as an axis-aligned bounding box, or AABB, or aaaaaaaaaaabb. It is axis aligned because its top and bottom edges are always parallel to our x axis, while its left and right edges are always parallel to our y axis. Again, no matter what the image for an object looks like, such as a diagonal or upside down character, the AABB remains the same.
AABB is one of the most common forms of collision detection used in 2D games. This method simplifies our computation, while also producing visually accurate collisions in the eye of the player in most circumstances. Generally speaking, the more squarish the game's art is and the better that the art occupies the full frame of the bounding box, the better AABB collision detection will look.
Think about AABB collisions from a logical standpoint. You know that two objects are colliding in 2D space when their bounding boxes overlap one another. Recall that our object are positioned according to their center origin point. Furthermore, with the size of our objects, we can calculate the positions of their edges. When we know the top, bottom, left, and right edges, we are able to produce our AABB. We can compare two AABBs against one another to determine whether they collide.
Given two objects, how will you know when they are colliding on the x and y axes? What conditions must be true for you to be certain that they are colliding? Once you can answer these questions, you can add conditions to your CheckCollisions() function that determine whether the objects are colliding.
To help get you started, consider this code, which determines whether our character and collectable object collide on the x axis. As you can see, we take the character's x position and offset it by half of the character's width to determine the left and right edges. We do the same for the collectable. If the character's right edge exceeds the collectable's left edge and the character's left edge does not exceed the collectable's right edge, we know that the objects are overlapping on the x axis.
However, we do not yet know that they are colliding, since they could be at completely different y positions. Hence, see if you can expand this code to check for overlap on the y axis. Once you can confirm that the objects overlap on both the x and y axis, you can be certain that there is a collision.
Once you detect a collision, the final requirement is to destroy the collectable. To accomplish this, use Unity's Destroy() function. This function destroys a GameObject or one of its components on demand. In our particular case, destroying the collectable will remove it from the game world. As such, the player will no longer see the object on screen. This gives the impression that our character picked the object up and collected it.
To use the function, type Destroy followed by parentheses. Inside the parentheses, place the object that you would like to remove from the game. For example, you can type the gameObject command inside the Destroy function. Recall that gameObject refers to the object our script is attached to. Since the Collectable script is attached to the collectable that our character is colliding with, this is precisely the object we want to destroy.
Besides the visual aspect of making our object disappear, note that Destroy() permanently removes an object from our game world. On one hand, this is a good thing, since that object no longer consumes computing resources. Thus, if we're done using an object in our game, it is a good idea to destroy it. However, take caution when destroying objects. Once an object is destroyed, it cannot be used again. If you try to access the object in your subsequent code, you will produce an error that stops your game from working. Hence, make sure that you only destroy objects that you no longer need and that you always destroy an object as the last thing you do with it.
Your challenge was to code your Collectable script, such that you could allow Lana to collect the sippy cups in our game world. Let's review the requirements.
Now, let's consider an example solution. Open your Collectable script. Note that no global variables were used for this solution.
Inside the Update() function, we call the CheckCollisions() function. That's because we want to continually check for collisions between our character and the collectables. Lana is constantly moving around the screen, so we have to be ready to detect collisions at any moment.
All of the action takes place within our CheckCollisions() function. We establish local variables to retrieve the position and size of our character and collectable. The Player object is retrieved according to its tag. Dot notation is used to store the player's position in a Vector3 component. The Vector3 has x and y properties, which give us its x and y position in the game world. Similarly, we retrieve the size of our character using the size property of its SpriteRenderer component. Size is also a Vector3, whereby the x property represents the full width of the object, while the y property represents the full height of the object. The collectable object has the Collectable script attached. Therefore, we use the gameObject command to retrieve it. Otherwise, its position and size are accessed in the same manner as our character.
The final portion of our CheckCollisions() function involves using this information to determine whether the characters collide. Essentially, we know that two objects collide when they overlap in 2D space. Since we are using axis-aligned bounding boxes, we compare the four edges of each object. If the character's right edge is greater than the collectable's left edge and the character's left edge is less than the collectable's right edge, the objects overlap on the x axis. In addition, if the character's top edge is greater than the collectable's bottom edge and the character's bottom edge is less than the collectable's top edge, the objects overlap on the y axis. We have a collision only if there is overlap on both the x and y axis. All of these conditions must be true.
Consider a visualization to help you think through the logic behind AABB collisions.
Once you are certain that a collision occurred, you call the Destroy() function from inside the body of your if statement. Provide the gameObject command to destroy the collectable to which your script is attached. Your script is complete.
Return to your project and test your game. The Collectable script attached to each sippy cup is constantly checking for a collision with Lana. As soon as Lana collides with a sippy cup on screen, it is destroyed and removed from the game. Conveniently, this script can easily be reused for nearly any collectable or collision object in your game. The code that you wrote implements logical AABB collisions in a script that can be attached to any object that we want our character to collide with. You could further expand or modify this script to handle other collision events, such as incrementing the game's score, rather than only destroying the object.
In this challenge, you will create an on-screen inventory to store the objects collected by our character. In doing so, you will become familiar with managing collections of objects and communicating across different scripts.
Open the challenge project. Look to the Hierarchy window. Notice that our familiar Player and Collectable objects are present. In addition, an Inventory object has been added to our scene. This object will store the collectables that our character gathers. In the Inspector window, you can see that it carries the Inventory tag and the Inventory script. Essentially, your challenge is to code the Inventory script such that it displays all of our character's collectables on screen. Here are the specific requirements:
1. Each object the character collects is stored in a List.
2. When an object is collected, it is added to the List.
3. As each object is collected, it is positioned in the on-screen inventory.
4. When a key is pressed, the last object in the inventory is removed. That's assuming that there is at least one object in the inventory to begin with.
Open the Inventory script. You are provided with empty Start(), Update(), AddObj(), and RemoveObj() functions. Remember to create and initialize any variables that you need to implement your system. Also, include any necessary code inside your functions.
Note that the AddObj() function has a special note. This function is accessed from inside our Collectable script. Basically, when our character collides with a collectable object, we want that object to be added to our inventory. To do so, our Collectable script needs to communicate with our Inventory script. We will explore the how this is done as part of this lesson.
Open the Collectable script. One minor change has been made. Previously, we destroyed the collectable after our character collided with it. However, this time we want to add it to our on-screen inventory. Therefore, we retrieve the Inventory object from our scene and call the AddObj() function inside Inventory script. Hence, whenever our character collides with a collectable, the Inventory script is notified. From there, you need to code the appropriate actions in your Inventory script.
Your completed project should look something like this. Lana collides with the sippy cups and they are added to an on-screen inventory. On a key press, the cups can be removed from the inventory. Remember to design a logical system for how you will achieve the challenge requirements. Then, proceed to the hints for additional assistance with this challenge.
A namespace is like an organized collection of code that lies outside of your script.
Conveniently, Unity, Microsoft, and others provide many built-in features for us to use, which can be accessed through existing namespaces.
To access a namespace, we place a USING directive near the top of our script. For example, you may have noticed that almost all of our scripts begin with USING UNITYENGINE. This gives us access to many of the excellent default features built into Unity, such as the ability to use the GameObject type. There are several other namespaces in Unity with specialized features, such as UNITYENGINE DOT UI, which gives us access to user interface elements.
Similarly, Microsoft provides namespaces that extend our capability to utilize the C# language. Often, we will want to add extra features to our scripts with a USING statement that refers to SYSTEM DOT SOMETHIHNG.
We are exploring how to manage collections of objects in our code. Therefore, we need to add the USING SYSTEM DOT COLLECTIONS DOT GENERIC line to the top of our script. Specifically, this allows us to access a C# List, among other things. In this challenge, we will use a List to organize our inventory of collectable objects, so be sure to include this line in your Inventory script.
On a side note, if you try to use a List or some other feature included in an external namespace, but you haven't imported that namespace into your script, your code will not work. You will produce an error, perhaps even with a message suggesting that you import the appropriate namespace. If you ever see an error like this, make sure to check that you have added the necessary USING statements to your script. Once an external namespace is added to your script, you have access to all of the features included in that namespace.
A List is one of several ways that we can manage groups of objects in the C# language.
Since we want to be able to add and remove objects from our character's inventory in real time, a List is an excellent choice for our current challenge.
A List must be assigned a specific data type. For example, you could make a list of the GameObject or integer type. All of the objects stored in the list must be of that type. Every object in a List has an index position. This tells us the order that our objects are arranged. A List is zero indexed, meaning that the first object has an index of zero. Each subsequent object has an index one higher, such as 1, 2, 3, and so on. Note that many aspects of computer languages are also zero indexed, so you will become quite familiar with starting from 0 rather than 1 like you might be used to.
A key feature of a List is that it has the ability to add or remove objects in real time. Therefore, we refer to a List as having a variable length. By contrast, other collections that you will use later on, such as an array, have a fixed length that never changes.
To summarize, a List stores data of a single type, is zero index, and has a variable length. To declare a List, use this format… To initialize a List, use this format… Why not declare and initialize a List in your Inventory script now? This List can store the collectable objects in your inventory.
We are able to access any object in a List on demand by referencing its index position.
To retrieve an object from a List, we type the List name, followed by square brackets that contain the desired index position. Here are some examples…
Once you retrieve the object, you can perform calculations on it, store it in a variable, or use it in a function.
Note that you will cause an error if you try to retrieve an object at an index position that does not exist. The error will read something like INDEX OUT OF RANGE. If you see this error, check to make sure that you did not attempt to access an invalid index position. Remember that a List is zero indexed as well, so the first object is actually at an index position of zero, while the second object is found at one.
Recall that objects can be added to or removed from a List at any time. We have some convenient built-in functions that help us perform these operations.
The Add() function adds an object to the end of a List. Inside the parentheses, we place whatever object we want to add to the List. Alternatively, the Insert() function inserts an object into a List at a specific index position. Inside the parentheses, you must provide the index position where you want the object placed, followed by a comma, and the object itself. This function does not modify any objects that are already in the List. Instead, it puts the indicated object at the specified position and bumps any subsequent objects to an index position one higher than before.
To remove an object from a List, use the Remove() function. Place the object that you want to remove inside the parentheses. Note that this function only removes the first instance of an object in the List. If there are additional matching objects, they will remain in the List. For more control, use the RemoveAt() function to remove an object at a specific index position. Provide a specific index position inside the parentheses and whatever object is found at that index is removed from the List. Again, no other objects are modified. It simply removes one specific object. Subsequently, any objects that follow in the List slide down to one index position lower than before.
There you go. You can control the objects in your List using the Add(), Insert(), Remove(), and RemoveAt() functions. Think about your current challenge. When do you need to add and remove objects from your inventory? Be sure to represent this in your code.
Every List has a LENGTH that tells us how many objects it contains. Access the length of a List using the Count property. Type the name of the List, followed by a period, and the word Count. Here is a demonstration.
You can use the length of a List in a variety of interesting ways. For instance, since a List is zero indexed, you can find the index position of the last object in a List by taking its Count minus 1. That gives you an easy way to access the very last item in a List.
You can also use the length of a List to ensure that you are only accessing valid index positions. To ensure that a List has at least one object, check whether the Count is greater than 0. To ensure that a specific index position is valid, ask whether it is less than the Count. There are other useful ways to use the length of a collection as well, which you will explore in future challenges.
Something you've done frequently already, but probably haven't thought about deeply, is use function arguments in your code. Any time you input information into a function, you are providing it with an argument. Typically, we do this by placing a value or variable inside the parentheses of a function.
For instance, when you add an object to a List, like so, you are providing the Add() function with an argument that contains the desired object. Subsequently, the Add() function takes that object and adds it to the end of the List.
Let's consider some example code. We have an integer value and a function that accepts an integer value as an argument. When an argument is passed into a function, it can be manipulated by that function. Note that the argument provides a name, or pseudonym, to the argument. Regardless of what value or variable is provided to the function as an argument, the body of the function will only refer to the argument by its pseudonym. Thus, we don't have to worry about the name of the original value passed into the function. We just use the pseudonym. Once inside the function body, we can utilize the argument. For example, we can modify its value and print it to the Console window.
Let's consider how arguments can be used in our current challenge. Open your Collectable script. Notice that once a collision is detected, the Inventory script is accessed and its AddObj() function is called. As you can see, the Collectable script provides an argument to the Inventory script's AddObj() function. This argument is the gameObject command. Hence, we are sending the collectable object that our character just collided with to the Inventory script's AddObj() function. Essentially, that means we are telling our Inventory script to add the collectable object to our on-screen inventory.
Open your Inventory script. Go to the AddObj() function. As you can see, the function receives an argument and refers to it by the pseudonym of theObj. Thus, theObj represents the collectable object that our character just collided with, which was sent to us by the Collectable script. At this point, you can manipulate the collectable object inside your AddObj() function using theObj variable. Your job is to code the AddObj() function so the collectable is added to your inventory List and displayed appropriately on screen.
Type casting involves the explicit identification of a variable's data type. There are times in our code where the type of certain data in our calculations is ambiguous. Usually, the computer will assume a default type for us, which we often don't want. We always want to be in control of our code, so there are times when we want to use a type cast to tell the computer exactly how we want our data treated.
A common use case for type casting is when we are making calculations on variables that have different data types. For instance, we might perform calculations on integer and float values. When this happens, the computer can make a decision for us, which often leads to unexpected results. For instance, you might expect that 5 divided by 2 equals 2.5. If you are using a float, you would get 2.5. However, integers can only be whole values, so 5 divided by 2 would give you 2 instead. To avoid situations like this, we can place a type cast in front of any ambiguous variables.
To do so, place parentheses containing the desired data type directly in front of the variable. Here is an example. When you use a type cast, you are telling the computer that you want your data to be treated as a specific type and it will honor your request. Make sure to take control of your code and use type casts in those occasional cases where you are performing calculations with values of mixed data types.
From a design standpoint, we think of our game world as a 2D space made up of pixels. However, from a coding standpoint, Unity thinks of our game world as a 3D space made up of units. To compromise between these two perspectives, we often need to convert between pixels and units.
All of the art used in our game is made up by 32 by 32 pixel squares. For example, Lana, the penguin, the sippy cup, and the individual platform pieces are all 32 by 32 pixel images. However, Unity's coordinate system does not understand pixels. As we've seen by looking at the position of various objects through the Transform component, Unity uses float values called units.
Our code must abide by Unity's system, even though it may be easier for us to design our game world in pixels. Fortunately, it is fairly simple to convert pixels to units and vice versa in our code. We will do this often when calculating positions, movement, collisions, and other events in our game.
Recall that our Constants script stores a PIXELSTOUNITS constant with a value of 100. To convert from pixels to units, we divide a pixel value by 100. Hence, 100 pixels is equal to 1 unit. In reverse, to convert from units to pixels, we multiply a unit value by 100. Hence, 1 unit is equal to 100 pixels. These conversions are quite important to keeping our calculations consistent.
Our constants script also stores the size of our objects in the TILESIZE constant. This has a value of 32 to match the 32 by 32 pixel images that compose our game. Similarly, we store the screen width and height as constants. All of these constants are integers that represent pixels.
However, if we want to use these values to position objects in our game, we must convert them into units. We can do so by taking the desired value and dividing it by the PIXELSTOUNITS conversion ratio.
Yet, since units are float values, whereas our constants are integers, we must remember to cast our calculations as float values. This ensures that the result of our calculation is a float value in units. It also yields a result that we would logically expect from our calculation, since float values can be decimals, whereas integers cannot.
In your current challenge, you need to position the collectable objects in your inventory on screen. We're already familiar with moving objects and compensating for their center origin points. However, this time around, you can leverage the values in your Constants script and type casting to position your objects accurately on screen according to Unity's units system. This is good practice for the many pixels to units conversions that we will make throughout the creation of our game.
Your challenge was to create an on-screen inventory to store the objects collected by our character. Let's revisit the requirements. Let's review the example solution.
Open the Collectable script. Notice that once a collision is detected, we no longer destroy the collectable object. Instead, we use GameObject.FindWithTag() to retrieve the Inventory object from our scene and GetComponent() to retrieve its attached Inventory script. From there, we place a call to the Inventory script's AddObj() function. Importantly, we pass an argument into the AddObj() function that contains the object that was just collected.
Open the Inventory script. We make sure to access the System.Collections.Generic namespace so we can create a List. Next, we create a global List named _objects, which represents the objects contained in our inventory. Inside Start(), this List is initialized. Then, our Update() function checks for the T key to be pressed. Any time the key is pressed, our RemoveObj() function is called.
Meanwhile, the RemoveObj() function first checks to make sure that at least one object exists in our inventory. If we try to remove an object from an empty List, there will be an error. Thus, this check prevents that possibility. If there are objects in the inventory, we proceed to retrieve the last object. We can calculate the last index value in any List by taking the Count minus 1. Hence, we retrieve the object at that index position. Subsequently, we use RemoveAt to remove the last object from our List. Finally, we Destroy the object to remove it from the screen. As you can see, we manage the object's logical existence in our code as well as its physical presence on screen. Anything we want to happen in our game world can be made possible through our code. And that's nice.
Lastly, we have the AddObj() function. This function accepts a single argument in the form of a GameObject named GameObj. Recall that our Collectable script calls the Inventory script's AddObj() function when there is a collision. It sends the collectable object that our character just collided with to our Inventory script, so it can be added to our inventory. Hence, the AddObj() function receives a collectable object and is responsible for including it in our inventory. To do so, we Add the object to our List. After, we want to organize the object in our on-screen inventory. Thus, we retrieve its position using GetComponent and store it in a Vector3 variable.
From there, we need to calculate a new position for the object. This example positions the objects in the lower left-hand corner of the screen. Each new object is added next to the previous one from left to right. This is just an example. You could have applied any reasonable system for organizing your objects on screen. To start, we need to line up our object at the left edge of the screen. Hence, for any given collectable, the x position starts at the left edge of the screen. We create a float variable to represent the new x position.
Furthermore, our screen and objects have a center origin point. Thus, the left edge of the screen is calculated as negative one-half of the screen width. Yet, our Constants script stores integer values in pixels, whereas the Unity game world stores float values in units. To make the necessary conversion, we cast our calculation to the float type and divide by the PIXELSTOUNITS ratio. This ensures that our calculation matches the units of our game world. Hence, the left edge of the screen is found at the negative screen width divided by two divided by the pixels to units conversion ratio, cast as a float.
From there, we must also compensate for the width and center origin of the collectable. All of the objects in our game have the same 32 by 32 pixel size. Thus, we add tile size divided by two to offset for the collectable's center origin point. Again, to convert between pixels and units, we cast this calculation as a float and divide by the PIXELSTOUNITS ratio.
Thus far, we've managed to align our collectable object to the left edge of the screen. If we stopped here, all of the collectables in the inventory would be stacked on top of one another. Therefore, we need a way to line them up in a nice row. We want each object to be positioned just after the previous one. Therefore, we take the total count of our objects minus one. This tells us how many objects are already in the inventory. We multiply that number by the total width of the collectable, again converted to units. Effectively, this shifts the position of our collectable from the left edge to the last position in line after any existing collectables. If there are none, our object will be exactly at the left edge. If there are existing collectables, our object will be added just after the last one. Conveniently, this works no matter how many objects we've collected.
With the x position complete, we create a float to represent the y position. Since we are creating a horizontal line of objects, they are all going to be assigned the same y position at the bottom edge of the screen. Again, we must convert our calculations from pixels to world units and compensate for the center origin of our objects. Thus, we take the negative screen height as a float, divide by two, and divide by the PIXELSTOUNITS ratio. That gets us the bottom edge of the screen. To that, we add the tile size as a float, divided by two, divided by the PIXELSTOUNITS ratio. That gives us half of the collectable's height and allows us to position the bottom of the object just at the bottom edge of the screen.
With these calculations complete, we update the x and y coordinates of our stored Vector3 position variable. Last, we set the position of our object equal to our stored position variable. This visually moves our object on screen to its new position.
This concludes your Inventory script. Walk yourself through these calculations as many times as needed to ensure that you are comfortable with them and understand how they work. Feel free to experiment with other methods of positioning your objects as well.
Run your game and see how it works. Notice that the collectables line up nicely in the corner of the screen. Furthermore, you can remove objects with a keypress and collect subsequent objects without disturbing your nicely organized on-screen inventory.
Conveniently, this on-screen inventory system can be used as the basis for many types of object management systems, such as visual health interfaces or equipment inventories.
You've looked into moving your character, detecting collisions, and collecting objects. These are all essential components of our game. As we progress towards making our first level, we need to dive into some more sophisticated code. Rather than placing all of our game's objects by hand, we are going to use our code to help create many copies of objects rapidly and efficiently. This will involve the use of loops that help us write code a single time, yet execute it many times over.
Your challenge is to spawn several copies of our collectable object in the game world. Once you are familiar with cloning and positioning one object in your code, you will be able to generate unique levels using just a few different objects.
Open your challenge project. Find the CollectableSpawn object in the Hierarchy. This object has the Spawn script attached. Your responsibility is to code the Spawn script such that you can clone many copies of the collectable object in the game world.
Open the Spawn script. You are provided with empty Update() and SpawnObjects() functions. You need to complete this script to meet the following challenge requirements:
1. Each spawn should be cloned from a prefab.
2. Each spawn should have a randomly generated position that falls within the bounds of the screen.
3. Each spawn should be organized under a common parent object.
4. Apply both a for loop and a while loop in your code. Start by using a for loop to spawn your objects. Once it is working, write a while loop that has an identical effect.
Once your project is complete, you should be able to run your game and spawn numerous objects on the screen. Here is a demonstration. Use these requirements to think through the logic behind your system. Be sure to leverage the hints in this lesson, since many new coding concepts are introduced.
A prefab is a special type of object in Unity. We can arrange a complete GameObject with all of its various settings and components a single time, then turn it into a prefab. With a prefab, we can easily create copies of our object, update its properties, and reuse it many times over throughout our game. Furthermore, any time a prefab is modified, all of its existing copies are automatically updated for us. This makes things quite convenient, since we often make frequent use of a limited set of objects to produce our game world.
Open your challenge project. Look to the Project window. Find the Assets > Prefabs folder. This is where all of our game's prefabs are stored. For example, notice that there is a prefab for our Collectable object. This is the same object we've been used to explore collisions and an inventory system in previous challenges.
You can drag any number of prefabs into the Scene window to add them into your game. After, they also appear in the Hierarchy. In the Inspector, note that they all have the same properties as the prefab.
Try making your own prefab now. Select GameObject > Create Empty from the menu to add a new GameObject to the Hierarchy window. In the Inspector, you can rename this object and add any components that you like using the Add Component menu. Afterwards, drag it into your Prefab folder. It is now a prefab that can easily be cloned and added to your scene.
While it is nice to be able to drag and drop prefabs into our scene, especially when designing levels by hand, we can do even more with our prefabs using code. We will explore how to spawn many copies of prefabs in this challenge.
An interesting feature in Unity involves how some public variables are represented in the Unity editor. Many types of variables, but not all of them, become visible in the Unity Inspector window whenever they are made public. These values can be edited in the Inspector while being used from inside the code of our scripts.
For example, look at the Player object. Inside the UserMove script, the speed integer variable is visible and editable from the Inspector window. Right now, the speed is set to 256, representing 256 pixels per second. Hence, Lana moves across our entire 1024 pixel screen in about 4 seconds. If we lower the speed, she will move slower. If we raise the speed, she will move faster. Try changing the value and running the game now.
Another interesting thing is that this value remains editable even while the game is running. If you keep your eye on the Inspector window and run the game, you can change the value of the speed variable in real time. This helps you test potential values of the variable to see what impact it has on your game. That's quite handy for balancing your game's design to produce a better player experience. However, note that any changes you made while running the game are lost once you stop running the game. Thus, if you find a value you like in real time, be sure to record it and update the variable afterwards.
Our current challenge gives us an opportunity to test out this feature. Recall that our collectable objects are all based on a single prefab. To be able to spawn a prefab with our code, we must be able to access it from within our script. How can we do it?
Open the Spawn script in your code editor. Create a public GameObject variable to represent your prefab. Since you made your variable public, it will show up in the Unity Inspector. Now, you can click on the variable to assign any one of your prefabs to it. In this case, select the Collectable prefab.
At this point, the prefab variable in your Spawn script has been set equal to the Collectable prefab in your project. Therefore, you can use your prefab variable to clone collectable objects.
Editing a public variable from the Unity Inspector is convenient in a few cases, such as when we want to reference prefabs or fine-tune our design. However, note that not all data types appear in the Inspector, even if they are made public. This is also a feature unique to Unity that may not be available in other game engines. Furthermore, we can always initialize our variables from inside our code, as we have done before.
Now that the prefab variable inside your Spawn script references the Collectable prefab in your Unity project, you can proceed to clone collectable objects. To make a clone from a prefab, use the Instantiate() function. The Instantiate() function accepts an argument that represents the object we want to clone. In this case, we want to clone our prefab variable, so we pass it into the Instantiate() function.
Furthermore, something we haven't yet discussed is that functions not only accept information, but sometimes return information as well. Instantiate() returns to us an exact copy of our prefab. Since information is being returned to us, we have to be prepared to do something with it. Hence, we can set up a GameObject variable to store the cloned object that the Instantiate() function returns to us.
Imagine two people playing catch with a ball. Sending an argument into a function is like throwing the ball to the other player. Receiving returned information is like getting the ball back. We send our prefab to the Instantiate() function and it sends a cloned copy back to us.
It is recommended that you place this sample code in your Spawn script's SpawnObjects() function. Once instantiated, the cloned object is added to our game world. After we have stored the clone in a variable in our code, we can modify it just like any other object. For instance, we can set its position or check it for collisions.
Most games involve some degree of chance, luck, or randomness. Therefore, generating random numbers is a common task for game developers. Unity provides us with a convenient Random.Range() function for generating random values. This function accepts two arguments that represent the minimum and maximum. It returns a value within the specified range. Here are some example uses of the Random.Range() function.
Note that we can use either integer or float values with Random.Range(). If we use float values, the minimum and maximum are inclusive, meaning they themselves can be selected. However, if we use integer values, only the minimum is inclusive, meaning that the maximum is excluded.
In this challenge, we can use random numbers to place our spawned objects at different positions in the game world. Recall that we used Instantiate() to create a clone of our collectable prefab. You can retrieve the clone's position Vector3 from its Transform component and then generate new x and y position values. You have calculated the positions of objects several times already and should be familiar with this process. Use Random.Range() this time to position your cloned object on screen. Remember that you want to factor in things such as the size of the object, the size of the screen, and the differences between pixel and unit coordinates, to ensure your object remains visible to the player. You may also want to test your calculations step by step to debug how your code works in the game world.
If you take a bunch of prefabs and drag them into your game world, you will quickly notice that our Hierarchy window becomes messy. Imagine creating a level where you used 10s or 100s of these objects. Thankfully, we can use parenting to better organize our objects in Unity. When we give an object a parent in Unity, it becomes a child. All children of the same parent are organized nicely under their common parent.
To assign an object to a parent, you set its Transform's parent property equal to the Transform of the intended parent object. By the way, not only does parenting help keep our objects organized, but it also helps us to manage them in our code. For instance, we can move or destroy a parent object to affect every one of its child objects at once, rather than handling them individually. You will take advantage of parenting opportunities like this in the future.
In this challenge, you want to set the parent of your cloned objects. The Spawn script is attached to the CollectableSpawn object in your Unity project. Every time you clone a collectable in your code, you should assign the CollectableSpawn object as its parent. This ensures that all of your collectables are organized under a common parent.
At this point in the challenge, you have written all of the necessary code in your SpawnObjects() function to clone, position, and parent a single object. Here is an example of what your SpawnObjects() function might look like.
Again, this code only spawns a single object each time it is run. However, you can expand your code using loops. This will allow you to spawn as many objects as you like each time your code is run. Using a loop is a fundamental technique that allows us to rapidly repeat specific code many times over. We will consider two common loops, including the for loop and the while loop.
The for loop has the following structure. It begins with the for keyword. Inside parentheses are 3 major parts, all separated by semicolons: the iteration variable, condition, and increment. An iteration variable must be declared to give our loop a starting point. It is common to name this variable with the letter i or j. The condition must evaluate to true for the loop to continue running. Whenever the condition is false, the loop stops running. The increment increases or decreases the value of the iteration variable. Hence, each time the loop runs, the iteration variable changes, which eventually causes the condition to be false and the loop to end. After the parentheses, we place the code that forms the body of our loop inside curly brackets. This code is executed each time the loops runs.
Take a look at this example loop. Can you predict what will be printed to the Console window? Here, the iteration variable i is given a value of 0. The condition asks whether i is less than 3. The increment increases the value of i by 1. The code's body prints the value of i to the Console window. Initially, the value of i is 0, which is less than 3, so 0 is printed. Subsequently, the loop runs again to print 1, followed by 2. Eventually, i is equal to 3, which makes the condition false, so the loop ends and does not print anything else.
Note that for loops can be written in reverse as well. In this case, we have a similar loop. However, the value of j starts at 3, the condition asks whether j is greater than 0, and the increment decreases the value of j each time the loop is run. This is perfectly valid structure. The only difference is that we are decreasing our iteration variable over time instead of increasing it. See if you can reason out what gets printed to the Console window by this loop. Try putting it inside your project to test the result.
As you can see, these examples make use of new operators, perhaps smooth operators. Plus-plus is the increment operator. If you place it after a variable, it will add 1 to that variable. Minus-minus is the decrement operator. If you place it after a variable, it will subtract 1 from that variable. These are commonly used inside loops. However, you can use them everywhere in your code. For instance, instead of writing x = x + 1, you could simply write x++.
Try writing a for loop inside your Spawn script's SpawnObjects() function. The existing code, which only handles a single object, should be placed in the body of your loop. Then, you only need to apply the for loop structure to spawn however many collectable objects you want in the game world. Test and retest your project as you work to make sure it is functioning properly. Then proceed to learn about another kind of loop.
The while loop has the following structure. After the while keyword, a condition is placed inside parentheses and followed with brackets. Any code inside the brackets is executed each time the loop is run. As long as the condition is true, the loop continues to run. When the condition becomes false, the loop ends.
Note that a while loop can be functionally identical to the for loop, even though it has a different structure. A for loop has an iteration variable and increment phase that clearly help us define when the condition is false. The while loop does not have these elements in its structure. Therefore, we must define our own way to ensure the loop ends. If we don't, we arrive in a situation called an infinite loop, which essentially means we create a loop that never ends. Thus, it eventually consumes all of our computing power and causes our game to crash. We never want to write an infinite loop in our code.
Here's one example way that we can ensure a while loop ends. We create our own counter variable, such as an integer. Our condition checks the value of the counter variable. Each time the loop runs, we increment this counter variable. Thus, we ultimately ensure that our loop ends. This approach is quite similar to the for loop. We simply created our own iteration variable and increment.
Another way to ensure a while loop ends is to use a Boolean flag, like so. We set the flag to false and check it inside our condition. Somewhere in the body of our loop, we look for a condition that want to end our loop. At that point, we switch our flag to true, thus making the condition false and ending the loop.
These are just a few examples of loops. You can use loops to make a variety of complex systems in the future. At this point, you should be able to write a loop in your Spawn script's SpawnObjects() function that places a variety of objects in your game. Try using both a for loop and a while loop in your code. You should be able to run your game and verify that the number of objects you told your loop to produce are indeed added to the game whenever the SpawnObjects() function is called.
This challenge looked at spawning many objects through a process of cloning prefabs, randomizing positions, parenting, and looping. Let's revisit the requirements.
Open the Spawn script. Note that two global variables are present. The prefab GameObject stores the prefab that we want to clone. The numSpawn integer helps us control how many times our loops run, thereby controlling how many clones are made. Both of these variables are public and are assigned values in the Inspector window. The prefab variable stores our Collectable prefab, whereas the numSpawn variable is given a value of 10.
Meanwhile, the Update() function contains code that should look familiar to you by now. A key press is checked for. If found, the SpawnObjects() function is called. Remember our earlier discussion of making things easier on ourselves when debugging our code? This is one of those times. When we run our game, we can press a key as many times as we like to spawn more objects. This helps us rapidly test our code without having to repeatedly start and stop the game.
Everything else takes place in the SpawnObjects() function. We begin by calculating the size of our object and screen. These calculations leverage our Constants script and are converted from pixels into world units. We'll use them to position our spawned objects.
Next comes our for loop. We initialize an iteration variable to zero, check whether it is less than our numSpawn variable in the condition, and increase it by 1 in the increment. Since our numSpawn variable is set to 10, we know that this loop will run 10 total times.
In the body of our loop, we use Instantiate() to create a clone of our prefab and store it in a local variable. Two float values are used to store the spawn's x and y position. On the x axis, we use Random.Range() with a minimum of the left edge of the screen and a maximum of the right edge. Similarly, on the y axis, we use Random.Range() with a minimum of the bottom edge of the screen and a maximum of the top edge. In all cases, these values take the center origin of our objects into account and are in world units. Hence, we are certain to produce a position that is inside the boundaries of our screen. Once calculated, we create a Vector3 variable named randPos and set it equal to the current position of the spawn. Then, we update the randPos x and y values to match our calculated position. To move the spawn on screen, we set its position equal to randPos. Our final step is to give the object a parent. To do so, we set its transform equal to the transform of the object to which our script is attached. In other words, we take our cloned spawn and make it a child of the CollectableSpawn object in our scene.
Note that the code inside the body of our loop only handles a single spawn. As the loop completes several cycles, we end up repeating our code to spawn many objects.
As an alternative, let's consider how you could have used a while loop instead. We could set up a counter variable of the integer type named totalSpawns. In the condition of the while loop, we ask whether totalSpawns is less than numSpawn. From there, the body code is identical to our for loop until the very end, at which point we increment our counter variable. This ensures that our while loop ends and produces the same result as the aforementioned for loop.
This was an exciting lesson. Not only did you learn how to spawn objects in a game world, but you also practiced using loops. These are essential techniques that you will apply over and over again as a game developer. Conveniently, your Spawn script can be reused to create many other types of objects just by assigning a different prefab variable. For instance, you can spawn Sippy Cups, Penguins, Platforms, or even Lanas all over the screen!
In this challenge, you will finally create your own scripts from scratch and write your own functions while implementing an interesting blink effect in your game. In our game, the player is represented by Lana and the obstacle is represented by the penguin. Lana wants to avoid colliding with the penguin. However, when there is a collision, we want to provide feedback to the player that lets her know what happened. Therefore, we can use a blink effect similar to what you may have seen in many classic 2D games. When our character collides with an obstacle, we rapidly alternate the transparency of our character for a few seconds. During that time, we give the player a chance to recover by not triggering any additional obstacle collisions. However, after a few seconds, the player returns to normal and the blink effect can be triggered once again. Here are the challenge requirements that will guide you towards implementing the blink effect:
1. Create a UserBlink script that manages the character's blink effect.
2. Use an Enum to define the possible blink states.
3. Use an accessor to allow the blink state to be referenced by other scripts.
4. Write one or more functions to manage the blink state. Write at least one function that accepts an argument.
5. Once the blink effect is triggered, the character alternates between visible and invisible for a specified duration. While active, the blink effect may not be triggered. Once complete, the blink effect may be triggered again.
6. Create an Obstacle script that detects a collision between the character and an obstacle.
7. When the character collides with an obstacle, trigger the blink effect.
In the challenge project, you will find a Player object that you can attach your UserBlink script to. Similarly, the Obstacle object should carry your Obstacle script. Note that the obstacle has been placed just in front of our player on screen. This makes it easy to test our blink effect rapidly when we run the game.
Interestingly, you already know most of the technical details necesary to implement this system. For example, you can detect collisions between objects, create time-based abilities, and modify the visibility of objects. Therefore, you will want to leverage what you already have done in previous challenges. Meanwhile, you are applying some overarching organizational techniques for the first time, such as functions and enums. Therefore, you will also want to leverage the hints as you work through this implementation.
Now is a great time to kick things off by creating a process map. For instance, you can use a process map to clearly describe the different states of your blink effect and what events cause them to change.
In the Project window, navigate to your Assets > Scripts folder. To create a new script, select Assets > Create > C# script from the menu. Afterwards, the script is automatically added to your Scripts folder where it can be renamed.
Subsequently, you can attach your script to an object. Select the object from the Hierarchy window. In the Inspector, click on the Add Component button. Choose Scripts and then select the script that you want to add. After, you can see that the script is attached to the object. From here, you can open the script in your code editor and get to work.
For this challenge, remember to create UserBlink and Obstacle scripts. Attach UserBlink to the Player object and Obstacle to the Obstacle object.
An enum, or enumeration, is a set of named constant values. We can use enums to help manage the state of our game and its objects in our code.
Create an enum using the enum keyword. Follow with a valid variable name. Inside curly brackets, place additional names that represent various states. By default, each name is associated with an integer value. The values begin with 0 and are incremented sequentially thereafter. However, you can assign a different starting value or a specific unique value to each name.
When using an enum for states, it is useful to create a variable in your script that describes the current state of the system. To do so, create a variable whose type matches the name of the enum. While your system runs, you can update this variable to keep track of the system's state. To do so, set it equal to the name of the enum - dot - the name of the state. As you can see, one of the benefits of using an enum is that our code uses clear and logical names. This makes it easy to keep track of and compare states from anywhere in our code.
Here is an example enum for the state of a light switch, which can be on or off. We can track the current state with a variable. Later on in our code we might update our state under certain conditions. For instance, we can check for a key press and, based on the current state, either turn the light on or off.
As you work through this challenge, think about what possible states your blink effect needs and represent them using an enum.
You have used functions many times before. This is your opportunity to begin creating your own functions. Data types, variables, collections, loops, and now functions - from this point forward, nothing can stop you.
A function contains specific code that is executed on demand. Technically, we could put the entire code of our game into a single function. However, it would be nearly impossible for us to understand and modify that much disorganized code all at once. Therefore, functions help us break up our code into smaller pieces. This makes our code easier to understand, modify, and reuse. Every function you write should have a specific purpose. For example, we have used a CheckInput() function to check for key presses. We have used CheckCollisions() to check for collisions between two objects.
As you are aware, functions can be called by writing their name followed by parentheses. Each time a function is called, all of its code is executed. Rather than having to write this code over and over in our game, we simply write it once in a function. Thereafter, we call the function any time we want its code to be executed.
Thus, functions help us logically organize and make efficient use of our code. You should look for opportunities to break your code into specific responsibilities, each of which may become a function.
Here is the basic function structure. Include an access level, return type, name, and parentheses. The arguments, if any exist, are defined inside the parentheses. Follow with curly brackets that contain the body code. Most of these elements are already familiar to you, particularly from creating variables.
Note the return type however. In the most basic functions, this will be set to void. Void means that the function does not return anything. Similarly, the most basic function has no arguments. We will consider other options like these at a later time.
Here is an example function named PrintAMessage() that prints a message to the Console window. Recall that we must call a function for its code to be executed. Hence, to print a message, we type PrintAMessage() followed by parentheses in our code.
Think about your current challenge and the possible states of your blink effect. How can you represent those states as functions? What functions can you use to modify those states?
You've used functions that accept arguments before, such as Destroy(), which accepts an object that we want to remove from our game. Similarly, Random.Range() accepts two arguments, a minimum and maximum, that are used to generate a random number.
An argument represents information that is provided to a function. After receiving an argument, the function can make use of it. Now, it's time to write your own functions that accept arguments.
In the basic function structure, nothing is inside the parentheses. This means that the function accepts no arguments. However, we can allow the function to accept arguments by putting information inside the parentheses. To define an argument in the function declaration, put a data type and a name inside the parentheses. You can use any valid data type and name.
This time, our PrintAMessage() function accepts a string value with a name of theMessage. It takes that argument and prints it. Recall our earlier discussion of pseudonyms. When we pass information into a function as an argument, it is thereafter refered to by its pseudonym in the function's body code. It doesn't matter if the information had a differnet name before it was passed into the function.
You can add any number of arguments to a function by separating them with commas. This revised PrintAMessage() function accepts a message string and an integer indicating how many times to print it. It uses a loop to print the message the indicated number of times.
To call a function that accepts argument, type its name followed by parentheses. Inside the parentheses, you must place information that matches the quantity and type of arguments defined for the function. For example, if the function accepts a string and an integer, you must provide a string and integer.
Another key factor to consider when using function arguments is how the information is passed. Some values are "passed by value" whereas others are "passed by reference."
When information is passed by value, only the raw data itself is provided to the function. Most promitive data types are passed this way. For instance, if you pass a variable with an integer value of 0 into a function, the function only receives a value of 0. Whatever the function does with that value has no impact on the original variable passed in.
In contrast, when information is passed by reference, the function is pointed to the exact object that was provided. Most composite data types are passed this way. For example, if you pass your Player object into a function, the function has access to the actual Player object in your game. Anything you modify inside your function with directly modify the Player object itself.
This is an important distinction. Sometimes, you only want to reference values and use them to perform calculations without directly modifying any objects in your game. At other times, you want to modify specific objects in your game through your function code. You should always be aware of whether your arguments are being passed by value or reference. This will help you maintain control over your code and avoid any unexpected circumstances from arising.
Work through this example to check your understanding of arguments. You can run this code in your project to test out how it works. That's all you need to know about function arguments. Think about how you can use arguments in the context of this challenge. What values or objects might you need to pass between different functions in your code in order to update the state of your blink effect?
Accessors define how we get (or read) and set (or write) a variable's value. Therefore, they are also called getters and setters. Accessors are defined by default for us in C# whenever we create a variable. We normally don't worry about them, although we constantly make use of them. Here is an example.
The set function looks like this. It simply allows us to set a value for our variable. Behind the scenes, it gets called every time we set the value of our variable. Meanwhile, the get function looks like this. It simply allows us to retrieve the value of our variable. Again, it gets called every time we use our value in our code. However, we have the ability to define our own accessors. We might do this when we want something special to happen when we get or set the value of our variable or when we want a private variable to be accessed by external scripts under certain circumstances.
For example, suppose we want to keep track of how many times a variable's value has been set. We could create a counter variable and increment its value inside our setter. Thus, every time the value of the variable is set, the counter is incremented as well.
Suppose we have a private variable. We don't want to make it fully public, because we don't want any other scripts to be able to set its value. However, we do want other scripts to be able to check the value of the variable. To accomplish this, we can create a public getter for the private variable. Since we only included a getter, other scripts can retrieve the value of the variable, but they cannot set it.
In the current challenge, you may want to do something like this. You may have defined a private state variable for your blink effect in the UserBlink script. Meanwhile, your Obstacle script might need to make reference to that state in order to trigger the blink effect. You don't necessarily want the Obstacle script to be able to modify the state, but you probably want it to be able to know the state. Hence, you could place a public getter in your UserBlink script that allows this to happen.
Every script that you write and system that you implement ultimately becomes part of your code base. Your code base refers to the body of knowledge and the tangible implantations that you possess as a coder. As your experience grows, so does your code base. You always want to leverage what is already in your code base as you work on new implementations. This challenge is a great opportunity to leverage what you have already done. Here are some examples.
The Obstacle script needs to detect a collision and trigger the blink state. Previously, you wrote a Collectable script that detected a collision between Lana and the Sippy Cup. You can leverage this same code to detect a collision between Lana and the Penguin. The only difference will be what happens once the collision is detected.
Furthermore, your UserBlink script implements a time-based system that adjusts the visibility of your character. Back when you created your UserInvis script, not only did you implement a time-based system, but you also made your character invisible. Hence, you again have a great reference from the past that you can incorporate into your current challenge.
Be sure to leverage your Collectable and UserInvis scripts in this challenge. Moreover, always take your code base into consideration at the start of every implementation.
Utilizing your code base is an excellent way to accelerate your work. Instead of writing everything from scratch every time, you can begin with a solid foundation from what you have already done. Things may be added and modified thereafter, but with greater efficiency thanks to your code base.
In this challenge, you had the opportunity to create your own scripts and functions in order to implement a blink effect when the character collides with an obstacle. Let's revisit the challenge requirements.
We'll review the solution now. Recall that the complete solution has been provided to you and can be referenced at your convenience. The first step was to create two scripts. UserBlink should be attached to the Player object, while Obstacle should be attached to the Obstacle object. Inside the UserBlink script, several global variables are established. Note that an enum lists our possible blink states as on, off, start, and stop. Meanwhile, a variable of the blink state type tracks the current state of our system.
Inside Start(), we retrieve the SpriteRenderer component attached to our Player object. This will be used to set the visibility of our character on screen, which ultimately makes the blink effect apparent.
We use the UpdateAlpha() function to modify the visbility of our character on screen. This function accepts two arguments, one that represents the SpriteRenderer we want to modify and one that indicates the desired alpha level. The alpha level represents how transparent or opaque an object is. It ranges in value from 0, which is invisible, to 1, which is fully visible. Inside UpdateAlpha(), we retrieve the color property from the SpriteRenderer. The color.a, or alpha property, is set equal to the alpha value passed into the function. Then, to update the visibility of our character on screen, we set the SpriteRenderer's color property equal to our calculated color.
Note that this is a good example of passing arguments by value and by reference. The SpriteRenderer that is passed by reference, meaning that we update the actual object provided to the function. Hence, when we pass the Player object's SpriteRenderer into this function, we are updating the actual Player object in our game world. On the other hand, the alpha argument is passed by value. It is merely a number that has no association with a specific object in our game world.
Note that our script has a separate function for triggering each possible state. These are BlinkStart(), BlinkStop(), BlinkOn(), and BlinkOff(). They directly relate to the system that was designed to manage the blink effect. Start refers to when the effect is triggered and stop refers to when it ends. On refers to the moment when the character is visible on screen and off refers to the moment the character is invisible. Thus, we start the effect and rapidly alternate between the on and off states to make the character blink. After the full duration is complete, we stop the effect.
In BlinkStart(), we first ensure that we are not already in the Start state. That's because once our blink effect is triggered, we don't allow it to be triggered again until its full duration is complete. Every one of our state functions makes a similar check for this same reason. Afterwards, we update the state, reset the start time so we can calculate the duration of the effect, and call BlinkOff().
In BlinkOff(), assuming the state is not already off, we set the state to off, update the visibility of the character, and update the interval start time so we can calculate when to rapidly switch to the on state.
In BlinkOn(), assuming the state is not already on, we set the state to on, update the visibility of the character, and update the interval start time so we can calculate when to rapidly switch to the off state.
In BlinkStop(), assuming the effect is not already stopped, we update the state and reset the character's visibility to the default, which is fully visible on screen. At that point, our effect is over and the state will not changed until the next time it is triggered.
Inside Update(), we simply ask whether the current blink state is stop. If it is anything other than stop, we call our Blink() function. In other words, the script does nothing when the blink state is stopped, since this script is only responsible for our blink effect. On the other hand, when the blink effect is active, our script is hard at work.
The conditions governing our blink states are handled in the Blink() function. We begin by calculating the total duration of our blink effect by taking the current time minus the time at which it started. If we have exceeded the allotted duration, we stop the blink effect immediately. Otherwise, we can assume the blink effect is underway. Now, we calculate the interval duration, which represents how long it has been since we switched between the on and off states. If the allotted interval duration has been exceeded, we know we need to switch between on and off. Thus, if we are off, we turn on. If we are on, we turn off. That's all there is to it. Our Blink() function checks how long we have been in certain states and helps us switch between them whenever necessary.
That's the complete Blink script. As a reminder, notice that this script is a more advanced version of the UserInvis script you made for the invisibility challenge. You should be able to notice some similarities between these scripts.
At the very end of the script, we make a public getter for our _currentState variable. This allows the Obstacle script to access the current state of our UserBlink script.
Speaking of the Obstacle script, it is nearly identical to the Collectable script you created before. For a full description of the collision code, see the solution video from the collectable collision challenge. Other than that, only a few names have been changed to protect the innocent. In addition, once a collision is detected, we retrieve the UserBlink script from our Player object. We check the blink state to ensure that the effect is not already active. If necessary, we start the blink effect. Hence, if Lana collides with a penguin at any time and she is not already blinking, she will begin blinking to tell our player that the collision has occurred.
With your UserBlink and Obstacle scripts complete, you can run your game and see it in action. Notice that Lana starts blinking as soon as she collides with the penguin. While she is blinking, any collisions are ignored until the full duration is complete. Afterwards, she can collide with the penguin and start blinking again.
You made some important progress in this challenge. You are now fully capable of creating your own scripts and functions, as well as managing states in your code. Beyond that, you implemented a nice effect that makes your game more professional and improves the player experience. In the next challenge, you will expand your game by introducing the critical character ability of jumping. This is sure to put your coding skills to the test.
It's time to really bring your character to life and implement the primary mechanic of our game. That is jumping. In our game, Lana is going to jump from platform to platform collecting sippy cups and avoiding penguins. We've all been there. Coding an effective jump is perhaps the single most important part of our game in terms of delivering a quality player experience.
When the player presses a key, you want Lana to jump into the air for a limited amount of time, then fall back down. This requires you to check for user input, manage states, and modify the y position of our character. Many of these things are familiar to you from past challenges. However, you have not yet embarked on a challenge this big or exactly like this one. It's a great opportunity to apply your skills.
Furthermore, you will implement a pixel-perfect solution, which ensures our game world operates in 2D behind the scenes just as great as it looks on screen. This is no easy task, but once you have it figured out, you'll have it in your code base for future use.
Here are the challenge requirements:
1. Create a UserJump script that manages the character's ability to jump.
2. Use an Enum to define the possible jump states.
3. Use an accessor to allow the jump state to be referenced by other scripts.
4. Check for a key press that causes the character to jump.
5. Once the character jumps, she should rise for a specified duration. Afterwards, she should fall.
6. Write one or more functions to manage the jump state. Use arguments and return values as needed.
7. Create a pixel perfect implementation.
In the challenge project, you will find a Player object that you can attach your UserJump script to. Beyond that, it's all up to you. Well, not entirely. You also have also built up a code base already that you can leverage. Moreover, the hints are here to help.
To get started, diagram the logic behind your jump system. The player presses a key, the character rises, the character falls. How can you represent your logical implementation of this system in terms of user input, states, and functions?
One special ability of functions is that they can return information back to us. In fact, you've already used functions that do this. For instance, Instantiate() provides a cloned object to us, which we can subsequently use in our code. Thus, Instantiate() is an example of a function that returns information.
The most basic function structure does not return any information. Thus, it has a return type of void. However, we can designate a specific primitive or composite data type as the return type. This allows a function to return information of that type. For example, we could specify bool or GameObject as the return type. This means our function should return data of that type.
To return information, a function's body code must contain the return keyword, followed by a value that matches the specified type. Typically, the return statement is found near the end of a function. That's because once a function returns a value, it ends immediately. No code that follows the return statement is executed. Hence, think of returning a value as the very last thing a function does.
When information is returned, it is provided to the caller of the function. Thus, wherever you call the function in your code is where the returned information is provided.
You need to be prepared to utilize this information. There are several ways to do this. One common way is to store the returned information in a variable. Thereafter, the variable can be used in your code.
Another way is to use the returned information right away inside a conditional statement. The returned information can help you make a decision in a specific condition, which allows your code to proceed.
There are different options for using returned information in your code which suit different needs. The key is that you should always have some way to utilize returned information in your code. Otherwise, there is no reason for your function to return information in the first place. Functions with return values are especially useful when you need to provide information back to the caller. Try using return functions in your code.
Not only can functions accept arguments and return information, but they can do both. You can mix and match arguments and return values when designing your functions.
In fact, Instantiate() is a mixed function. Recall that it accepts an object as an argument and returns a copy back to us. Likewise, Random.Range() accepts two arguments and returns a random number to us. Both of these functions, and many others, use arguments and return values. Here are some example mixed functions.
The ability for functions to accept information, perform calculations on it, and then return it provides you with a tremendous amount of flexibility in how you implement your code. The purpose of functions is to help us organize our code and execute it efficiently. You should make full use of arguments, return values, and combinations of these features to write better code. This is a learning process that takes practice, so start working with different kinds of functions in your code starting now.
Remember our earlier discussion of frame rates and the game loop? There is a phenomenon common to all games. Every individual player has a different computer with different hardware. The frame rate is determined by two things: the power of the player's hardware and the power required to run our software. As developers, we have control over how demanding our software is, but we cannot control the player's hardware.
Therefore, we aim to achieve frame-rate independence when implementing game systems. When our game is frame-rate independent, it means that it will run the same for every player regardless of what hardware is used. Hence, we eliminate the piece of the equation that we have no control over.
A key way to achieve frame-rate independence is by using time-based systems. For example, you have already implemented a time-based invisibility system and blink system. Time-based systems work because timing is consistent. 1 second is always 1 second. In contrast, frames are not consistent. A single frame could last 1/60 second, 1/30 second, or longer. If we base our system on time, rather than frames, we can ensure consistency.
To achieve frame-rate independence in Unity, we use Time.deltaTime. Time.deltaTime stores how much time has elapsed since the last frame. When we multiply a value by Time.deltaTime, we eliminate the frame rate as a factor in our calculation. This is a universal technique for achieving frame-rate independence in Unity.
Time.deltaTime is often used when calculating movement as part of the game loop. Take a look at the Move() function in your UserMove() script. Each frame the Move() function is called, we calculate the character's change in movement as speed times direction times Time.deltaTime. Normally, you might think of movement as having a speed and direction. This is logical. However, since we want the added bonus of frame-rate independence, we multiply by Time.deltaTime. That's it.
For this challenge, make sure you apply Time.deltaTime to achieve frame-rate independence in your UserJump script. As your character is rising or falling frame by frame, you can use Time.deltaTime to create a time-based system that is independent of the frame rate.
As you are aware, the art and design of our game world are in pixels. Meanwhile, our code conforms to the game engine's measurement of units. You are already familiar with converting between pixels and units.
One of the reasons that we are going through the trouble of these conversions is that we are creating a pixel perfect game world. That basically means that we are trying to make our game look and run well in terms of pixels despite using an engine that is not based on pixels.
Part of this occurs on the art and aesthetics side of things. This is a coding course, so you don't have to worry about this. Just in case you are curious, some things we have done to make our game look better are: sized our sprites and our game world at powers of 2, modified Unity's import settings so they don't ruin our images, and applied a 2D orthographic camera. Again, you don't need to worry about these sorts of things right now, but know that there are some steps to take to make a nice-looking 2D pixel-based game if you go on to create one in the future.
What you do need to be concerned with is the code involved in making our world pixel perfect. All positioning in our game must be handled according to the game engine. Unity stores positions as float values. Float values suffer from a phenomenon known as floating-point error. Essentially, a float can never be guaranteed to precisely represent a value. Instead, a float is an extremely close approximation of a value. Most of the time in game development, floating point error is inconsequential and easily avoided. However, when creating a pixel-perfect 2D world, we must take specific steps to eliminate it.
Unlike floats, integers always have an exact value. Integers also align nicely to our pixel-based world. For instance, our screen is 1024 pixels wide. We can use an integer value to represent every one of those pixels. We could choose to confine objects to exact pixel positions using integers. However, Unity requires that our objects be positioned according to float values. Clearly, some negotiation is in order.
Here's how we handle this situation. We separate the logic of many events in our game, such as movement and collisions, from the positioning requirements of our game engine. This means that the structure of our code will follow a specific pattern. That pattern entails retrieving an object's position in units, converting it to pixels, handling all of our game's events in pixels, then converting the position back into units at the end. By following this process, we ensure that our game's logic is executed in pixels, but the ultimate positions of our objects are handled in units.
As an example, our UserMove script already applies this pattern to move our character. Look at the Update() function. We begin by retrieving the object's position in units. By the way, Vector2 is just like Vector3, but without the z coordinate. Meanwhile, Gameobject dot transform dot position is a shorthand way of retrieving an object's position. Next, the position is converted from units to pixels. Notice that this conversion requires multiple steps. First, a float version of the pixel position is stored by multiplying the position in units by our conversion ratio. Second, an integer version of the pixel position is created using the Mathf.RoundToInt() function. This takes our float value and rounds it off to an integer. Third, a variable named remainderX stores the difference between the float and integer positions we just calculated. It uses Mathf.Abs() to take the absolute value of the difference. Floats store decimal values, whereas integers are whole numbers. When we convert a float value into an integer, some information is lost. We want to save the lost information so it can be added back into our final position calculation. That's what the remainderX variable is for. At this point, we have a pixel representation of our x position stored in the iUserPixelX variable. We can use this position to calculate every kind of event in our game strictly according to our pixel perfect world. For instance, you can see that our UserMove script calls the CheckInput() and Move() functions to update the object's x position according to user input. Once all of our in-game events are settled, we ultimately need to convert our object's position back into units so it can be positioned according to the game engine's requirements. Since the remainder was stored as an absolute value, it is multiplied by the direction variable. Thus, no matter which way our character is moving, our remainder has the right sign. We make sure to take the remainder and add it back to our pixel position. This ensures that we don't lose any information in translation. Then, we divide by our conversion ratio to convert the position into units. Last, we set the object's position equal to our final calculated position in world units.
Try to work through the code in your UserMove script. In many ways, it resembles the UserBlink script you created. The major difference is the implementation of pixel perfect calculations in Update(). Remember that this all follows a pattern. Retrieve a position in units, convert the position to pixels, handle any events, and convert the position back to units. We will use this formula many times throughout our game to implement a pixel perfect world.
Conveniently, the UserMove script provides an excellent template for your Jump script. UserMove handles movement along the x axis, whereas Jump involves the y axis. You should be able to adapt the code in your UserMove script to suit the needs of your Jump script.
In this challenge, you gave our character the all-important ability to jump. Recall that the complete solution has been provided to you and can be referenced at your convenience. Here are the challenge requirements.
Inside our project, we create a script called UserJump and attach it to the Player object.
Inside the UserJump script, several global variables are established. These define the parameters and possible states for our jump ability. Note that a remainderY variable is created. Similar to remainderX from our UserMove script, this variable is used to assist with keeping our world pixel perfect.
At the bottom of our script, an accessor allows the jump state to be accessed, but not modified, by external scripts. This is similar to what we did with the UserBlink script.
Note that our script has a separate function for triggering each possible state. As we have done before, each state first checks to ensure that it is not already active. This prevents us from triggering a state once we are already in it.
JumpGround() represents when our character is not jumping at all. We set the state, update the direction to 0 to represent no y-axis movement, and begin listening for user input.
JumpRise() represents when our character is rising upwards. We set the state, update the direction to 1 to represent upward y-axis movement, and set the start time to keep track of how long the character has been rising.
JumpFall() represents when our character is falling downards. We stop listening for input to prevent the player from jumping in mid air, set the state, update the direction to negative 1 to represent downard y-axis movement, and reset the start time.
Three primary functions execute the technical aspects of our jump. These are Rise(), Fall(), and Jump().
Rise() controls the rising phase of our jump. It accepts an integer representing the character's current y position. Since our ability is time-based, it also accepts a float indicating the percentage of the allowable duration that has been completed thus far. To create a nice smooth arc for our jump, we apply acceleration. In other words, our jump ability decreases the character's speed from the maximum to the minimum as she approaches the peak of her jump, then proceeds to increase her speed towards the maximum as she falls. This gives us our character a realistic jumping motion.
We update the current speed to equal the maximum minus the percentage complete times the difference between the maximum and minimum. As the rising phase of our jump progresses, this formula gradually slows our character down from the maximum speed to the minimum. Next, we calculate the character's change in movement as a float by multiplying the speed times the direction times time.deltaTime. On our y axis, a direction of 1 represents rising, while a direction of negative 1 represents falling. Hence, this formula makes sure we are moving the character in the right direction. time.deltaTime is used for frame-rate independence. Then, Mathf.RoundToInt() is used to convert the float value into an integer that represents pixels. This integer value is used to update the position. Ultimately, our update pixel position based on movement is returned by the function. As always, we want to update our remainderY value to capture anything that might be lost when we convert from float to int. Therefore, we set it equal to the absolute value of the float minus the int. In addition, we check whether we have completed 100% of our rise phase. If so, we trigger the fall phase.
Similarly, Fall() controls the falling phase of our jump. It accepts an integer representing the character's current y position and a float indicating the percentage completed thus far. We update the current speed to equal the minimum plus the percentage complete times the difference between the maximum and minimum. This formula gradually speeds our character up as she falls from the peak of her jump, thus producing a pleasant arc. Next, we calculate the character's change in movement by multiplying speed times direction times deltaTime. Then, Mathf.RoundToInt() is used to convert the float value into an integer that represents pixels. That integer is used to calculate the character's new y position. After, remainderY is updated to capture any remainder from the float to int conversion. Last, the newly updated y position is returned so it can be used to move our character.
Jump() controls the status of our jump and figures out whether we need to rise or fall.
It accepts an integer that represents the y position of our character and stores this value in a local variable. The function only proceeds if we are not on the ground. If we're on the ground, there's no jump taking place, so there's nothing to do. Next, we calculate how long the character has been in the current state, regardless of whether we are rising or falling. This value is converted into a percentage between 0 and 1 using the Mathf.Clamp01() function. Notice that the allowed duration is divided by 2 in this calculation. Remember that our jump ability is broken into two halves: rise and fall. Meanwhile, our duration variable sets a total time limit on the jump ability. If we want to know how long each phase of the jump lasts, we must divide the duration by 2. That's what we did here. At this point, we know how long we have been in the current jump phase in percentage terms. We proceed to check the state in a switch statement. If we are rising, we set our y position equal to the value returned by the Rise() function. The Rise() function is provided with our current y position and the percentage completion of our current phase. If we are falling, we set our y position equal to the value returned by the Fall() function. The Fall() function is provided with our current y position and the percentage completion of our current phase. Whether rising or falling, we update our y position based on the character's movement over time. Ultimately, we arrive at the end of the function where the newly calculated y position is returned. Hence, we have executed a single step in our character's jump ability and modified her position. As this cycle repeats throughout the game loop, we produce movement on screen.
Inside Start(), we set the current state to Ground and _isListening to true, which means we are allowing the player to trigger the jump ability.
CheckUserInput() manages the jump state according to user input. We only proceed if our script is currently listening for input. If the character is in the ground state and the player presses a key, we initiate the jump ability by calling the JumpRise() state. On the other hand, if we're in the rise state and the key is released, we stop listening for input. These features coordinate to allow the player to trigger the jump only a single time with a key press. From there, the player is not allowed to jump again until the process is complete.
In Update(), we implement our pixel perfect system. That is, we retrieve the character's position in units, convert it to pixels, handle our jump ability in pixels, then convert the final position back into units.
1. The character's current position in world units is retrieved.
2. The y coordinate is converted to pixels and stored as a float.
3. This float is converted to an integer.
4. The remainder from the conversion is stored in remainderY.
At this point, we have a pixel representation of the character's y position stored in iUserPixelY. We check user input. After, we set iUserPixelY equal to the result of the Jump() function with the current y position provided as an argument. In other words, all of the previously discussed code that forms our jump system is executed to determine what character's new y position is. Once complete, we have the new y position stored in iUserPixelY.
From there, we take the character's y position in world units and set it equal to our pixel position converted to world units. The conversion is made by taking the pixel position, adding the remainder from previous calculations times the direction of movement to ensure the proper sign, all divided by the conversion ratio. The last step is to physically update our character's position on screen by setting its transform dot position property equal to our calculated position.
On a side note, notice that some extra code has been added for convenience and testing purposes. Right now, our game has no collisions, so Lana will continue to fall off the screen forever after jumping. We'll implement collisions later that allow Lana to land on platforms and jump again. But for now, we can simply detect when her position leaves the screen, reset her back to her original position, and reset the jump state. This helps us rapidly test our jump system.
Congratulations. Run your game and test it out. You should be able to press a key and see Lana jump up and fall down. This was quite an implementation that built the foundation for much of what the player will do in our game. In the next challenge, let's consider a small way to fine-tune the character's jump to make it even better.
Here's a mini challenge that will make your game more professional and extra cool. Right now, our character jumps for a specific amount of time. That means her jump is predictable and exactly the same every time.
A feature found in many excellent 2D platform games is the ability for the player to vary the character's jump. For instance, if we hold the jump button longer, we get a big jump, whereas if we tap it really fast, we get a small jump.
Let's implement this super jump in our game. We can do so by modifying our UserJump script. Here's what we want to see.
1. The player can trigger the jump with a key press.
2. If the player holds the key, the jump gets extended up to a certain time limit.
3. If the player lets go of the key, the extension ends.
4. If the time limit is exceeded, the extension ends.
5. After the extension ends, the jump proceeds as normal.
Here's what your game will look like after this implementation is complete. Notice that the player can tap the key quickly to create a small jump, hold the key to achieve the biggest possible jump, or anything inbetween.
To implement your super jump, you need to expand the logic of your existing code. In the previous challenge, your UserJump jump script allowed a single key press to trigger the same jump every time. This time around, the jump is also triggered by a key press. Likewise, the rise and fall can be calculated the same way. However, you are inserting an additional opportunity to extend the jump after it is triggered by holding down the key and releasing it.
Recall that we have Input functions called GetKeyDown, GetKeyUp, and GetKey, which handle key presses, releases, and holds. You will want to use all of them inside your CheckInput() function. Furthermore, since your extended jump is limited to a specific duration, you will need to keep track of how long the player has held the key. If the player releases the key before the limit is reached, you should carry on with the jump as normal. However, if the limit is exceeded, you should carry on with the jump as normal regardless of whether the player continues to hold the key.
As you can see, this will require managing the state of your jump system as well. You have to keep track of the possible user input methods and jump states to take the appropriate actions at the appropriate times.
Your challenge was to build a super jump that allows the player to control how much or how little energy our character puts into her jump. The super jump can be implemented by making just a few changes to your existing UserJump script from the previous challenge.
Open the UserJump script. Two global variables are added. MAXDURATIONEXTEND defines how long the jump may be extended, in seconds. STARTTIMEEXTEND marks the time at which the extended jump began. The STARTTIMEEXTEND variable gets set inside the JumpRise(), which is called when the jump is triggered. Regardless of whether the player chooses to extend the jump or not, we need to record when it began to determine how long it may be extended.
Inside CheckInput(), we already have a condition that checks for the jump key to be released. This covers us when the player extends the jump for a shorter duration than the maximum limit. We add another condition that checks for the jump key being held down while the player is in the rise state. Indeed, this is when the jump is being extended. Inside that condition, we calculate how long the jump has been extended by taking the current time minus the time at which the extension began. If the maximum duration is exceeded, we stop listening for input, which effectively ends the super jump, so the jump can proceed as normal. Otherwise, we update the start time.
This may seem counterintuintive. Our jump already started and we recorded the start time. Why change it now? Interestingly, this is how we create our super jump. Normally, the jump begins at a specific time and we keep track of the total duration to determine how far the character jumps. We "trick" our system by updating the start time. The system thinks the jump just began, so it will execute the full duration of the jump. As long as we keep updating the start time, the system keeps thinking that the jump just began. Once we stop updating the start time, the jump proceeds as normal. This technique is excellent for us, because we don't need to make any changes to our various functions and calculations. We found a way to insert the super jump ability into the existing system from the very start.
That's all you need to change to implement the super jump. Test your game, play around with the super jump system, and enjoy. You now have a great-looking and fun-to-use super jump built into your game.
You have created some excellent abilities for your character, such as moving, jumping, and blinking. However, up to this point, we have largely been implementing our features in isolation. It's time to bring things together and really bring our character to life. In this challenge, you will work to revise your code such that your character becomes a coherent, unified entity that is composed of these once-separated features. Here are the challenge requirements:
1. Create a UserController script that manages the character's abilities. Each script retains its individual responsibilities, such as calculating the distance moved, but the UserController tells each script when to execute its responsibilities.
2. UserController initializes the UserBlink, UserMove, and UserJump scripts. Accordingly, these scripts do not initialize themselves.
3. UserController updates the character's movement on both axes. Accordingly, the UserMove and UserJump scripts no longer update movement individually on a single axis. They still check for input and calculate movement, but only when told to do so by the UserController.
Once complete, your game will have a unified UserController script that manages all of the character's abilities. You will be able to move, jump, and blink all at once. At the same time, your scripts will maintain their independence, meaning they can easily be modified or reused in future projects.
Interestingly, this challenge is not about implementing a bunch of new features. It is about reorganizing your existing code to yield better structure and execution. That's why you'll learn about a few important fundamental coding techniques in the hints.
Coupling refers to how dependent different aspects of our code are upon one another. If our scripts are highly dependent upon one another - that is, if they reference each other heavily and cannot operate independently - we have tight coupling. On the other hand, if our scripts can operate independently without referencing one another, we have loose coupling. Generally speaking, we want loose coupling between our scripts. That's why we have separate scripts to handle different responsibilities, such as movement and jumping.
Cohesion refers to how focused our code is on specific responsibilities. If we put all of our game's code in a single script, we have low cohesion since a single script is responsible for everything. If we create scripts with specific responsibilities, such as script that only handles movement, then we have high cohesion. Generally speaking, we want high cohesion in our scripts.
Overall, we want loose coupling and high cohesion in our code. When we keep our code focused and independent, it becomes easier to understand, maintain, and reuse.
Our Unity game engine is specifically designed to support loose coupling and high cohesion. It has a component-based structure. That's what allows us to create objects and attach various abilities to them. It's easy for us to mix and match different components to create a variety of objects. Scripts, Transform, and SpriteRenderer are all example components that we have already attached to GameObjects. Hooray, Unity!
Refactoring is the process of reorganizing our code to improve its structure, without altering its functionality. New structure - same functionality. In the current challenge, you have nicely designed code that is loosely coupled and highly cohesive. However, this means that all of our scripts are focused on their individual responsibilities and do not communicate with one another. This is mostly a good thing, but it introduces a problem as well. Sometimes we do need our scripts to communicate and coordinate with one another.
One way to solve this problem is to refactor our code. We can create a single manager script whose responsibility is to coordinate between multiple independent scripts. Since each independent script is able to communicate with the manager, we solve our communication problem. Yet, since each independent remains independent of the other scripts, we retain the benefits of coupling and cohesion.
Essentially, we want to keep our UserBlink, UserMove, and UserJump scripts independent, just as they are. Yet, we also want to coordinate these scripts simultaneously to produce a fully-functional character in our game. Therefore, our UserController script will act as the manager. It communicates, coordinates, and manages our UserBlink, UserMove, and UserJump scripts.
Think logically about what your different scripts should and should not do. For example, the UserController script should be responsible for initializing each script and updating the character's movement. UserMove checks for input, calculates the character's movement, and tracks the movement state. UserJump does the same, but for jumping. UserBlink handles the states and representation of the blink effect. Each script has individual responsibilities, but the UserController brings them together to ultimately update the character over time.
To implement your UserController script, you need to rearrange a few things in your code. One step is to create a public variable for each script inside your UserController script. Remember to attach the UserController script to your Player object. Then, assign the existing scripts attached to your Player object to the corresponding variables in your UserController script. That way, your UserController script has full access to each of the scripts it manages.
Another step is to convert the Start() functions in each script into custom initialization functions. Recall that Start() is automatically run by Unity. After refactoring, we don't want this to happen. The UserController is taking control over the other scripts. Therefore, we can put a Start() function inside the UserController that calls the initialization function inside each individual script. That way, we use our UserController to control exactly when each of our scripts is initialized. Here's an example Init() function for UserBlink. Init() is our own custom name, so Unity does not automatically execute it like it does with Start(). Instead, our UserController must specifically call the UserBlink script's Init() function from inside its Start() function.
Yet another step is to merge the Update() functions from each individual script into a single Update() function inside the UserController script. Again, we don't want separate Update() functions running wild in each script. Instead, we combine them into a single Update() function inside the UserController, thus taking full control over the frame-by-frame update process for our character. For instance, UserMove updates our character's x-axis movement while UserJump updates our character's y-axis movement. It is perfectly reasonable to update both x and y movement inside UserController rather than handling them seperately in different scripts.
Note that you may need to make minor adjustments to various parts of your code, such as variable names, while you engage in the refactoring process. This is not a problem. So long as your code is readable and operational when you're done, you will have greatly improved the organization of your code. In addition, refactoring helped you maintain the benefits of coupling and cohesion in your scripts.
After refactoring your code and implementing the UserController script, you should be able to play your game and see the character move, jump, and blink.
As a reminder here, are the challenge requirements.
Your UserController script has 3 public variables - one for each script. The UserController is attached to the Player object in our scene and each script is assigned to its corresponding variable. Since the UserController is now responsible for updating the position of our character, we create a Vector2 variable named _remainderPos. Previously, we had remainderX and remainderY inside the UserMove and UserJump scripts. These variables have been removed from those scripts. Instead, when we convert our x and y positions from float to int, we will store the remainder in our remainderPos variable's x and y coordinates. This is but one example of how our UserController is taking control over certain responsibilities that used to be distributed between our individual scripts.
Continuing, the Start() function initializes the remainderPos variable. Then, it calls the Init() function within each script. Again, these scripts have had their individual Start() functions replaced by Init() functions. That's because we want the UserController to tell each script when to initialize, rather than allowing Unity to automatically start them without our permission.
Recall that the UserController is in charge of updating our character's position and telling the individual scripts when to execute their responsibilities. Accordingly, the Update() function has been removed from the UserMove and UserJump script. These are replaced by the UserController script's Update() function, which coordinates both x and y axis movement.
The UserController script's Update() function follows the same pixel-perfect process that we are already familiar with. It retrieves the character's position in world units, converts the position to pixels, handles events, and converts the final position back into units.
First, the position is retrieved in world units and stored in a Vector2 variable.
Second, the position is converted into pixels. Note that both the x and y coordinates are calculated. Furthermore, the remainder from the float to int conversions are stored.
Third, movement along the axes is calculated. The StepX function calculates x movement using the UserMove script, while the StepY function calculates y movement using the UserJump script. Fourth, after the final position is determined, it is converted back into world units. Last, the position is set to physically move the character on screen.
That completes the Update() function. The process is familiar, but has been expanded to coordinate x and y axis movement across the UserMove and UserJump scripts.
Let's take a closer look at the StepX and StepY functions now. These functions are called from Update(). They are used to update our pixel position based on the character's movement.
StepX accepts the character's current x position as an argument. Notice that the UserMove script's CheckInput() function is called, followed by the Move() function. This is an example of the UserController script telling the UserMove script when to execute its responsibilities. Once we're ready to move the character on the x axis, our UserController calls StepX and asks UserMove to generate a new position based on user input. After, that newly calculated position is returned to the UserController and used to update the character's position on screen.
StepY is almost identical. It accepts the current y position as an argument. Then, it tells UserJump to check for input and calculate the character's new y position. Afterwards, that position is returned.
Thus, once we execute StepX and StepY, we have taken the character's current pixel position, checked for input, and calculated a new position. From there, the UserController script's Update() function converts the position back into world units and updates our character on screen.
The biggest thing to remember out of all of this is that our individual scripts, such as UserMove and UserJump, are entirely independent. They don't even know that each other exist. This means we can easily update these scripts without causing any conflicts. Moreover, we can attach just a move ability or just a jump ability to a different object in our game without any trouble. Or, we can create a future game that uses our jump script, but not our move script. And so on.
Only the UserController script is aware of each individual script. It is the critical piece that coordinates the scripts for this particular game. In future projects, we could choose to mix and match our independent scripts in different ways with great ease. In that case, we might create a different UserController to manage that particular combination of scripts. However, the scripts themselves remain loosely coupled and highly cohesive.
This pattern of creating focused, independent scripts with an overarching manager script that controls them will be a recurring theme in your game development career, especially whenever you use a component-based engine.
John M. Quick leverages a unique combination of design, development, and research expertise to dramatically improve the learning experience.
John earned a PhD in Educational Technology at Arizona State University, where he researched the user experience of enjoyment and motivation in games. His statistical models, including the Gameplay Enjoyment Model (GEM) and Gaming Goal Orientations (GGO), are used to inform the evidence-based design process.
John has released more than 15 games for mobile devices, desktops, and the web. His game-based learning simulations drive meaningful organizational outcomes, such as improved performance, decision making, and engagement.
John has over 5 years of teaching experience at the higher education level. He has instructed courses on game design, programming, and computer literacy at Michigan State University, Arizona State University, and DigiPen Institute of Technology. He has created 100s of learning experiences that produce tangible results for clients.
John is the author of Learn to Implement Games with Code (ISBN: 9781498753388), Learn to Code with Games (ISBN: 9781498704687), and Statistical Analysis with R (ISBN: 9781849512084). All of his books apply novel learning approaches to help diverse audiences build practical skills in technical domains.