Lesson 37 - Understanding Generic Classes and Methods

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


Transcript

Alright, in this lesson we're going to talk about generics. Generics in C# are quite useful towards the task of adding yet more flexibility to your classes and methods. Generics, which are signified by those angled brackets after the class name or after the method name, allow those classes and methods to have constituent parts that are open-ended. Hence the generic moniker. For example, you might have a class that has a field but its type shouldn't have to be consistent. The type of that field. You might sometimes want that field to be a string, or you may want it for another instance to be an integer. That's the basic premise of generics. Whatever the case, generics allow you to accomplish this by specifying the type when you, say, create the instance or access the method within those angled brackets. You should know that we won't be creating our own generics for any use in our game here, or any of our games that we create for this course. It's a bit of an advanced concept, and not at all necessary for creating games.

You've already seen that Unity makes good use of generics. In particular, the GetComponent()) and you saw a little bit there the AddComponent() methods. I guess these methods were good candidates for generic usage because their basic tasks are adding and/or retrieving Components of varying type. Really the process of adding or retrieving a Component should be pretty much the same in virtually every way except for the type-specific aspect. That's, I'm sure, why Unity used generics for these methods where, again, you can specify the type within those angled brackets. Since you'll be working with generics on the consuming side of things, right, as a consumer of the Unity API, I want to give you a better picture of how they work by dabbling a bit on the definition side of things. First, let's set up a test script called Generics in our test folder. We want to start off by creating a Generic class here in this script. What you're going to see is kind of different from what we've been doing up to this point. I just want you to know that it's perfectly okay to declare multiple classes even in a script that gets attached to GameObject. In other words, a script that has a class that's a MonoBehaviour. It's okay because Unity knows to attach an instance of whatever MonoBehaviour class is in this script and ignore kind of the others in terms of creating instance automatically off of the class. In other words, if we attach this to a game object, it will create a script Component instance of generics. Whatever other classes we declare here will be ignored in terms of creating instances of those classes for the purpose of attaching it as a Component to the GameObject, which can be the case because they're not MonoBehaviours.

Actually, I'm going to just declare my own namespace for this. This belongs in a different namespace, actually. There is no particular namespace. It's in the broader sort of global namespace, I guess you can say. Here I'll declare a namespace called Mock to create our Mock GameObect as sort of an illustrative point. Here I'm going to make a generic GameObject class definition. This is nothing like GameObjects in Unity. I'm just using this for illustrative purposes to give you a sense of how they can be useful. See we're allowing for generic type to be passed in. Wherever that type is passed in, we reference it with the same, whatever we put in here. It doesn't have to be T. You'll often see T. It could be any alphabetic character. It could be a whole word, for that matter. Usually it's T. I guess it stands for type. Let's call this field a Component. This is going to be a field. Let's make some methods. Actually, let's make a constructor. It will take in whatever a T is. We'll use that to initiate this object. Actually, the Component. We'll make the Component whatever that is, whatever we pass in, and of whatever type that the instance is going to be later. We'll also want to make a public void GetComponent() as an analogy. Simply here, output it to the Debug.Log(). A message, so generic GameObjects Component. Then concatenate that with we'll use the get type method available to all fields pretty much, or properties. We'll just invoke that, and it will give us the type. whatever the type is that that field becomes later determined in the instance, right? R

Right now there's, well we don't know the type. It's generic. It could be any type. Later on, we'll determine the type when we create the instance. It's really important that you bear in mind that this is going to be an exceedingly loose analogy of how you can think of GameObjects containing generic Components. A Component that again could be a script, a transform, a SpriteRenderer and so on. This is not how it actually works in Unity. I just want to use this as an example that may still have some familiarity to you. Then later I'll mention why it's actually not a good example of generic usage, of generic class usages for this particular idea. Now that we have our generic definition, what we want to do is use that Generic class, make an instance of it, and in that instance we turn that generic into a specific type. Do this here in the, well, we just need Start(). In Start(), let's make an instance of that generic GameObject class from the Mock namespace. Access the Mock namespace. Let's say we want its Component to be a SpriteRenderer. We can pass in anything we want. We'll pass in a SpriteRenderer. We'll give that GameObject a identifier. A local name so we can reference it. Just like any object, we'll have to do this new keyword followed by Mock GameObject.

Do this all over again. You have to do that. Notice here for the constructor, it notices that because you've chosen as a specific for the generic type, and in specific for this instance, you're now determining that it's no longer generic. It's specifically accepting a SpriteRenderer for the generic type for this object. It's expecting now a SpriteRenderer to be passed into the constructor. That's why we said T here. We've made that SpriteRenderer. It expects a SpriteRenderer, and it's going to sign that as a SpriteRenderer. We're going to pass in a SpriteRenderer for the init. You can just say, new SpriteRenderer(). That's all it wants. It just wants an existing or a new SpriteRenderer. That's good. A really long line of code. This is a little ugly. I'll do that. You know what? I'm just going to get rid of this. We don't need that right now. Minimize that. Now to see the type for this object, this otherwise generic object that now is a specific object of type SpriteRenderer, we reference the GameObject's GetComponent() method. We can now output it to the Debug Log() whatever that type of that Component is, that's the only reason I'm doing that. Here in Unity, let's actually attach this test script. We'll want to watch the Debug.Log(). There you go. Generic GameObject's Component is of type UnityEngine.SpriteRenderer.

To see the beauty of generics, let's now pass in a different type making that Component field completely different. Let's change, instead of passing in. We're saying Component, but it could be anything. It doesn't even have to be a Component, but to keep the analogy alive, let's just say, CubeController. We'll pass in now a CubeController. See, IntelliSense complains that, now recognizes that, "Oh, this needs to be CubeController" As soon as we determine it here in the object's type, it's a GameObject t of type CubeController. That's how you look at that. Because I'm passing in a MonoBehaviour, I guess it's complaining here. Otherwise, it's able to do its job and it shows us that it's giving us a CubeController. It's actually really important to realize that you're not passing in any particular type like you would when you pass in an object or a value with an input parameter. Instead, what you're doing is you're just instructing the particular generic method or class, class in this case, to treat something within it as that given type, wherever the type placeholder is referenced. Again, we used T here. You can use anything. I hope that makes sense. We're just passing in the type. This is actually an awkward use of generic classes for this particular analogy or example, because we're tying in an instance of the Mock GameObject class with the type of Component property it holds.

We know that this isn't anything like the way Unity works, because GameObjects are not tied into any particular Component. They can have many different Components. What we'll do next is define a generic method within a non-generic class that determines that class' Component type. This again is an analogy of how things are working under the hood. Don't take it too literally, but this will hopefully give you a better example of what something like AddComponent() and GetComponent() would work like in the Unity engine. Actually, let's make another class like I mentioned, a non-generic class. GameObject, Make no mistake, this is a different class. It actually has nothing to do with this class. This one is GameObject

Whatever that generic is. This is an ordinary class, which happens to share a very similar name. Here we will, say, do a little polymorphism. Create an AddComponent() that's generic. All we'll do is reference that Component field, and we'll say, new T(). Whatever that is, we know it's going to want to be an object of some kind. At least constraining it to that regard. It complains here because we actually have to add a little bit of extra information, unfortunately. We just know it's going to be an object of a certain type. A reference type, not a value type in other words.

Then we want to actually cast that type to make sure that it's exactly that type and not the base type. Output to the Debug.Log(). "Added Component of type: " Use this other ... I don't know if it's a method. I guess it's got an alias. I guess it's sort of an aliased method called typeof(T). It will return whatever type that is in this instance, or in the instance I should say. In whatever particular instance is employing this generic method. Here we have to put a little clause here where it says, where T: Component, so we're constraining... it has to be a Component. It has to be basically an object or a reference type. That's just to make sure that works. Don't get too bent out of shape about this stuff. This is not something you'll be doing any time soon. Let's also make a GetComponent() that returns type T. We don't know what the Component type is going to be. We'll need to put that clause there, too, where T : Component. All we'll do is return, well, we'll cast to that type. Again, just to make sure it's of that type. Kind of an awkward way of doing this, but hopefully it will be illustrative. We're using a Component here. We're constraining to a Component type, and we're referencing the Unity engine's built-in Component type.

Now, unfortunately there's a technicality here that prohibits us from actually using the Unity Components in this way. The technical reason that you don't really need to know is that Components get set by the Unity engine to null unless they're attached to an instance of a real GameObject. That's just too much stuff for what we're worried about right now. Not to worry. What I'll do is just make a Mock definition in this Mock namespace for the basically empty class definitions for Components and reference those instead of the Unity actual Components. Let's just say, class Component(). Part of the Mock namespace. Don't confuse it with the Unity's Component, which is part of the Unity engine's namespace. Let's just make it something we can reference. Go Name ... and then say, "class SpriteRenderer "Component ... derives from Component," alright? Now what we're going to want to do is use that, use all this definition side stuff, and in the consuming side we'll pretend we're adding this Component to our GameObject instance. Let's first make that GameObject instance of the GameObject class for Mock GameObject. Call it gameObject2 so we don't confuse it with the other GameObject that's of a totally different type, the generic GameObject type ... Equals new Mock GameObject() empty constructor. We'll reference that variable and use the AddComponent() generic method. We'll pass in ... See, it's telling us T, so it has to be of type Mock Component and new.

Let's type in Mock.SpriteRenderer. SpriteRenderer is a Component. I'll output a message to the Debug.Log(), add Component. I'll comment this out so it doesn't get in our way. Let's watch the console now. "Added Component of type: Mock.SpriteRenderer." Now we can sort of simulate getting that Component reference, similar to how we did it in our Unity project with using GetComponent() to get the reference to the SpriteRendererer and create a local variable that acts as a reference to the actual SpriteRenderer. When we change it, it refers back to that Component that's actually attached to the GameObject. Again, this is going to be kind of like how that's being done in Unity. This is all just an analogy. We'll use the GetComponent() that we defined. It's going to return ... Actually, let's just do it this way. gameObject2.GetComponent(), and what is it going to be? It's going to be a Mock SpriteRenderer. We're getting that component. Could be any Component... but we know it's going to be SpriteRenderer. It's going to return with IntelliSense. What does it tell us? It tells us that it's going to return a Mock SpriteRenderer.

Let's return it to a Mock renderer and have that local reference called renderer, arbitrarily. Now what we'll do is we'll change the reference for the renderer's name. Remember we created a Name field for Components, any kind of Component, and let's just say... "Tabors" referring sort of again, analogous to our game project. Now what we'll do is in Debug.Log(), we'll output ... We'll say Component name, and we'll actually reference the ... not the local renderer name, but the GameObject Component. Let's go gameObject2.component.Name. Again, we're not referencing this local variable. We're referencing the gameObject2 component name. Because we're dealing with reference types, when we change the name for this local reference, again analogous to how you see it in Unity, it will change the reference of the actual field. The real Component, you know? Component Name: Tabors See? Changes that reference the actual Component. That's not important. Essentially, the thing that I'm trying to show you here is that if you have a different kind of Component, it will retrieve that Component

That's the benefit you gain from using generics. That's the real takeaway I want you to have with this, and not get too worried about all the nitty-gritty, the details that may not be clear. We'll actually stop here with the analogy, because if we go any deeper it will probably just end up confusing you if it already hasn't. Even I'm kind of confused looking back at this again and trying to impart it into you, what it all means. I'm sure a lot of this stuff is just sort of not useful at this point probably in your mind. I just, again, want to give you a sense of how this works underneath with Unity. You will be accessing generics, and you'll see those little angled brackets several times, I'm sure, as we move on through this course. Remember just the type is what is getting passed in with those angled brackets. It's not being passed in like a ... input parameter or an argument like we would in a method. We're just passing in the type. I know when I first saw generics, I thought, "Great. Not yet another bracket "to have to worry about." I sort of fretted over it, but... You know those little angled brackets and everything. Then it eventually occurred to me that if you take away those angled brackets where you're supplying the type, you're just left with an ordinary class or method. It just looks funny when you're scanning through the definition side of things. I think because, you know ... The way I think the brain works is it wants something concrete to hold onto rather than abstract things like a generic T. If you replace that generic T in your mind with an actual type, it actually starts looking as simple as it is, which is quite simple otherwise.

Anyways, that's generics in a nutshell. Not much more to say about that, so we're done with that for now. Hopefully that'll give you some insight moving forward. I'll see you in the next lessons.


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