Section 3: Lockon System

In this section, we talked about how to lock onto enemies during combat so that the player doesn't have to adjust their camera during gameplay constantly.

Debugging File Updates

In this lecture, we learned how to debug a problem whenever updating a declaration file. When you update a declaration file, and compile Unreal, it's possible that Unreal may be unable to recognize the variables and functions on the class. In that case, you can do one of three things:

  1. Compile the blueprint.

  2. Restart the Unreal editor.

  3. Remove and add the component again.

This solution also applies whenever you're unable to find a variable or function on a component after updating it.

Creating an Actor Component Class

In this lecture, we learned how to create a component class in C++. Unreal takes a composition approach to building actors. Behaviors are added to specific components instead of adding all available features directly to an actor. For this reason, we're going to outsource a lot of the logic for the player into components to keep our project clean and organized.

To create an actor component in C++, you can go to Tools > New C++ Class and select Actor Component from the list of options. In total, we created 6 components:

  • Combat/LockonComponent

  • Characters/StatsComponent

  • Characters/PlayerActionsComponent

  • Combat/CombatComponent

  • Combat/BlockComponent

  • Combat/TraceComponent

Adding Input Actions

In this lecture, we created an input action to activate the lock-on feature. Locking onto enemies shouldn't happen automatically. Players should trigger this behavior by pressing the tab key. To listen for input, we created an input action, which is a file to describe the action being performed. We called it IA_Lockon.

Next, we added this input action to the input mapping contexts, which is a group of input actions available in our game. We bound the tab key to our IA_Lockon action.

Lastly, we defined a function that logs a message to the console.

void ULockonComponent::StartLockon()
{
	UE_LOG(LogTemp, Warning, TEXT("Lockon Started!"));
}

When adding the input action to the player, we connected the Started pin to the StartLockon function. We used it because it only gets called once. Here are the following pins we discussed:

  • Triggered - Executed multiple times when an action has been performed, and all conditions are met.

  • Started - Executed once when the action is performed.

  • Ongoing - Executed multiple times when action has been performed but not all conditions are met.

  • Canceled - Executed when an action starts but all conditions haven't been met.

  • Completed - Executed when an action is finished.

Resources

Logging - https://dev.epicgames.com/documentation/en-us/unreal-engine/logging-in-unreal-engine

Understanding Tracing and Channels

In this lecture, we talked about how tracing is a feature for detecting actors in a game. Before performing tracing, we'll need to add a channel. Channels can be thought of as categories to help us determine how actors collide each other. We can set a channel to either Ignore to let objects pass-through each other or Block to force both actors to detect each other.

We decided to add a custom channel for fighters by navigating to Edit > Project Settings > Engine > Collision. We added a new trace channel called Fighter with a default response of Ignore.

By setting it to ignore, all collision objects will ignore each other on this channel. For this reason, we must update the player and boss by setting the Collision Preset to Custom and setting the Fighter channel to Block like so:

Resources

Performing Traces With C++

In this lecture, we began performing a trace with Unreal's trace system using its C++ functions. To do so, we must call the GetWorld() function to grab a reference to the world, which contains the functions for performing sweeps.

GetWorld()->SweepSingleByChannel();

The SweepSingleByChannel() function can be used to trace actors with a specific channel. In addition, it only returns the first result it finds if there are multiple results. You can use the SweepMultiByChannel() function to grab all results instead.

Afterward, we started to pass in the configuration settings.

FHitResult OutResult;
FVector CurrentLocation{ GetOwner()->GetActorLocation() };
FCollisionShape Sphere { FCollisionShape::MakeSphere(Radius) };
FCollisionQueryParams IgnoreParams{
	FName(TEXT("IgnoreCollisionParams")), false, GetOwner()
};

bool bHasFoundTarget { GetWorld()->SweepSingleByChannel(
	OutResult,
	CurrentLocation,
	CurrentLocation,
	FQuat::Identity,
	ECollisionChannel::ECC_GameTraceChannel1,
	Sphere,
	IgnoreParams
) };

Here's what we're doing:

  1. The first argument is a variable for storing the target found by the trace.

  2. The second argument is the starting point for the trace.

  3. The third argument is the ending point for the trace. We're using the same value as the second argument since we're only interested in finding the enemy within a certain distance of the player.

  4. The current rotation of the trace shape. FQuat::Identity can be used when you don't care to apply a rotation. The shape will use the same rotation as the world.

  5. The trace channel we added. You can check the config/DefaultEngine.ini file for the proper channel enum for custom channels.

  6. The shape you would like to draw. In our case, we're drawing a sphere.

  7. A list of actors to ignore.

For the sphere, we're going to allow the radius to be configurable through a parameter on the function, which is available via a blueprint. It's perfectly acceptable to add parameters to functions with default values like so:

void StartLockon(float Radius = 750.0f);

After performing the trace, we checked the results and outputted the actor's name.

UE_LOG(
	LogTemp, 
	Warning, 
	TEXT("Actor Detected: %s"), 
	*OutResult.GetActor()->GetName()
);

Locking the Camera and Player

In this lecture, we worked on disabling the camera from being moved with the mouse and disabling the player from rotating when moving. First, we need some references.

ACharacter* OwnerRef;

APlayerController* Controller;

class UCharacterMovementComponent* MovementComp;

We're grabbing the following:

  • The ACharacter class gives us access to functions for grabbing the movement component.

  • The APlayerController class is responsible for controlling where the player looks.

  • The UCharacterMovementComponent class can be used for controlling the player's rotation. It's already available on our character.

Next, we assigned these variables values from the BeginPlay() function.

OwnerRef = GetOwner<ACharacter>();

Controller = GetWorld()->GetFirstPlayerController(); 

MovementComp = OwnerRef->GetCharacterMovement();

Grabbing the player's controller is only available via the world context since multiple player controllers can exist in a single game instance. We're using the GetFirstPlayerController() function to grab the first controller. Since our game is single-player, we're guaranteed to grab the current active player. As for the movement component, that's possible through the ACharacter class with the GetCharacterMovement() function.

After grabbing that information, we can disable the player's ability to look around by calling the SetIgnoreLookInput() function.

Controller->SetIgnoreLookInput(true); 

MovementComp->bOrientRotationToMovement = false; 
MovementComp->bUseControllerDesiredRotation = true; 

As for modifying the player's rotation, we can stop them from rotating toward their movement direction by setting the bOrientRotationToMovement to false. Since we're responsible for controlling the player's rotation, we can enable the bUseControllerDesiredRotation variable to true to help with smooth rotations.

Rotating the Player Toward the Target

In this lecture, we used the UKismetMathLibrary class to help us find a proper rotation for the lockon behavior. First, we need a reference to the actor we want to target. We defined a variable in the declarations file to store the reference.

AActor* CurrentTargetActor; 

Next, we updated this variable when find a target from the StartLockon() function.

CurrentTargetActor = OutResult.GetActor();

After grabbing the target, we moved onto updating the TickComponent() function, which gets called on every frame.

if (!IsValid(CurrentTargetActor)) { return; }

FVector CurrentLocation{ OwnerRef->GetActorLocation() };
FVector TargetLocation{ CurrentTargetActor->GetActorLocation() };

FRotator NewRotation{ UKismetMathLibrary::FindLookAtRotation(
	CurrentLocation, TargetLocation
) };

Controller->SetControlRotation(NewRotation);

We're doing a few things here.

  1. Checking if the CurrentTargetActor variable has valid value.

  2. If it does, we're storing the player's and target's locations.

  3. We're passing these locations onto the FindLookAtRotation() function, which will handle calculating an appropriate rotation.

  4. Lastly, we called the SetControlRotation() function to set the rotation with the NewRotation variable.

Resources

Adjusting the Camera

In this lecture, we took the time to adjust the camera to have a better view of the enemy and smoothly move around. To create smooth movement, we modified the Camera Boom comonent by enabling the Enable Camera Lag Rotation option.

To move the camera, we did two things. Firstly, we updated the TargetOffset variable on the SpringArmComponent by adjusting the Z-axis. This will position the camera higher above the player.

SpringArmComp->TargetOffset = FVector{ 0.0, 0.0, 100.0 };

In addition, we adjusted the calculated rotation by moving the target's location a bit lower so that the camera looks at the feet of the target instead of directly at them by updating the Z variable on the TargetLocation variable.

TargetLocation.Z -= 125;

Breaking the Lockon

In this lecture, we learned how to break the lockon behavior when the player moves far away from the enemy. First, we defined a variable for configuring the breakaway distance that is also editable from the Unreal editor.

UPROPERTY(EditAnywhere, BlueprintReadWrite)
double BreakDistance{ 1000.0 };

Next, we updated the logic inside the TickComponent function to check the distance on every frame. We used the FVector::Distance function to grab the distance between two vectors.

double TargetDistance{
	FVector::Distance(CurrentLocation, TargetLocation)
};

Lastly, we compared both variables. If the distance is greater than the BreakDistance variable, we ended the lock on and returned the function early.

if (TargetDistance >= BreakDistance)
{
	EndLockon();
	return;
}

Resources

Adding a Target Widget

There are no notes for this lecture. Please watch the video.

Resources

Lockon Image

Adding Interfaces

In this lecture, we learned how to apply an interface to a class. Interfaces are a feature in Unreal to force classes to define a set of functions. It also allows us to grab a class without having to know every detail about it.

When creating an interface, two classes will be made. One with the name prefixed with a U and another prefixed with an I. The class with the U prefix is used to validate a class, while the I version is the actual interface itself.

To apply an interface, you must include it and then add it as an additional class to inherit. For example, we have an interface called IEnemy that was implemented on the BossCharacter class. Here's what that looks like.

#include "Interfaces/Enemy.h"

UCLASS()
class ACTIONCOMBATDEMO_API ABossCharacter : public ACharacter, public IEnemy
{
	
};

Validating Interfaces

In this lecture, we validated the interface on an actor by using the Implements function that is available on all actors.

if (!OutResult.GetActor()->Implements<UEnemy>()) { return; } 

When using this function, be sure to use the interface class name prefixed with the letter U and not I.

Implementing Interface Functions

In this lecture, we defined functions on our interface, called them and implemented them from our blueprint. To define a function that can be implemented in a blueprint, you must add the UFUNCTION(BlueprintImplementableEvents) macro.

UFUNCTION(BlueprintImplementableEvent)
void OnSelect();

We can call this function by calling it with the Execution_ prefix, which Unreal handles defining for you.

IEnemy::Execute_OnSelect(CurrentTargetActor);

When calling this function, you must pass on a reference of the actor with the interface.

Lastly, you can implement the interface like so from a blueprint:

Combat Locomotion

There are no notes for this lecture. Please watch the video.

Observer Pattern

In this lecture, you'll learn how to use the observer pattern, which is a pattern for telling other classes when an event happens in another class. To define an event, we must use the following macro:

DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam(
	FOnUpdatedTargetSignature,
	ULockonComponent, OnUpdatedTargetDelegate,
	AActor*, NewTargetActorRef
);

This macro should be defined from within the lock on component's declaration file. This macro has a few arguments.

  • The event name.

  • The class where the event originates.

  • The variable for storing information on the event from the same class.

  • The type for the parameter.

  • The parameter name

There are other variations of this macro available:

  • DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_OneParam - When an event has to send one parameter.

  • DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE - When an event does not have to send additional information.

  • DECLARE_DYNAMIC_MULTICAST_SPARSE_DELEGATE_TwoParams - When an even has to send two parameters.

After defining the event, we must define the variable that contains the event information in the class:

UPROPERTY(BlueprintAssignable)
FOnUpdatedTargetSignature OnUpdatedTargetDelegate;

This variable should be public so that other classes can listen to this event. It's also advised to add the BlueprintAssignable so that the event can have functions assigned to it from the blueprint graph.

To tell other classes of the event, we must call the Broadcast function with the parameter mentinoed from the macro.

OnUpdatedTargetDelegate.Broadcast(CurrentTargetActor);

Calculating the Player’s Direction

In this lecture, we finalized our lockdown behavior by updating the animation instance's boolean and direction variable. We have two functions for handling when the player locks onto an enemy and when the direction should be updated. In the declaration file for the PlayerAnimInstance class:

UFUNCTION(BlueprintCallable)
void HandleUpdatedTarget(AActor* NewTargetActorRef);

UFUNCTION(BlueprintCallable)
void CombatCheck();

The implementation for these functions is the following:

void UPlayerAnimInstance::HandleUpdatedTarget(AActor* NewTargetActorRef)
{
	bIsInCombat = IsValid(NewTargetActorRef);
}

void UPlayerAnimInstance::CombatCheck()
{
	APawn* PawnRef{ TryGetPawnOwner() }; 

	if (!IsValid(PawnRef)) { return; }

	if (!bIsInCombat) { return; }

	CurrentDirection = CalculateDirection(
		PawnRef->GetVelocity(),
		PawnRef->GetActorRotation()
	);
}

For updating the target, we accepted the target from the event and then checked if its valid reference with the IsValid() function.

As for the CombatCheck() function, we're checking if we have a valid pawn reference and the player is locked onto an enemy. If both conditions are true, we updated the CurrentDirection variable with the CalculateDirection() function. This function accepts the velocity and rotation of the actor. It'll return a value between -180 and 180 that's compatible with our blendspace.

Last updated