UHG Week 7 – Code cleanup and UI

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.

Note: you can click on these images to open them in a separate tab.

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 Update Timer Event is called immediately after the Timer Event is called. The Update Timer Event will be called every 0.1 seconds until the Max Hold Time is reached OR if the player releases the Interact button prematurely.
This is what the widget looks like. The label is updated with the currently bound interact key and the name of the action that will be completed. This is done in the widget’s constructor.
HoldWidget‘s constructor

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

This is a zoomed in picture of our InteractiveObjective_BP shown earlier.

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!

Leave a comment

Your email address will not be published. Required fields are marked *