Streamlining Objectives
The original plan for Week 7 was to finish the other two objectives. As I was creating the radio tower objective, I had an idea. I thought it would be more user friendly if the player was given a prompt to interact with the radio tower. I also wanted the player to hold down the interact key to complete the objective. I thought this would build tension while completing objects, forcing the player to leave their back open while completing objectives. This “cool idea” required that I make some major changes to how I was handling objectives. As a result of my work this week, all future objectives will be instances of an objective blueprint object called InteractiveObjective_BP. It took a lot of work to setup, but this new system will speedup the creation of future objectives and is more flexible.
What’s the player looking at?
In order to know when we need to show our progress widget, I needed to add more functionality to the InteractInterface. We now have functions that are called by the player when they are looking at an object that implements the InteractInterface. (I also added the functionality for detecting when the player is finished interacting with an object)
...
public:
// Interface specific to Blueprint
UFUNCTION(BlueprintNativeEvent, Category="Interact")
void Interact();
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
void InteractEnd();
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
void LookAt();
UFUNCTION(BlueprintNativeEvent, Category = "Interact")
void LookAtEnd();
// Interface specific to C++.
UFUNCTION()
virtual void InteractPure() = 0; // = 0 tells the compiler there is no implementation in the cpp file.
UFUNCTION()
virtual void InteractEndPure() = 0;
UFUNCTION()
virtual void LookAtPure() = 0;
UFUNCTION()
virtual void LookAtEndPure() = 0;
These functions and the pre-existing Interact() functions are all now implemented in a class called InteractiveObjective. I am not going to show the code for these files because they are simply empty implementations of the interface methods.
Now that our LookAt() functions are implemented, we need to call them from our player character. The following code is ran every in-game tick.
protected:
...
/** Interacts with object */
void Interact();
void InteractEnd();
IInteractInterface* InteractInterface;
FHitResult PreviousHitActor;
/** LookedAt Interactions */
void IsLookingAtActor();
...
void AGameCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
IsLookingAtActor();
}
void AGameCharacter::Interact()
{
// Get the location of our player's camera and the forward vector
FVector start = FirstPersonCameraComponent->GetComponentLocation();
FVector end = ((FirstPersonCameraComponent->GetForwardVector() * TraceDistance) + start);
FHitResult Hit;
FCollisionQueryParams TraceParams;
// Our Hit variable will be updated with the actor the raycast has hit.
// ECC_Visibility should hit any actor that is visible.
bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, start, end, ECC_Visibility, TraceParams);
// Debug line for testing
DrawDebugLine(GetWorld(), start, end, FColor::Red, false, 1.f);
if (bHit) {
// Check if our Hit actor can be casted to an interface and call the Interact functions
InteractInterface = Cast<IInteractInterface>(Hit.GetActor());
if (InteractInterface) {
InteractInterface->InteractPure(); // For c++ only
InteractInterface->Execute_Interact(Hit.GetActor()); // Blueprint Only
PreviousHitActor = Hit;
}
}
}
void AGameCharacter::InteractEnd()
{
if (InteractInterface) {
InteractInterface->InteractEndPure(); // For c++ only
InteractInterface->Execute_InteractEnd(PreviousHitActor.GetActor()); // Blueprint Only
}
}
void AGameCharacter::IsLookingAtActor()
{
// Get the location of our player's camera and the forward vector
FVector start = FirstPersonCameraComponent->GetComponentLocation();
FVector end = ((FirstPersonCameraComponent->GetForwardVector() * TraceDistance) + start);
FHitResult Hit;
FCollisionQueryParams TraceParams;
// Our Hit variable will be updated with the actor the raycast has hit.
// ECC_Visibility should hit any actor that is visible.
bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, start, end, ECC_Visibility, TraceParams);
// Debug line for testing
//DrawDebugLine(GetWorld(), start, end, FColor::Red, false, 1.f);
if (bHit) {
// Make sure we are not looking at the same actor
if (PreviousHitActor.GetActor() != Hit.GetActor()) {
// Check if our Hit actor can be casted to an interface and call the Interact functions
if (InteractInterface) {
// LookAtEnd for old object
InteractInterface->LookAtEndPure(); // For c++ only
InteractInterface->Execute_LookAtEnd(PreviousHitActor.GetActor()); // Blueprint Only
}
InteractInterface = Cast<IInteractInterface>(Hit.GetActor());
if (InteractInterface) {
// LookAt new Object
InteractInterface->LookAtPure(); // For c++ only
InteractInterface->Execute_LookAt(Hit.GetActor()); // Blueprint Only
PreviousHitActor = Hit;
}
}
}
else {
if (InteractInterface) {
InteractInterface->LookAtEndPure(); // For c++ only
InteractInterface->Execute_LookAtEnd(PreviousHitActor.GetActor()); // Blueprint Only
InteractInterface = nullptr;
PreviousHitActor = Hit;
}
}
}
Creating our blueprints
With the LookAt() functionality in place, we can now create UI widgets whenever our player is looking at an objective. To do this, I created a blueprint called InteractiveObjective_BP that is based off of our InteractiveObjective class.
Whenever the player is looking at the objective, we create a new UI widget that will show their progress. If the player interacts with the object, we start a timer that will update the progress bar widget. We also call an event dispatcher called Player Action. This delegate is important because we will bind events to this delegate on future child instances. Example: attaching our generator refuel code to Player Action.
The new Generator Objective
With our parent InteractiveObjective_BP created, we can now create a child instance for our new and improved generator
Most of this code is the same as our original Generator Objective, but it now has parent methods for creating a progress widget. This means that we can add future features such as sound effects very easily in our parent blueprint.
We can also override certain parent methods to add custom functionality for specific objectives. For example, we are able to only show the interact widget if the player is currently carrying fuel:
You can see that we run the Should Show Widget method in the parent and check the results with an if statement
Demo Time!
As you can see, it works! Our fuel tank and generator objectives both have progress widgets attached to them. Additionally, the generator’s widget is not visible until the player has collected fuel.
Bonus: New Generator Model
As you can see in the demo, I also created a new model for the generator. Here is a better picture of it:
I livestreamed the process of creating this model. You can view the unlisted livestream here.
That is it for this week! Next week I intend on quickly finishing the remaining two objectives. After I finish the objectives, I intend on creating the AI enemy that will be stalking our player while they try to complete their objectives.
Thanks for reading!