Lesson 60 - OOP Enemy Classes

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


Transcript

For this project, we're going to have a larger variety of enemy types than we had in the previous project that we worked on. When creating game elements such as enemies that are varied and yet have properties in common, we shine a spotlight back to object oriented programming and the benefits of this programming approach. Recall that OOP is quite useful when constructing or modeling class blueprints that you then intend to create instances from, often multiple instances. And OOP has all sorts of ways of maximizing code reuse through things like constructors and fields inheritance and polymorphism and so on. The idea is that you can use a combination of OOP principles in order to simplify your class structures and yet be able to create dramatically unique instances off of these simplified class structures with relatively little code.

Now thinking about these benefits of OOP also shines a spotlight on this sort of dual nature of Unity that I mentioned before, where you have the OOP nature of C# sort of obscured behind a somewhat non-OOP component-based interface. This component-based interface of adding scripts as components onto GameObjects for example is a really simple and intuitive system, but it might make you scratch your head wondering how you'd go about creating something like an enemy blueprint. You know with inheritance trees and such all in code, right? Perhaps we’ll a base class for all the common methods and properties, and then inherited classes for all the enemy variants, and then just instantiate them preferably with a new keyword right. Well, this lesson will go into how you could go about doing exactly that. sort of merging C#’s OOP benefits with the Unity system.

As a refresher, these would be the OOP features that would make all of this work. You have constructors, which are often great for initializing the fields and properties that fundamentally describe the object size, speed, how it appears, and so on. You have inheritance which is great for taking a common foundation and expanding on it, rather than rewriting it all from scratch. You can just have an enemy base class and then a wizard and a knight, for example, then inherit from it. You have Polymorphism which is great for allowing you to treat objects in code as simpler base types that they inherit from, wherever this is possible. So you don't have to write a bunch of extra code just to handle cases like “If it's a knight, deal damage like this. Else if it's a wizard, do it like this.” Instead, with polymorphism you can just say “I don't care if it's a knight or wizard. As long as it's an enemy, figure out what kind of enemy it is and just run its particular way of dealing damage.” Things like that are the benefits of Polymorphism. And you also have generics generic classes and generic methods that let you specify a type when the method is called, or with classes when the instance is created, and thereby treat some internal detail differently depending on the type specified.

So all of these OOP features are fantastic when creating video games because a lot of game design is about creating objects and running their methods. Often objects only have slight variances between them. these slight variances should be able to be handled with the OOP toolset I just mentioned, and keep your code lean in the process with a lot less repetition. And keeping your code lean is not just good for reducing repetition, it reduces the chance of error that you're repeating over and over again, causing a really big headache as you're developing. So it's a good habit to get into.

So how would one begin to think about implementing OOP enemies? For instance, within the confines of the Unity engine. While perhaps the most obvious way you might think to do this is to start with a class that inherits from the GameObject class in order to construct your custom GameObject enemy class and just attach the relevant components that it needs and go from there. But the GameObject class unfortunately is strictly sealed, meaning you can't inherit from it, so that won't work. The next approach then might be to create a prefab GameObject with all the typical components pre-attached and then use the AddComponent() method to attach a suitable MonoBehaviour script that handles much of the specific behaviors of that GameObject. That approach would work, but a lot of that will seem redundant and lacking flexibility. And also might not be obvious how to initialize the object's values without a proper constructor in the MonoBehaviour script. MonoBehaviours don't allow constructors, I’ll mention this again in a moment.

So before we get started on our approach, just know that there are a lot of ways to get the job done you don't have to do it the way I'm going to show you in this lesson. Ultimately, we're doing it this way for maximum flexibility. Not necessarily because it's always the most convenient approach. Alright, so having said all of that, on to the first step which is creating an Enemy class. But before that, let's create a new folder in our Scripts folder for enemies. Call it Enemies. We’re going to need a resources folder here in the assets folder, so call it Resources. Remember we’ll be using this to load dynamically at runtime any sort of asset that we’ll need. Actually, we're going to need the Eyeball asset, which is to be common to all enemies. We're going to want to load that at runtime, so I can just move this into the Resources folder. So when in our Enemies folder, let's create an Enemies script to start off with. Enemies.cs.

So what we're going to do is we're going to do is have an inheritance tree of the different enemy types, all of which inherit from a basic Enemy class which will be a MonoBehaviour, so all those inherited classes will also be MonoBehaviours. It's important. So these classes will end up being the main ScriptComponent. Attach the enemy GameObject that will define the enemy's properties, movement, methods, damage and so on. So it will look something like this. So we're actually to start from scratch. We’ll say public abstract, and I’ll mention in a moment what that's all about, class called Enemy. It’s going to be a MonoBehaviour, so it’ll be our base class for all the other enemies that we used to inherit from it. So we'll have a bunch of enemies, I'll just show you how the inheritance structural kind of look like to start off with. So we'll have a public class Bouncer, Enemy type. So it's going to inherit from Enemy, thereby also making it a MonoBehaviour, as I mentioned before.

We’ll have a public class Gigantour, and we’ll have several more, I'm just showing you this for the general class structure how this is going to look. Now we’ll create a generic class that we’ll end up instantiating with the new keyword, which will itself then create the GameObject that we’ll want for enemy and attach a ScriptComponent of our choosing and that will determine the exact type of enemy that we want. We’ll do this within the generic angle brackets. Right. So we'll construct that generic class here. We’ll say public class Enemy<T> this will be the ScriptComponent we attach later. So we’ll say where T is of type enemy, it’ll have to be one of these guys. So, when we create this generic Enemy, which don't get confused with this, that’s a totally different class. It's going to take in an enemy, a generic enemy, that’ll take in an enemy and when we create this we’ll also get a public GameObject and we'll have a reference to the ScriptComponent we ended up attaching, so we’ll just have it here as a field.

So it is going to be of type T, we don't know exactly what it’s going to be; a Bouncer or Gigantour or what other enemies we come up with. So we’ll call it ScriptComponent. And then we'll have a constructor Public Enemy, a rapper. This is going to be an enemy in our game... Rapper, shows my age right there. OK string name. So it's going to take in, we’ll specify the name of that enemy here. It will end up showing up in the inspector. And it will just initialize the GameObject in the field to a new GameObject with the string name we've provided, and then we'll add the ScriptComponent to the GameObject with AddComponent() whichever one we give it. And we’ll return that reference back to the ScriptComponent field so that it will be usable once we actually instantiate this elsewhere. We can access it easily through this ScriptComponent field.

So now let's flush out the base Enemy class that the other enemy classes inherit from. So this should basically house everything common to every enemy basically. So our enemies are going to need a RigidBody. So Rigidbody2D will be called Body. We’ll need a SpriteRenderer, call it Sprite. We’ll need a CircleCollider2D because they're all eyeballs, so CircleCollider makes most sense. We’ll call it Collider. And every enemy will have a Speed field as well as a Direction field. And, well, they're all going to need their own movement patterns, so I'll go into this a little bit more in a moment what this means. So we'll call this protected abstract void called MovementPattern(). I'm not going to specify any details to this method. I'll mention again in a moment why that's the case.

I'll mention really quickly now that protected is an accessibility modifier just like public and private. In this case, protected means that only inheriting classes, classes that inherit from Enemy can see this field. So outside classes can’t see this, sorry, in this case a method. Right? You could do with a field as well, make a field protected and only the inheriting class will see it. And we have a new method here that I'm going to talk about called Awake(). There's nothing too difficult to wrap your head around I promise I'll describe exactly what Awake() is all but a moment. So I'll put a little comment hear say ‘Add common components’ which relates to the fields that we just specified. We’ll say Body equals... Scroll this up take the GameObject property. Use AddComponent() and attach the RigidBody component.

Take the sprite and it will hold a reference to GameObject.AddComponent() try to guess what it’ll be. SpriteRenderer. And I’ll take Collider, the field, and that reference will point to GameObject.AddComponent() the CircleCollider. And now another comment, I'll say ‘Set common Sprite’ it’ll be the eyeball, right, that we will need to access through the Resources.Load() I believe. And say, Sprite.sprite = Resources.Load<Sprite>(“Eyeball”). Also want to set the collisionDetectionMode, so say Body.collosionDetectionMode = CollosionDetectionMode2D.Continuous. We’ll also want to tag the the GameObject. We’ll put here the tag.. So we’ll want to give it the tag so we can easily reference it in code. Of type Enemy, or string “Enemy”, but we’ll be treating it that way like we’re sort of tagging it as the type that it is. And also want to set it to a layer for the collision detection. So we’ll need to use a LayerMask and the method called NameToLayer(), so put it on the EyeBall layer. And the collision layer, so that would be here right here.

So here we have a method called Awake(). It's another Unity method similar to Start(), actually it's pretty much exactly the same as start. Basically the only difference between Start() and Awake() is like the difference between Update() and LateUpdate(), which run exactly the same way it’s just the order of execution that's different. LateUpdate(), remember, always runs after all updates have already been run. Well Awake() is much the same in that all of the MonoBehaviour scripts call Awake() first and then calls to start follow that. So the usefulness of this is if you have a scenario where one script start method has a reference to an outside GameObject and we have no way of knowing if that outside GameObject’s script’s Start() method has already run. We need to be sure though that the outside script is in a valid state. So we can make sure of this by using Awake() to set that outside script’s essential fields and properties. Treating it more like a constructor, using Awake() in that way so we’re setting that script’s particular state that is necessary to make it useful to put in a valid state. While we then can use Start() to manage the connections between these objects, knowing that they're already in a valid state having already run Awake(). So that's typically how we should be using Start() and Awake(). We really haven't had the need to do it that way, but we're at the point where problems might occur if we’re not aware of this script execution order, so that's part of why I'm showing you this.

But actually we're not going to use Awake() in exactly this context to solve any of those kinds of problems. The way that we're going to use Awake() here is for a bit of a different reason. We're going to use Awake() to set up all the common elements that will be common across all enemy types. Each enemy class will inherit this Awake() method and instead each enemy class will use its own Start() method to set up its properties that are more specific to itself. So in a way I'm actually kind of inverting the typical usage of Awake() and Start() like I mentioned previously, But that's OK. We're going to use it to solve this particular problem we have. Alright, continuing on with setting up our base Enemy class. We have a bit of problem that we need to solve. Each enemy's properties, or fields actually, for things like Speed, Direction, Position and possibly other fundamental attributes, will have to be set at instantiation. But how do we set this without a constructor, right? MonoBehaviour should have constructors because, you know, Unity is responsible for calling MonoBehaviours. They're out of our hands. Well, what we’ll do to solve this is we’ll create our own Initialize() method as a sort of pseudo constructor that we can run right after instantiation, because we're handling the instantiation of the generic Enemy type. So then we can call the Initialize() method right after. I’ll show you how we can do that, but first we'll have to set it up. In the base Enemy class, we’ll have an Initialize() method and so I’ll have a little comment here to remind us what this is all abou. “Insert all unique values to be determined at instantiation” So, this needs to be public, it’ll return void called Initialize(). And we’ll need, again, it's a pseudo constructor so we’ll need a speed given, a direction, and a Vector3 position. And we’ll just say the Speed field will be whatever we provide as an argument to this method. direction will be provided for Direction and transform.position will be the Vector3 we pass in called position. Alright, I’m going to call this. Temporarily, I'm going to put a prefix to this, a modifier of sorts, called virtual. So we’ve got a virtual method called Initialize().

So I should quickly mention what all this is about; virtual, abstract, these little sort of prefixes are modifiers that we can add to or prepend to methods or properties for that matter, though we're not going into that. What are they all about? Well when we first looked at inheritance in an earlier lesson, we basically just looked at how inheriting classes just sort of silently import code from a base class, right? Well we're going to go a bit deeper into the subtleties of inheritance by having the option of specifying a method. Again or a property, as being virtual or abstract. So first, virtual. Well virtual simply means that the implementation of the base class, for example in this virtual method Initialize() is the default implementation for all classes that inherit from this base class. However, those inheriting classes have the option to override this implementation with its own Initialize() method, effectively ignoring the base class’ virtual method and instead using its own. So we have this base class, Enemy, so here in the Bouncer class (The red squiggly I’ll mention what that's about in a moment), I can have a public override, we’re going to override the Initialize() method, right? And there I press tab and it gives me the default implementation, which actually just calls the base Initialize() method. No real purpose for that right now. So what I would I could do is I could set up my own properties: Speed = speed + 5, whatever. I can have my own implementation in the inheriting class, in this case Bounce, right? So that's what you do. You override the Initialize() method. If I get rid of this, and I don't have an initialize method and thereby I’m not overriding it. I just inherit this default implementation, right? Now actually I'm just showing you this option, I'm not necessarily going to use that. Not yet anyways.

We’ll now move on to abstract, right? Now abstract is a similar modifier to virtual, except that it's used to mark a method or property in a base class that must be overridden in the inheriting class. So, here we're marking the MovementPattern() method as abstract in order to remind us when writing inheriting classes that we must have a unique MovementPattern() method with its own unique implementation details in the inheriting class. Because presumably all enemies should have a movement pattern, they should all be unique, right? So that's also why there are no implementation details when this method is marked as abstract, because you're never accessing this method, in this case the base class’ abstract method, you always have to override it in the inheriting class. So being abstract, we just have to determine the method’s name and input parameters, its signature in other words. And then fulfill that when we create inheriting class. So you also see that we mark the class as abstract, that's possible as well. An abstract class can have abstract and non abstract members as you see here, but an abstract class is never meant to be instantiated. It's sort of like a pure blueprint or base class that other classes use as a reference. So that's why we’re marking the base Enemy class as abstract. We're never actually going to instantiate a base Enemy, right, we're going to only instantiate inheriting classes.

Alright so after all this, we’ll now start fleshing out our inheriting enemy classes. So starting off with what I think will be the most simplest, or one of the simplest enemy types, the bouncer type that I have come up with. So for this class, we're using Start() to set its own particular important attributes’ starting point. So we’ll say it is going to be, a Collider that we’re inheriting down from base class, we’ll have its radius at .19f. A transform.localScale, so how large it is in our game, we’ll say new Vector3(1.2f, 1.2f, 1). And see the squiggly line. Bouncer does not implement inherited abstract member Enemy.MovementPattern(), that’s what I was talking about earlier, about how it reminds us. It actually strictly enforces that each inheriting class needs to have its own particular MovementPattern(), so we can set that up. Protected override void MovementPattern(). And get rid of that Exception, we don't need to throw an exception. So the Body’s velocity, this will be its movement. new Vector2, and set its direction and velocity on the Y. I’m not going ot to describe too much of the MovementPattern() because =we've got to kind of move along quickly. Some of them may not make much sense. I just sort of hack this out, so once we start playing a game maybe you'll be able to pick apart how some these MovementPatterns are working. Some are easier to understand than others.

So I can say, if(Body.velocity.y == 0), just have it bounce. Body.AddForce(), pass in a new Vector2. The Y value of 300, and we'll call this in FixedUpdate(). We're going to just get rid of this here. Actually, put this in its own file. We’ll start making each enemy class have its own files, just to make it easier to organize all this. Going to cut that, and create a Bouncer class. We're just going to put that there. Now we're going to test, finally, the instantiation of our enemy by creating a factory script called EnemiesFactory. So, create a new script. Call it EnemiesFactory. And actually, before I move forward I'm going to want to have this attached to the Floor GameObject. And I will run this when we start our game. So that’s in the BG1_Foreground parent here. Put that there, and here is where we’ll be creating our enemies using the code we just written.

Again we’ll say void Awake(). We'll want a Bouncer enemy, so we'll need to start off with a generic enemy, could be any inheriting type from the enemy class. I’ll just say Bouncer, and we’ll call this guy bouncyBill. And we’ll say new Enemy of type Bouncer. And we’ll pass in the name bouncyBill, do that again. And remember we need to access the Initialize() method, which we can do now through the ScriptComponent. So bouncyBill.ScriptComponent.Initialize(). So this will return whatever we put in here, so it’ll return the Bouncer script that we have attached, and we’ll run its Initialize() method. And we’ll say speed... I'm doing this a little differently, I'll mention in a second why, or what this is all about. Then, direction, say random.Range(). This will give us that random direction, minus one or one to the left or right. And position gives new Vector3(). We’ll just put it in the middle of the screen. We're just testing right now. Sorry, these are arguments for the Initialize(), I actually forgot that temporarily.

So why am I using the name for the input parameters or arguments here? Speed, direction, position. Well, these are called named parameters, which is a fairly new feature of C# I believe. And the reason I'm using them here is basically because it's a handy way of telling us what these values we’re inputting into into the Initialize() method. In this case, what they relate to. Otherwise we're just dealing with seemingly random values, unless of course we were to hover over the input and find out what the exact nature of the arguments are, what the what the type and the name is on the definition side of things. So just showing you how named parameters are available to you. You can get rid of these and you'll be working just like any other method that you've used before, with just the values input. But I think this is a little bit easier to understand what's going on as we're creating our enemies.

Alright, so now let's run this and make sure that it's being instantiated correctly in our game. We’ll wait and see if bouncyBill comes into existence. We got two of them because we already have a clone floor, right? Because of our tiling script. But that's OK because every time we're going to tile a new floor GameObject, we're going to spawn new enemies. We'll do the next lesson, we'll worry about it then. So great job powering through this lesson, we’ll continue flushing out enemies in the next lesson.


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