Skip to main content

Week 9 - Dependencies, Casting, & References

Learned This Week

Overview

We're in the home stretch of learning now! I finished up the Balancing Blueprint & C++ course this week which just leaves the rest of the GameDev.tv course to finish up before I begin my final project for my independent study.

The end of the Balancing Blueprint & C++ course primarily focused on applying the learning from last week's modules to improve the template content and code. I learned how to identify and remove directory (hard file path) references from C++ code in favor of values exposed to and set from blueprint as well as how to dynamically switch between class types (inherited from a specific parent type) using the TSubclassOf template variable type.

Additionally, I finished up the current project in the GameDev.tv course called CryptRaider (the full playthrough can be found below) and got started on the next one called Toon Tanks. I plan to finish that project up next week and gear up to begin my final project during week 11.

The final project for this independent study will mirror the Unreal Content Examples style of level, and will be aimed at displaying, implementing, & explaining the major (and minor) key points I've learned throughout the study.

Key Points:

  • Dependency Injection
    • When a reference is needed to an object that is not directly attached to the thing you're coding, it's hard to get a reference to it in order to perform operations. A way around this is by using dependency injection - setting up empty references which you can set at runtime.
    • This is often done by creating a public setter function for your private access variable and calling that function (almost exclusively in blueprint) from the class itself to set up the reference correctly at runtime.
  • Eliminating Hard References
    • Whenever possible, specifically when working in Unreal, it's best to avoid using hard references to file locations, as those file locations can change and the editor doesn't notice it until the code is recompiled and the file isn't in the same place.
    • Instead, it's best to set up the base components in C++ and their contents in blueprint in order for Unreal's reflection system to either fix the reference itself or at least notify the developer that the reference has been broken and needs to be reset to the asset's new location.
  • Forward Declaration
    • When creating variables of types not included in your current header file, it is not recommended to include the header file of that class type within your header file. This leads to larger compilation times and a lot more dependencies when that header file is then included in others.
    • Instead, you can use forward declaration and include the class type header in your .cpp file.
    • Forward declaration allows you to declare an "empty" class as a type in the header file, then fill in the details later in the .cpp file, as the compiler will figure out what the class is supposed to be as long as the class type header file is eventually included in the .cpp file.
    • An example of setting up a UStaticMeshComponent variable with forward declaration would be:
      • Header File:
        • class UStaticMeshComponent* Mesh;
      • C++ File:
        • #include "Components/StaticMeshComponent.h"
  • Casting
    • Casting in C++ is an operation which forces a variable of one type to be converted into another. In Unreal, it's especially useful for taking Actor references and converting them into class-specific variables. It's not a magic wand that always does what you need as you're required to provide it some input (it needs an object to operate on and a type specified), but it's very helpful in some cases.
    • However, casting creates a hard reference between classes and is generally not used unless fully required. Additionally, it needs to include the other class type, so casting can be expensive if you are casting to complex types (basic engine types are usually alright).
  • UPROPERTY Meta
    • As a quick but helpful note, private variables in C++ can be made accessible to blueprint (and stay private in C++ to maintain encapsulation) by using the AllowPrivateAccess meta tag and setting its value to true.

Nitty-Gritty

Jump to:
  • Unreal 5: Learn C++ GameDev.tv Course - Jump to Top
    • Dependency Injection
      • Our Grabber component in C++ has a dependency with (is dependent on) the PhysicsHandle attached to our player - it can't do its work without it
      • However, in a case where the object our code is dependent on is not connected to the code object itself, we need to use dependency injection
      • For the wall of the crypt that we want to make move downwards when the trigger component starts working, we need to create a dependency injection between the trigger and the mover
      • To do this injection, we can make a function on the TriggerComponent to set the mover component pointer that takes a mover pointer as an input like so:

      • We can then call that function on BeginPlay of the blueprint in order to set the dependency in the TriggerComponent:

      • And now in the TriggerComponent's C++ code, we can use that pointer in order to call functions like setting it to move when an acceptable actor is detected in the TriggerComponent

      • However, a problem with this setup is that the gargoyle will get kicked out of the alcove before the wall is finished closing, so we're going to take a look at attaching it in the next module - the current setup looks something like this:

    • Casting & Actor Attachment
      • In order to resolve the issue we had in the previous module, we're going to attach the gargoyle actor to the alcove and disable its physics (so it slides seamlessly with the alcove into the ground)
      • To attach the gargoyle actor, we can use the AttachToComponent method of the AActor class
      • And to disable its physics, we can use the SetSimulatePhysics method of the UPrimitiveComponent class (and set it to false)
      • We need to be able to call the methods on the right type of class, and in order to do so we need to cast our gargoyle's root component to the right class (done with a Cast template function) like so:

      • Which we can then use if it's not a nullptr to attach the actor as well as disable its physics:

      • And as a result, the door goes down all the way:

    • Adding and Removing Tags
      • We want to be able to query what object is grabbed by the player at any given time, and one of the ways to do that is to dynamically add and remove gameplay/actor tags as we grab the object
      • Thankfully, this is fairly straightforward to implement in our Grabber component as it already gets the references we need in its Grab and Release functions so we can just add and remove the appropriate tag in those functions:


      • We can now use that tag moving forward to help the alcove know if the object it is attempting to attach is a grabbed component
    • Boolean Logical Operators
      • In order to avoid the alcove snatching the gargoyle out of the player's hands awkwardly, we want to be able to test if the gargoyle is both in the trigger component and not currently being held
      • In order to do that, we can run an if statement with a logical AND operator (&& or and)
      •  So where we check if there's an acceptable actor in the overlap zone, we can add the Grabbed tag to its if statement like so in order to only return an Actor pointer if the Actor has an acceptable tag and is not actively being grabbed:

      • And the resulting door closing looks like this:
    • Project Completion: CryptRaider
      • Now that we've created these components and systems that can work together, we can apply them to other parts of the level to create a full game environment. The finished product looks like this:

    • Project Intro: Toon Tanks
      • The next project in this course will be centered around creating a cartoonish tank game where the player is a small tank rolling around that can fire projectiles at enemies who have some basic AI allowing them to fire back at the player
      • The level and assets for the game have been included with this project, as it will be more focused on the code implementation for the game features - the basic input and action mappings are also already set up
    •  Pawn Class Creation
      • We're going to inherit both our enemy and our player from the same base Pawn class which will have a capsule component, the ability to look around, and the ability to fire projectiles - those derived classes can then use the same code without having to copy it manually
      • Because the Character class is geared towards bipedal characters (using the CharacterMovement component), it's not going to be very useful for our implementation which is a tank that will move around, so we'll use the Pawn class in order to have our pawn receive input (additionally, this is how we would have set up the AI enemy as well as it needs to receive input from an AI controller)
    • Creating Components
      • There are a few things we'll want to do with this base pawn class:
        • Create a capsule component and set that as the root component for the class
        • Create and attach some static mesh components for the meshes of the player and enemies
        • Create and attach another scene component to use as a locator for where projectiles should spawn when fired
    • Forward Declaration
      • Once we set up those components in C++, they will be inherited by any derived classes, C++ or blueprint
      • If we start setting up a UCapsuleComponent, we'll get an intellisense error because the class isn't known to the header file - we could include it, but it would bog down our header file
      • Instead, we can use forward declaration, where you define a class with no additional information in the header file, then include its engine file in the .cpp file for our class - that way, we can resolve the compiler's confusion at an unknown class in our header file without including the class information until the .cpp file:

      • The advantage of this is if we were to include our header file in another class, the capsule component class information doesn't get copied over as well, meaning smaller file sizes for each header file and all the declarations still get resolved!
      • Whenever possible, forward declaration is highly recommended to decrease file sizes and compilation times
    • Constructing The Capsule
      • Now that we've created the capsule component pointer, we can start setting it up as our root component in our class' constructor
      • Applying knowledge from previous modules and courses, this is how that would be done:

      • With the resulting pawn being set up like so if placed in the world:

    • Static Mesh Components
      • We need two new static mesh components which we can set up using a similar method, one for the base mesh and one for the turret mesh
      • Header file

        • Note that we don't need to forward declare this as Pawn classes include the UStaticMeshComponent class by default
      • C++ file 
      • And as an additional step, we can set up our projectile spawn point the same way as a SceneComponent:

    • Deriving Blueprint Classes
      • Now we can begin deriving blueprint classes from our base C++ class
      • Once we create a blueprint derived from the BasePawn class we created, we can open it up but we can't edit any of the details of the StaticMeshComponents we have set up in C++ because we haven't exposed them to blueprints - we can do that using UPROPERTY specifiers in the next modules
    • Instance vs Default, Editing Exposed Variables, & Exposing The Components
      • When working with UPROPERTY variables, it's important to know the difference between Defaults and Instance - if a property is labeled "Anywhere" it can be edited/viewed either in the class defaults or in an instance of an object, but if either is labeled "Only" then they can only be edited/viewed in that location
      • Defaults refers to the class defaults pane, and will apply to all instances of the class unless overridden
      • Instance refers to when an instance of the class is actually placed in the world
      • When accessing variables from blueprint, the variable needs to be either BlueprintReadOnly or BlueprintReadWrite - we'll run into an issue attempting to mark our variables as blueprint accessible as they are all private variables
      • A way to get around this in blueprint but still have the variables private in C++ is to add a meta tag of AllowPrivateAccess and set it to true like so:
      •  
      • This way, the variables on the C++ side can stay encapsulated and private, but on the blueprint side and in the editor, they can be accessed (read only as of now)
      • Once we've done so, we can set the meshes and adjust the capsule components for each pawn class we created:

  • Balancing Blueprint and C++ in Game Development Course - Jump to Top
    • GameMode Improvements
      • In order to set up additional functionality (flexible class assignment) in our GameMode, we need to make a new GameMode class for the project in blueprint deriving from the TopDownShooterGameMode (the one that comes with the template)
      • Which will allow us to dynamically set those classes without changing and recompiling our C++ code:

    • Projectile Improvements
      • We want to be able to swap out a few different types of projectiles in-game with different properties - we can make derived blueprint classes for that
      • We'll want to swap between a default speed, a fast, and a slow speed projectile so we can make 3 blueprints deriving from the C++ projectile class with different values for their ProjectileMovement components
    • Pawn Firing Improvements
      • And now we get to the fun bit - applying the information from last week's modules to spawn a projectile of a specific class by including it in our SpawnActor call in the Pawn C++ file
      • In order to do this, we can declare a subclass variable on the class and use that variable in the SpawnActor call in order to set the type projectile we want to spawn in the editor:

    • Removing Directory References
      • As outlined in last week's modules, hard referencing assets via string is not a great idea as if anything in the directory path changes, the reference will be broken so we're going to go through the template project and remove those references
      • In order to do so, we need to derive a blueprint class from our C++ class, then set the references in the blueprint (as they will be set to None because we are removing them)
      • Additionally as a nice side effect, we can delete a few includes from our files as we'll set up the component references in-editor rather than through the C++ constructors
      • This way, the editor knows what references are needed for each class and each blueprint, so if any changes are made, the editor can either fix them or alert the developer that a change was made and it needs to be remedied which is the suggested workflow implementation of references to assets in-engine
    • Course complete!