Lesson 56 - Attack Script and Collision Events with OnCollisionEnter2D

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


Transcript

Alright, in this lesson we're going to create an attack script which, at first will just be a punching motion, and we’ll later use a Unity event called OnCollision2DEnter() to manage what happens when the enemy collides with our player’s Fist, right? So to start off with, let's just add an asset to our Character folder for a Fist, a sprite. So here in the CheeseHead folder import new asset. And as usual set that to “Point”, not really much of an art asset. Oh well, it’ll do the job. So now we’ll create the Fist GameObject, and we’ll parent it to the CheeseHead. And it's going to need it's own collision layers, so we're going to need to define that. So here, we’ll add the collision layer, calling it Fist and in the collision matrix, we’ll set what collides with what. So, let’s see here. Well will leave default for default GameObjects, but we can get rid of all these other ones here. All we’ll need is Fist to collide with an EyeBall and that's it. So for the Fist, we're going to need to give it a Rigidbody and set it to Is Kinematic to make sure that the parent, the CheeseHead GameObject, is unaffected by the interactions otherwise. Whenever a Fist comes out and hits an Enemy, it’ll push back the CheeseHead, whenever there is that interaction.

So, let's set up all of the components here, including that Rigidbody. So for Rigidbody2D, we’ll have Is Kinematic. And actually, not using physics to move the Fist, we’ll just use Lerp() so that's totally fine, won't affect that at all. We’ll freeze the rotation so it doesn't spin out of control. And that's good for that, and we’ll also want a Box Collider, and I already figured out the size here so I’m just going to put that here. You can always see that in the Scene’s pane if you want to, right now our Fist is invisible. So lets add a SpriteRenderer and we’ll put the Fist there, put on the Player layer and put it on the third Order in Layer, so it’ll be above our Player. And put it close to our CheeseHead for now, this will actually be determined in code not not in the Inspector, but just so it looks alright when we're working with this, we’ll just set this default value. And actually scale it down, so that will be set in the inspector this is important, so it’ll start scaled down. And we’ll put it on the Fist collision layer, and we’ll create also the attack script. So make a new the script. Call it AttackController, put that here in the Player folder. Actually, before we go into the AttackController and setting that up, let's give ourselves another enum PlayerState.

So public enum Attack, and right now we have either Passive or a Punch for attacking state. And we’ll also want to add it to our Singleton for PlayerState here: public Attack Attack. Also, we're going to need a a button set up for this, so again we’ll go to the Input Manager and give it another axis, so we’ll turn that to three, and here we’ll call this one Punch. And the positive button, we’ll just set it to the V on the keyboard, mouse button will be 0, so I guess a left click. Alright, so turning again to the AttackController, in Update() and start off by making the Fist visible or invisible depending on our attacking state. Then it will GetComponent<SpriteRenderer>() and enabled = true. Else we’ll make sure it's not enabled. So that you don't see the Fist. Alright, now we'll just set up some fields in the AttackController, so we’ll need a reference to the CheeseHead GameObject, which we’ll set in the Inspector. A few more fields here, float StartingPunchPosition. I like using descriptive name, especially for fields for when other people are reading my code, so that I'm especially explicit as to what I'm aiming at, so much as I can be. So float EndingPunchPosition. Int MaxPause.

Sometimes you'll see people using kind of cryptic variables I’m not a fan of that, even though they're a little bit longer I just think they're easier to read. So that's why I'm using sometimes very long variables. We don't need Start() right now, so now w’ll kind of utilize our button mapping for this, and set the conditional for that. So if (Input.GetButtonDown(“Punch”) && PlayerState.Instance.Attack == Attack.Passive) So this just makes sure we can’t keep rapid-fire punching, we have to wait till our previous punch had completed. If that's the case, then we'll set these initial conditions, PlayerState.Instance.Attack = Attack.Punch. StartingPunchPosition, we’ll set it to the middle of our CheeseHead.transform.position.x. EndingPunchPosition, we’ll set it to further ahead from the StartingPunchPosition by this amount, say 0.07 units. And to make sure that it punches in front of us, we’ll make use of make use of that DirectionFacing state. So PlayerState.Instance.DirectionFacing. So it's either going to -1 or 1, s we’ll need to cast that to an int for that work. And there we’re going to want the punch to remain paused at the full extension.

So for that we'll use the MaxPause field, say MaxPause is 10. Alright, so yeah that sort of sets the initial conditions for punching attack. Now what we’ll want to do is animate the punching attack, and we’ll do this using the familiar Lerp() method, right? We could have just imported a SpriteSheet for the animation. But I was just kind of honestly too lazy to do that, and this is maybe a bit more pure, just handling it in code. So we'll do it this way. We're going to need some more fields here. So float PunchMotion, and we'll set it to Mathf.Infinity. So Mathf.Infinity is when you need some sort of uppermost floor, or ceiling I guess you could say. You know the opposite of this would be when you need a floor, an absolute floor, you’d use 0. So this is kind of the opposite of that, we’ll need an uppermost ceiling, so we’ll set it to Mathf.Infinity. And we’ll need another one called AttackForce, AttackForce, an Accumulator for the Lerp, and a bool called Recoiling, for when we reverse the animation basically.

Alright so now what we’ll do here is we’ll say, if (PlayerState.Instance.Attack == Attack.Punch) So if we're in a punching attack state, we’ll take the Accumulator and start adding to it Time.deltaTime. So on every frame we’ll just add a little bit more to it, we’ll use that in the Lerp later. And we’ll say if PunchMotion, which we’ll determine later as the the output of the Lerp basically, if it's equivalent to EndingPunchPosition, say AttackPause+= Time.deltaTime * 60. As I mentioned before, at 60 frames per second, this will be roughly one on each frame, so it will pause by a value of one on each frame. And earlier we set MaxPause at ten, so you can probably figure in your mind how this is going to work. So if (AttackPause > MaxPause) then AttackPause = 1. We're basically resetting these values, we’re done with the attack pausing and at the full extension. Then we're going to want to recoil so let’s set the Recoiling bool to true. And then we’ll want to set the Recoiling conditional. Actually before that, I realize I could have just take this out here, and leave it at the bottom of this space because it’s the same conditional. So if (Recoiling) actually well start off with, if (!Recoiling) to set the motion of the initial punching motion, punching forward from the current position of CheeseHead., We’ll say PunchMotion = Mathf.Lerp() StartingPunchPosition to EndingPunchPosItion, and we’ll use the Accumulator now to basically set the rate of that, how fast that happens.

We’ll multiply it by seven; we’ll use this multiplier to make the punch faster or slower. And then we’ll also want to set the localScale. So transform.localScale, we’ll want to Lerp it actually, so the fist kind of gets a little larger in size, a little bit more of an interesting animation to the punching motion. So for that use Vector3.Lerp() and go from the current transform.localScale to, well we’ll want to go to… We’ll say new Vector3(1,1,1) WE’ll have ending Vector3 of that, basically an unmodified localScale. And at this rate, we’ll just kind of Lerp slightly slower than the punching motion itself, so say Accumulator * 5. So that's what's happening if we're extending or punching Fist. And else if, if Recoiling basically, we’ll do this: PunchMotion = Mathf.Lerp() we’ll go from EndingPunchPosition this time back to StartingPunchPosition, and we’ll go a little bit slower when Recoiling, right, not as much explosive force. We're kind of emulating with making the Accumulator multiplier smaller than our extending our Fist. And transform.localScale, also want to Lerp that back to what it was before.

Vector3.Lerp() from its current localScale to that .6 and .5 for X and Y respectively. So we’ll say new Vector3(0.6f, 0.5f, 1f) And Accumulator * 4, Recoiling for this localScale Lerp. And once we get back to our starting position, so with a conditional we’ll say: if (transform.position.x == StartingPunchPosition) we’re no longer Recoiling. And we're no longer attacking. Attack.Passive, right? And might as well reset the Accumulator as well, so we can do this all over again if we want to basically punch again. So finally, none of this actually does anything until, or unless we assign it to the transform.position, so say transform.position = new Vector3(PunchMotion, CheeseHead.transform.position.y, transform.position.z) So this makes sure that the Fist follows the CheeseHead on the Y, as well on the X, with PunchMotion that we've been determining up here. And for Z, we’re just taking the current Fist’s transform.position because the Z value is for depth and we don't really need that. OK now what we're going to need to do is set the initial attack state in the PlayerController.

And we’ll set up a block of code that limits our character's movement when attacking, basically preventing us from moving left or right, or jumping for that matter when we're attacking. So in the PlayerController, make these changes now to the PlayerState. So for Jumping, we're going to want to limit this not only to our having our Y velocity having to be 0, but also be in a Passive attacking state. So PlayerState.Instance.Attack has to be Attack.Passive. For here, we’ll do as well basically wrap our horizontal motion dependent on whether or not we are Passive or not. So what we’ll do is we’ll just create conditional here in Update() if (PlayerState.Instance.Attack != Attack.Passive) we’ll say CheesyBody.velocity = new Vector2(0) And we’ll basically make sure that you keep your velocity, you stop to a halt on the horizontal axis. On the Y axis, we're going to put it to 0.1. Why not 0? Well if we’re at 0, then we'll theoretically be able to jump after we're done punching, right? Even if we're in the air, right, so that's not going to be good.

So we’ll just give it a little bit of velocity on the Y axis to prevent that, but otherwise basically prevents us from falling as well when we’re punching. So we’ll be suspended in mid-air. I think it's a nice little mechanic, so it's easy enough to create and, well just to make sure our horizontal motion is also set at 0. And otherwise, for Passive basically, we’ll say else. And we’ll just just stick all of this in the else clause. Basically accepting our user input, right? And so now hopefully that's all set up correctly and we didn’t make any errors. So let's see here. Alright, let's first try, was it V? Sorry, our PlayerState. Good. And there's our PlayerState, it’s correctly changing from Passive to Punch. And when we punch our character,it kind of nudges it a little bit because the colliders are just intersecting. So what we're going to do now is we're going to fix that. Now what we'll want to do is use the AddForce() method whenever our Fist Collider intersects with an Enemy Collider.

So for this, we’ll use the OnCollision2DEnter() method, which is actually a method that's tied into something called an event. Which is a C# concept that we haven't looked at yet, and we're not really going to look at too carefully. Not really going to go into events, it's outside the scope of this material or a related concept called delegates, but to give you a simple primer on this, a delegate is just a way of holding a reference to a method within a variable. That's pretty much it, and an event is basically a version of a delegate that the intent is to hold a reference to one or more methods that subscribe to it. That's usually language you'll hear in reference to events. So, whenever something happens and the event launches, which is basically like calling an ordinary method, it will also call all of the methods that have subscribed to this event. So that's how events work but you don't need to know all that, it will probably be pretty obvious how this is working. So for this, we’ll go back to AttackController and alongside Update() we’ll also have this method that's tied into the event called void OnCollisionEnter2D() You have to spell it exactly because this is a just like Update() it's a pre-made method, or it's a method that's part of Unity.

So OnCollisionEnter2D() and it takes in a Collision2D called coll. And so here, what we’ll do is have a conditional. So we want to detect when the Enemy and our Fist intersect. So for this, I think I'll show you how to use a gamer.. Not a Gamertag. To use a GameObject tag in order to determine how this works with the OnCollisionEnter2D() So for the… See, it’s untagged here for Dummy GameObject, so we'll set a tag here to Enemy, you’ll see why this is useful. Here in our OnCollisionEnter2D() we’ll just say: if coll, so this takes this takes in the collider of the other Collider, basically that our Fist Collider is interacting with. So if (coll.gameObject.tag) we’re referencing the gameObject property, gameObject.tag == “Enemy”. So this effects only when our Fist interacts with Enemies. If that's the case, then, coll.gameObject.GetComponent<Rigidbody2D>() we’ll get the Rigidbody of the Enemy that we're colliding with, and we’ll just use this dot accessor to right away just use the AddForce() method. Which we’ll say new Vector2() we’ll just assign something here for now. Well first, we’ll want to create a force on the X-axis relative to the direction we're facing. So we'll just say PlayerState.Instance.DirectonFacing, and we’ll cast to a float to make this work.

Multiply that by the AttackForce that we had set up previously as a field. So on the X-axis, w’ll create a force of the amount of AttackForce. I forget how much we set it to. Well, we didn’t set it. Well, we will. Alright. And then for the Y, we don't have to worry, just push it upwards. We’ll use AttackForce, don’t have to worry about minus or plus values like we have to for the X value. And we’ll use again ForceMode2D.Impulse. It's too long of a line I think, so what we’ll do is put this here, and I think of an error here because of a missing parentheses. So we’ll say new Vector2() on that line. AttackForce on that line. And… Whoops, I had an extra parentheses, more than I needed right there. Now I need one here. Boy that looks bad, put this here. There, that looks better, although far from perfect. Hopefully you understand what’s going on there by line separation, looking a little ugly. Sorry about that. So for AttackForce, we’ll just set this all up in a initial state in Start() We’ll say AttackForce = 5.

We could play around this and see what's the the best force to add later on. And I'm not sure if we set the AttackPause, it should be OK but let’s set it here to 1 anyways. As well as the Accumulator = 0.02f, just to these initial values. And for consistency's sake, let's also put PunchMotion set to Mathf.Infinity there as well in the Start() for initialization for all these fields. Yeah, we set Maxpause there. So yeah OK we're good. So let's see now what happens when our Fist Collider collides with our Enemy Collider. Should be adding a force now. Yeah, there we go. Pretty good. Just to expand a little bit on events, even though we're not going to get into it. What's happening here is, in your mind you can picture whenever these Colliders intersect, the Fist Collider or the Eyeball Collider, the event that this method OnCollisionEnter2D() is sort of attached to, or subscribed to, launches. Which in turn just calls this method that’s subscribed to it in the background in Unity. You know, it's a predefined event that's prescribed to, or this method subscribes to in Unity. So why use events? Well events are a good way of executing code without having that code constantly see if it should execute by by polling it, right? By poll, I mean something like how all of our MonoBehaviours all have their own Update() method that, say, would run a conditional check on every frame, polling on every frame and asking sort of a question like, “Are the conditions met for executing this or that method? ” And it does this over and over again.

So with an event, you can have a bunch of methods that, you know, perhaps should all execute when a particular conditional polls as true in an event or an Update() method somewhere. Which is much better than having, say, multiple Update() methods in the same class of each method that should execute, basically polling the exact same thing. It's just a lot more efficient and clean to tie all those methods to a single event that all execute based on a single polling. So It just makes more sense logically and computationally, but events are just a little bit trickier to get your head around when looking at its guts. You know, how events and delegates are structured, so that's why we're not going to go into it at any more than we are right here with a couple of these pre-created events and methods that you have available to Unity. Thankfully you don't even have to fully understand how events and delegates work to make use of these preexisting events in the Unity engine. So with that, we’ll end this lesson and in the next lesson, we’ll move on to creating a projectile attack. See you there.

Previous Lesson | 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