Skip to main content

Week 10 - Projectiles & Damage

Learned This Week

Overview

Wow, week 10! This semester has absolutely blown by, and while I feel I've learned a lot, there's certainly more out there for me to explore. However, I've come to the end of my structured learning for this study. While I'll keep learning and looking around for further resources to help me along my journey, no further course materials will be discussed in the next blog posts for this semester.

Instead, my posts will be centered around my development process for the final project for this course. As covered previously, the project will mimic the Unreal content examples maps and is going to display the key chunks of my learning throughout this process. I'm planning on splitting it up into three levels: one covering C++ on its own (outside of the context of Unreal), one covering Object-Oriented Design (somewhat applied to Unreal, but mainly on its own), and the last centered around the application of C++ to the engine.

And with that, here's the cool info I've learned this week! A lot of the information is stuff I've already learned, but it helped to solidify the concepts (and I also learned some caveats and special information about the topics as well!).

Key Points:

  • Casting
    • I've gone over casting a few times already, and I've worked with it often in blueprints as well, but a quick point that was covered this week that "clicked" in my head was that a cast attempts to convert a variable of one type to another - that cast needs to be "possible," meaning the new type needs to either derive from the parent type or be convertible in some other way (primarily casting is used for derived classes though). Otherwise, the cast will fail. I understood the concept before, but that small bit of info never quite clicked until now!
  • TSubclassOf
    • When creating a variable, sometimes it's advantageous to allow the variable to be set to multiple different classes (like when choosing what type of projectile to fire, or when allowing the variable to be set to a blueprint-derived class of a C++ class). In order to do this, Unreal has a template variable type, TSubclassOf. The template allows for the selection of any class derived from the specified class. For instance, we could choose a blueprint derived from our C++ class Projectile if the variable was set as TSubclassOf<AProjectile>.
  • Projectile Movement
    • As Unreal is a game engine, it has a lot of built-in features designed to be reused for game applications. One of these handy dandy features is the Projectile Movement Component, which is designed to handle the behavioral movement of a projectile. Projectile Movement Components are placed on the projectiles themselves, and handle the movement of their owning actor.
  • Delegates
    • While I've studied delegates before, I was only controlling them within the same file. However, they're much more flexible than that. Delegates created and broadcast from one C++ class can be bound in another class.
    • Additionally, binding a function to a delegate adds it to that delegate's "invocation list," or the list of functions to call when the delegate is called/broadcast to.
    • It's worth noting that any function that is created and bound to a delegate has to match the parameter list of the calling delegate.
  • Damage Application
    • Another one of Unreal's built-in systems, damage can be easily handled by using the ApplyDamage function that's part of Gameplay Statics. We can bind an event in any actors that should be able to take damage to the OnTakeAnyDamage delegate. The setup takes care of a lot of common implementations of damage in games such as damage amount, type (i.e. environmental, projectile, melee, etc), what actor did the damage, and what controller instigated the action that did the damage.

Nitty-Gritty

  • Unreal 5: Learn C++ GameDev.tv Course
    • Creating Child C++ Classes
      • As we'll want to reuse the information (components) from our BasePawn class in both our Tank and Tower pawn classes, we can derive inherited classes from the BasePawn class to implement as the parent class for our blueprints
      • In the Tank class, we want to add a spring arm and a camera attached to it:
      • Header file:
      • C++ file:

    • Handling Input
      • Using the same learning from previous modules, we can set up player input mappings for our Tank class:

    • Local Offset
      • When we're moving the pawn, we want to take into account whether the movement offset we'll be adding should be in the local or the world axis
      • Our pawn will be rotating, so we should be moving it in local coordinates
      • To do this, we can use the AddActorLocalOffset function:

    • Movement Speed
      • It's good practice to be multiplying any value that changes on framerate update by DeltaTime in order to maintain consistent value differentials between frames
      • As we're not working within the tick function, we need to provide the DeltaTime ourselves to the function by calling the GetWorldDeltaSeconds function from GameplayStatics:
      • Additionally, we want to be able to check for collisions, so we can sweep with the function:

    • Local Rotation
      • Similarly to our forward and backward movement, we want to bind our turn keys to a function which rotates the tank the same way we implemented our movement:

    • Casting
      • Next, we want to be able to rotate the turret part of the tank in order to look at the location where our mouse cursor is on screen - in order to get that location, we can use a function on the PlayerController which means we'll need to cast to that controller in order to access the function
      • We need to cast the variable when we set it because the function on APawn to get the controller returns a type of AController, and we need a type of APlayerController
      • The cast looks like this when set up:

      • This only works because APlayerController is a class derived from AController
    • Using The Mouse Cursor
      •  Now that we've set up that pointer to our PlayerController, we can use the GetHitResultUnderCursor function to return a hit result from where our cursor is on the screen like so (implemented here with a debug sphere drawn):

      • The resulting movement and mouse reaction of which looks like this:

    • Rotating The Turret
      • Now that we have this point we want the turret to rotate around to look at, we can start affecting its rotation based on the cursor point in the world
      • We can use the .Rotation() method on our vector in order to get its look at rotation
      • Because we'll eventually want to do this for our turrets as well, we should implement this function on the base class:

      • The resulting turret rotation which comes out of that function is as follows:

      • However, the problem we can see fairly clearly is that the rotation doesn't have any interpolation between frames, so it's jerky and sporadic
      • To make it nice and smooth, we can interpolate every time we call the function between the current rotation and the target rotation (and as we're calling the RotateTurret function from Tick, we can just pass the DeltaTime along without needing to get it again from our GameplayStatics):

      • The resulting smooth rotation out of that is as follows:

    • The Tower Class
      • Now that most of our functionality we'd want for our Tank class is implemented, we can start on our Tower class
      • We want the tower to be able to look at the player if the player is within its range, so we need to get a hold of the player which we can do in the BeginPlay of the Tower class by storing it in a pointer:

      • And since most of our implementation has already been set up on the BasePawn, all we need to do is check whether the player is within the firing range of the tower, and if so rotate the tower's turret:

      • The result of which looks like this:

    • Fire
      • We can start working on the function we'll want to use to fire projectiles from the tower and turret, and we can implement it on the BasePawn class as both classes will need to have the ability to fire:

      • And we can bind that to our Fire input event in our Tank class:
      • Which gives us a basic implementation of what we'll want to do in order to actually fire a projectile
    • Timers
      • Because our Towers are not accepting user input, we'll have to tell them to fire in a different way (automatically) and we can use a timer to do so
      • We can set a timer for our CheckFireCondition function (which runs the same code that checks if the tank is in range but fires rather than rotating the turret) like so:

      • Additionally, we can migrate our fire range checking into a function and use it in both Tick and CheckFireCondition to reduce copied code:

      • And we can then use that to create our CheckFireCondition function:

      • Which results in this behavior:

    • The Projectile Class
      • We can quickly create a new class derived from Actor which we can use as our projectile class and create a blueprint based on that C++ class in order to be able to spawn a projectile from our C++ Fire function
    • Spawning The Projectile
      • Actors can be spawned in the world by calling the SpawnActor function on the world object where the actor needs to be spawned
      • However, we don't want to spawn the C++ version of the actor, we want to spawn the blueprint derived class we created so we can use the TSubclassOf template type to do so as it has the ability to interface with Unreal's reflection system
      • When created on our BasePawn class, it looks like this:

      • And as our BP_Projectile class we created is a class derived from our AProjectile, we can select the blueprint class in the defaults of our Tank and our Turret blueprint classes

      • Which we can then use in our Fire function to spawn an actor of that class:

    • Projectile Movement Component
      • And of course, the next thing we want to be able to do is make our projectiles move, and we can use an Unreal-created class to do so called a ProjectileMovementComponent which will handle the behavior of the projectile
      • The C++ construction is fairly simple as the component does most of the work for us:

      • And the projectile that spawns looks like this:

    • Hit Events
      • Now that we have moving projectiles, we want to be able to do something when a projectile hits an object
      • Primitive components have a few different delegates by default, one of them being OnComponentHit which is a multicast delegate
      • We can add one of our own functions to the invocation list of the OnComponentHit delegate on our ProjectileMesh
        • Invocation lists are a list of functions that should be called on a multicast delegate when it broadcasts
      • To do so, we can use the .AddDynamic method in our Projectile's BeginPlay like so:

      • And implement the OnHit function with the information that the delegate will end up passing (it needs to be a UFUNCTION in order to be seen by the reflection system and be bound to the invocation list of the delegate):

      • And at the moment, all the function does is print the name of the actor the projectile hit, and then destroys itself:

    • Health Component
      • In order to handle health for a given actor, we're going to use a custom component in order to make it reusable on multiple actors (i.e. on the Tank as well as on the Turret)
      • Because the component will be a "handler" per se, in that it won't need to actually exist in the world, we can implement it as an ActorComponent rather than a SceneComponent
      • We can set up some variables that we'll want to use like so (it's worth noting that we'll set our Health to MaxHealth in the component's BeginPlay in order to avoid setting the Health value directly from blueprint to keep our encapsulation solid):

      • Then, we can add our HealthComponent as another component on the BasePawn class in order to make it inherited by both the Tank and Turret classes
      • There's a function on GameplayStatics which is called ApplyDamage which calls an object's OnTakeAnyDamage delegate, which we can bind a callback function to on our HealthComponent
      • Our created function on our HealthComponent takes the following inputs (in order to match the delegate outputs):

      • And we can bind the function to the delegate of whatever Actor that owns the HealthComponent like so:

    • Applying Damage
      • The next step is to actually apply that damage and force our OnTakeAnyDamage delegate to fire by calling the ApplyDamage function on GameplayStatics
      • In order to call the ApplyDamage function, we need to provide some information about what is being applied and where, we need to tell it:
        • The actor that should receive the damage
        • The quantity of "base" damage to apply
        • The Controller that was responsible for causing the damage (in our case, what controller shot the projectile)
        • The actor that was actually responsible for the damage itself (in our case, the projectile)
        • And the type of damage done, as a DamageType subclass
      • As we'll be calling the ApplyDamage function from the hit event of our projectile, we can get those parameters in the following ways:
        • The actor receiving the damage will just be the actor that the hit event comes into contact with
        • We can create a variable on our Projectile class to store the damage it should apply
        • When we spawn a projectile from our BasePawn, we can set its owner to be the pawn it was fired from, and that lets us get the instigating controller
        • Because the function is being called from the projectile itself, the actor responsible for the damage would be itself, so we can just use this
        • As we don't have multiple types of damage in our game, we can just use a StaticClass of UDamageType
      • And the resulting function call is as such:

      • The Destroy() function call at the end not only takes care of only applying the damage once as the Hit event will only call once, but it also takes care of destroying the projectile if it hits anything that can't take damage, i.e. the wall or another projectile
      • Because we've already bound the DamageTaken function to the OnTakeAnyDamage delegate, we can start implementing the basic information we need to negate health from the HealthComponent:

      • The output of which looks like this:

    • Handling Pawn Death
      • While we've clamped the value of the HealthComponent to 0, nothing happens yet at that point
      • In order to make the death of a pawn extensible & applicable to multiple actors, we'll create a function on our GameMode class to send a notification to an actor that it has died, then the actor itself can handle its destruction
      • While this is certainly not the most efficient (or encapsulated) way of handling death, it's still a way of doing things
      • Our HealthComponent calls the ActorDied function on the GameMode class if the damage it has been dealt would have put it at 0 health:

      • And the GameMode then checks to see what type of actor died and performs different functions in each case (to be clear, this is a bad implementation of encapsulation, as the pawns themselves should be handling this code):

      • And each pawn implements a different version of the HandleDestruction function on the BasePawn class
      • Which results in this gameplay experience: