Lesson 28 - Applying What You've Learned and Refactoring

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


Transcript

Alright. Here we are, ready to go back to our game project that we started off with, having now learned a bunch of important C# concepts that will continue to fill in as we move along. Now we get to go back to the fun part, which is creating games. We'll pretty much dive right into it, but I want to mention that a lot of what we'll do to start off with is, we'll start by re-factoring a lot of the code that we had already written with the CubeController and SphereController scripts, in particular. Refactoring, to me, is quite a natural process in coding, which is basically taking usually fairly rough code and bending it, molding it, and reshaping it into hopefully cleaner, easier-to-read code or more separated code that handles very basic responsibilities and isn't too cluttered up. When you go on the internet, you'll often find the end result of this long, iterative process of coding. Coding is very much an iterative process, even though you often see the end result often, sometimes anyways, pristine and very clean, not too long. All the connections are just perfect and you wonder how somebody ever came to make this perfect jewel of code. Well, the fact is, that's usually after the result of a long, iterative process of tossing out bad ideas and realizing you could do it a better way an so on.

Refactoring is a very natural process and it's a good process to get in the habit of doing and understand its purpose. Now that we can apply what we've learned, I think refactoring is a good place to start, by refactoring old code that we had originally wrote and hopefully understand a lot better what it means. The first thing we'll want to do is open up the CubeController script. Here's what we'll do when we refactor all of this. You notice that we have everything, or most of our code, in one big method. There's the Update() method and there's a bad attempt at keeping them separate by using the LateUpdate() method for some code, not a very good use of LateUpdate(). I'll describe later what LateUpdate() is all about. Of course, there's some stuff in the Start method, as well. Well, a lot of this stuff in the Update() method can be refactored into its own particular method that handles a particular task, so here in the Update() method, we have the teleport code. We have movement code. Yeah, that's already 2 things too many for 1 method. The easiest thing we can do is just re-factor all this code into their own methods, so let's start with a teleport method.

We'll create a teleport method off of this code, so we'll just cut that out and we'll create a new, just make it a private method because it's not going to be called outside of the script, and call it TeleportCheck() just like that. That will handle all of that code and now we can put that at the top of the Update() method where the teleport code used to be, just like that. The movement code can go into its own method, as well. We'll just call it Movement() We'll replicate what we did with TeleportCheck() make a private void Movement() method, put it all there, and call it here underneath TeleportCheck() which is where it used to be, the previous code. LateUpdate() which, again, was not a very good use of the LateUpdate() method, LateUpdate() being a variation of the Update() method that is particularly handled by Unity. It's not something I just came up with. We'll actually get rid of the LateUpdate() method entirely because it's not useful in this context and we'll put that code, because it was handling the, checking the boundaries of the player's movement and constraining that, the movement, to the boundaries that we set. Let's do the same thing here and call this BoundaryCheck() and call that here as before, as we did with the other methods.

We'll keep the Start() method for initialization because that's a method, again, that comes right from Unity and it's good for handling initialization. We don't really have to make a separate initialize method or anything like that and call it in Start. We should get used to what Start() is all about and it will be quite clear to us what it does. Yeah, now as you can see there, this is a lot cleaner. When we look at our code, we don't really even have to look at the TeleportCheck(), Movement(), and BoundaryCheck() methods unless we want to look at their internals and understand them better. Otherwise, we'd probably just, when we look at this CubeController script, we'll just want to be interested in what the Update() is doing. At a glance, we know that it's doing a TeleportCheck() and it's handling Movement() of the player and the BoundaryCheck() as well. That's already a pretty well refactored script, so now let's run this to make sure everything runs fine, that we didn't make any errors. Yep. You can still move around and teleport.

Great. Here's another little thing I'll just throw in there. This is a C# concept. Right here we have variables. These are all variables, whether they're fields, properties, or local variables, declared locally to the methods that we create. They're called variables because they're variable. They can change. The information stored in them can be updated and changed at some point. Well, sometimes you have a variable that shouldn't be a variable. It's not going to change, and maybe you want to remember that that's the case, or maybe you want to constrain that from being changed by other developers you're working with on the same team. Whatever the case may be, you can make that variable non-variable, in other words, constant, by prefixing the const keyword. Here's a good place we can do that with the CamWidthX and CamHeightY that we had here, so just type in const. And now they're constant. You can't change that in your code. If I try to change it, you will get an error. Let's just try doing it here. CamHeightY= 7f. It doesn't let us. Basically, it's saying it needs to be a variable, but it's constant. That's a quick little thing I'll throw in there.

One other thing about constants is, you need to assign them right at where they're declared. If I don't assign it, there's a squiggly line that says, "A const field requires a value to be provided." We can't have an empty constant. All right. The next thing I want to do is, we'll refactor now our other main script, which is SphereController. Turning to the SphereController script, one of the first things that pops out to me that could have been maybe done better is we made RandomSpawn a field here and remember, fields are best used whenever they need to be kept alive between method calls that reference it or otherwise perhaps need to be referenced by outside scripts or classes or what have you. This is not the case with this field here. It's only being used to create a random value that then is, it sets the position, the random transform position of the GameObject we're instantiating here in this If conditional, based on the timer. We're spawning at every 500 tics, right? We don't need to have a field that survives, we can just run the Update() method and local to the method, have a Vector3 RandomSpawn. Right? You see, it's going to work because the Update() method is going to run as usual and then when it hits this if conditional, then it will create the RandomSpawn. We only really need it for this one purpose and then we'll set the x, y, and z floats accordingly.

Now, you may have noticed that in this case, we're not using the new keyword. We're not instantiating it, in other words, like an object. If you recall correctly, in the previous video we talked a bit about structs and we saw how Vector3 is a struct and how basically structs are different from objects in that they're value types whereas objects are reference types. Just like an int, a Vector3 and an int, they're both structs. You don't need to use the new keyword to carve out some new memory space. When you create the variable, it automatically carves out that memory space, right then and there. This automatically happens and it's implicit in the fact that it's a value type and because value types always belong to their own memory space, of sorts. They never hold just a reference to another memory space, for example. On the other hand, since reference types could either be referencing a new memory space with a new keyword or otherwise be referencing an existing memory space, you can use the new keyword with instantiating an object reference type. Now, there's actually a use for the new keyword with structs and actually, I'll demonstrate it right now. It's used differently than you would expect to use it with an object or a reference type.

Continuing with the refactoring of SphereController, what we'll actually do here is, we don't need this RandomSpawn identifier at all, actually we don't even need to initialize the variable. Let me just show you, you can do this differently than we've seen here. float x, because the Random.Range() returns a float. Right there it says, "Float." Float y for that and we'll say float z for that. Look at this. We can do this. Right here in the method, here it takes a... Here, let's see, what is the Instantiate() method take in? It takes in a Vector3 position, so that's going to be stored, again, in the position for the transform for this object that's being instantiated. It's going to be a Vector3, so we can do this even without having created a Vector3 variable already. We can just say new Vector3 and with IntelliSense going up and down on the options here, the third option is I can input float x, float y, and float z input arguments. This is a method for the Vector3. This is a particular method called a constructor that I'll mention a little bit more about shortly. Then we can just reference the x, y, and z floats we just created.

This is interesting. We didn't even have to create an identifier or a name that we can use to reference this vector. We just put it right into the input argument for this Instantiate() method. How is this possible? Well, when you really think about it, those names that we choose for variables, they're basically for us as programmers, so that we can then reference the variables in code. Otherwise, when you're creating something in memory, it's being created in memory with probably some sort of weird name that only a computer can understand. The human-readable name that a programmer creates is just for the programmer. In this case, we're creating the memory space and then we're immediately assigning it to the transform position of this object. For the Instantiate() method again, I'll show you the Vector3 position. This object, when it gets created, it's going to have a transform and that transform's going to have a Vector3 position. The name of that is going to be position. Anyways, this is a bit of a shorthand and you will see it often in code. I don't want you to get it confused. It's a little bit cleaner, if you ask me, when creating a functionality such as we did here, so that's why I did this here as a little refactor of sorts.

In the same spirit as what we did here with removing the random spawn as a field, there's another one that we can remove: CubeRef position. That just grabs the transform position of our cube that we're grabbing from up here as a field, a CubeReference. We only need one of these as a field. This CubeReference can stay as a field. Now we can say CubeRef position was a Vector3, so create a local Vector3 for that. That works just as well. Now that we made this variable CubeRef position a local variable, it's first declared in a method, not the class level. I want to employ and hopefully maintain this naming convention. That is, I want to employ camelCase for locally-scoped variables, variables that were declared in methods and PascalCase for fields. camelCase is basically, you just have a lower case first letter in the variable name. Here, with these fields, you use upper case first letter. Each subsequent word in the variable also is upper case. Same here with the subsequent words. That's the reason for camelCase. It looks like the humps on a camel's back. That's camelCase for local variables. We're going to have to change these references here. I'll show you another thing, an easy way to do that is to hold and press control+f on your keyboard and here, wherever you have CubeRef position, we'll want to change it to a lower case c.

What we can do is, hit that little arrow there. It gives us an option to replace all of those references. Right here you can just type in what you want to replace it to and this says "replace next," and this says "replace all." We'll hit "replace next" so we can see it in action, changing each instance of that variable. Handy little trick that Visual Studio gives us, another nice little bit of functionality to make things easier. Just as we did with the CubeController script, there's a lot more refactoring we can do to clean up this Update() method for the SphereController. As we can see here, we basically have two different kinds of processes going on here. There's this process, which is basically responsible for tracking the position of the cube and calculating the distance between the sphere and the cube. This part is just responsible for spawning more spheres. We can at least put these in two different methods. That would be a bit of refactoring we can do. Let's take out the spawn part and put it in its own private method. Simply call that, where that code used to be. Here, take that out. Put it in. It doesn't matter where, but I'll put it here above the SpawnEnemy() for no particular reason. private void EnemyProximity() because that's what it's doing. It's calculating proximity between the enemy and the cube. Copy and paste that code there and right here, call that method.

Again, a lot cleaner now. You don't have to even look at the details of what's going on with these methods. You can, at a glance, figure out what this script is doing. At this point, we should probably be checking to see we're not making any errors. Great. Works great. I noticed this little message here. We forgot to comment that out. Let's just do that now, otherwise it's going to get in the way. That was where? In the RPGController, I believe. Polymorphism() I think it was that method. Let's see if that's correct. Yep. Okay, good. Just wanted to get that out of the way. While we're here, I want to just show you a neat little thing. There's going to be this duality between dealing with the inspector and dealing with the actual code. Right here, where we're using the Find() method, that's a static method, that's obviously part of the game object class. We're using this method to find, dynamically at runtime, the cube object that we created. We name it as a string, passed in as an input parameter. Out from that comes the game object. That's how we keep a reference to CubeReference.

Here's another way we can do this. Let's just comment this out. I'll change this and say dynamic reference because in principle, it could change in code, whereas this is ... I don't want to say, "a static reference," because that's maybe confusing, but basically we can set the reference here, once and for all, by declaring this as public and then in the inspector for our Sphere, we see here CubeReference is exposed in the inspector. This looks like we can give it a game object. How do you suppose we can do that? Probably exactly as you guessed. We take the cube. We click and drag it, and just put it right there. There we go. Right in the inspector, we can assign that reference to that Cube. It may or may not be helpful. Some people like to keep these kinds of references in code because they stay in code as much as they can. Otherwise, you have to go all around your project and see where these references are and how they're being tied in. Right now, we'll keep it this way. The reason I showed you this right now is, there's many different ways of accomplishing things with Unity. It's very flexible, so this is one of the ways that you can hold a reference to an outside game object.

Alright. I'm going to end this lesson right here because I think we covered quite a bit of ground, and we'll pick up from where we left off in the next lesson. Alright. Thanks a lot.


Related Articles in this Tutorial:

Lesson 1 - Who This Course is For

Lesson 2 - What to Expect from this Course

Lesson 3 - Installation and Getting Started

Lesson 4 - Starting the First Project

Lesson 5 - Prototype Workflow

Lesson 6 - Basic Code Review

Lesson 7 - Game Loop Primer

Lesson 8 - Prototyping Continued

Lesson 9 - C# Fundamentals and Hello World

Lesson 10 - Variables and Operations

Lesson 11 - Variables and Operations Continued

Lesson 12 - Floats, Bools and Casting

Lesson 13 - If Statement Conditionals

Lesson 14 - If Statements Continued

Lesson 15 - Complex Evaluations and States

Lesson 16 - Code Syntax vs. Style

Lesson 17 - Variable Scope

Lesson 18 - Object-Oriented Programming Intro

Lesson 19 - OOP, Access Modifiers, Instantiation

Lesson 20 - Object Containment and Method Returns

Lesson 21 - "Has-A" Object Containment

Lesson 22 - "Is-A" Inheritance Containment

Lesson 23 - Static Fields and Methods

Lesson 24 - Method Inputs and Returns

Lesson 25 - Reference vs. Value Types

Lesson 26 - Introduction to Polymorphism

Lesson 27 - Navigating the Unity API

Lesson 28 - Applying What You've Learned and Refactoring

Lesson 29 - Constructors, Local Variables in the Update Method

Lesson 30 - Collecting Collectibles, Items and Powerups

Lesson 31 - Spawning and Managing Prefab Powerups

Lesson 32 - Implementing Powerup State Logic

Lesson 33 - Displaying Text, OnGUI, Method Overloading

Lesson 34 - Referencing Instantiated GameObjects, Parenting

Lesson 35 - Understanding the Lerp Method

Lesson 36 - Creating Pseudo Animations in Code

Lesson 37 - Understanding Generic Classes and Methods

Lesson 38 - Animations Using SpriteSheets and Animator

Lesson 39 - Working with Arrays and Loops

Lesson 40 - Debugging Unity Projects with Visual Studio

Lesson 41 - Camera Movement and LateUpdate

Lesson 42 - Playing Audio Clips

Lesson 43 - Routing Audio, Mixers and Effects

Lesson 44 - Adding Scoring Mechanics and Enhancements

Lesson 45 - Scene Loading and Game Over Manager

Lesson 46 - Understanding Properties

Lesson 47 - Controller Mapping and Input Manager

Lesson 48 - Understanding Enums

Lesson 49 - Dealing with Null References

Lesson 50 - Handling Variable Framerates with time.DeltaTime

Lesson 51 - Preparing the Project for Final Build

Lesson 52 - Final Build and Project Settings

Lesson 53 - Introduction to the Unity Physics Engine

Lesson 54 - Understanding FixedUpdate vs. Update

Lesson 55 - Movement Using Physics

Lesson 56 - Attack Script and Collision Events with OnCollisionEnter2D

Lesson 57 - Projectiles and Stomping Attack

Lesson 58 - Parallax Background and Scrolling Camera

Lesson 59 - Infinitely Tiling Background Sprites

Lesson 60 - OOP Enemy Classes

Lesson 61 - OOP Enemy Classes Continued

Lesson 62 - Trigger Colliders and Causing Damage

Lesson 63 - Multi-Dimensional Arrays and Procedural Platforms

Lesson 64 - Finishing Touches

Lesson 65 - Series Wrap


Comments

Please login or register to add a comment