Section 6: Enemy Behavior

In this section, we worked on getting the enemy behavior to detect, chase and attack the player.

Section Intro - Enemy Behavior

There are no notes for this lecture.

Starting the Boss Animation

There are no notes for this lecture.

Pawn Sensing

In this lecture, we learned about using the pawn sensing component to detect our player. When using this component, there's an event called on see pawn that'll tell us when the player was detected. We decided to define a function that'll accept the pawn detected and compare it with a pawn that we're looking for.

void ABossCharacter::DetectPawn(APawn* DetectedPawn, APawn* PawnToDetect)
{
	if (
		DetectedPawn != PawnToDetect
		) {
		return;
	}

	UE_LOG(LogTemp, Warning, TEXT("Player Detected"));
}

Comparing two pawns can be done by just using a comparison operator like != or ==.

Running a Behavior Tree

There are no notes for this lecture.

Blackboard Decorator

In this lecture, we added decorators to our selectors. Decorators can be used to add conditions to a selector. If a condition is not met, the selector's tasks will not be executed in the behavior tree. We created a key in our blackboard that will be used in our selector's condition. The key uses an enum to keep track of the boss's state. The enum is the following:

UENUM(BlueprintType)
enum EEnemyState 
{
	Idle UMETA(DisplayName = "Idle"),
	Combat UMETA(DisplayName = "Combat")
};

Next, we updated our behavior with the Blackboard Decorator and updated its settings to check for this key.

Adding conditions to the selectors.
Configuring the selector.

Setting and Getting Blackboard Keys

In this lecture, we learned how to grab blackboard values. First, we had to grab the blackboard component. To do so, we must include two files:

#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"

The AIController file gives us access to a class called AAIController. Controllers can be thought of as an actor without a physical form. Instead, it completely takes over another actor. To add AI behavior, a controller is created to manipulate the boss's behavior. When we run the behavior tree, Unreal creates this controller and attaches the blackboard component to this actor. So, we'll need access to the controller before grabbing the component.

BlackboardComp = GetController<AAIController>()
	->GetBlackboardComponent();

The GetController() function returns a controller but we can specify the controller by passing in a generic. Next, we chained the GetBlackboardComponent() function to get the component.

Once we have the component in our posession, we can use the SetValueAsEnum function to update the enums in our blackboard. It accepts the key name and the new value.

BlackboardComp->SetValueAsEnum(
	TEXT("CurrentState"),
	InitialState
);

Alternatively, we can grab an enum with the GetValueAsEnum() function.

BlackboardComp->GetValueAsEnum(TEXT("CurrentState"))

Creating Behavior Tasks

In this lecture, we created a custom behavior task for sending ranged attacks. To create a custom task, we must inherit from the BTTaskNode class. This class provides a function called ExecuteTask() that can be overridden to do something when the task is executed. We must tell our behavior tree that the task was executed successfully before it can move on to the next task.

EBTNodeResult::Type UBTT_RangeAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	UE_LOG(LogTemp, Warning, TEXT("Success!"));

	return EBTNodeResult::Succeeded;
}

To tell it was a success, we can use the EBTNodeResult enum. It has four values, where the Succeeded value can be used for a successful execution.

Resources

Playing the Ranged Attack Animation

In this lecture, we created an animation for our range attacks. Most of this process was familiar to us. One new thing we learned was about the OwnerComp parameter. The ExecuteTask function is supplied with the behavior tree component. Through this component, we can grab the controller by chaining the GetAIOwner() function, and then get the pawn with the GetPawn() function.

ACharacter* CharacterRef{ 
	OwnerComp.GetAIOwner()->GetPawn<ACharacter>() 
};

Creating a Projectile Actor

There are no notes for this lecture.

Spawning the Projectile

In this lecture, we spent time learning how to spawn a projectile. We defined a function that accepts the name of a component to set the spawn location and then the actor itself.

void UEnemyProjectileComponent::SpawnProjectile(FName ComponentName, TSubclassOf<AActor> ProjectileClass)
{
	USceneComponent* SpawnPointComp{ Cast<USceneComponent>(
		GetOwner()->GetDefaultSubobjectByName(ComponentName)
	) };

	FVector SpawnLocation{ SpawnPointComp->GetComponentLocation() };

	GetWorld()->SpawnActor(
		ProjectileClass,
		&SpawnLocation
	);
}

To grab a component by its name, we can use the GetDefaultSubjectByName() function which returns a class by its name. However, this will return an instance of the UObject class. To convert it into a component you want, you must use the Cast() function.

Next, we spawned the actor using the SpawnActor() function, which accepts the class to spawn and location as arguments.

When trying to accept a reference to an actor, you must use the TSubclassOf type, which accepts the AActor class as a generic. By doing so, Unreal will be able to render a complete list of all actors that can be spawned.

Rotation Interpolation

In this lecture, we took the time to calculate a rotation for the enemy to always look toward the player. We created a brand new component called LookAtRotationComponent. During this process, we used the UKismetMathLibrary::FindLookAtRotation() function to help us calculate the new rotation. However, we don't want to instantly rotate toward the player. So, we decided to use the UKismetMathLibrary::RInterpTo_Constant() function.

FRotator NewRotation{ 
	UKismetMathLibrary::RInterpTo_Constant(
		CurrentRotation, DesiredRotation, DeltaTime, Speed
	) 
};

This function accepts four values, which is the current rotation, the desired rotation, the delta time and the speed at which to smoothly move toward the rotation. We outsourced the value for the speed as a class variable.

Lastly, we decided to only grab the Yaw value from the rotation since we don't want to rotate the actor on their Y or X axis.

FRotator NewYawOnlyRotation{
	CurrentRotation.Pitch, NewRotation.Yaw, CurrentRotation.Roll
};

To set the rotation of an actor, you can call the SetActorRotation() function with the new rotation value.

ActorRef->SetActorRotation(NewYawOnlyRotation);

Resources

Rotating With Anim Notify States

In this lecture, we created an animation notify state to only rotate the boss during specific points of the attack animation. We added a boolean variable called bCanRotate on the LookAtPlayerComponent class. We toggled this variable from the animation notification state. Most of what we did has been done before.

What's new is that we can add a new notify track in the animation montage by clicking on the Notify dropdown and clicking Add Notify Track.

Resources

Collision Overlap Event

In this lecture, we created a custom function to handle the On Begin Overlap event from the sphere collision component.

This event supplies us with the actor that was hit with the Other Actor output. We used this to check if the player was hit since the other actor could potentially be an enemy.

void AEnemyProjectile::HandleBeginOverlap(AActor* OtherActor)
{
	APawn* PawnRef = Cast<APawn>(OtherActor);

	if (!PawnRef->IsPlayerControlled()) { return; }

	UE_LOG(LogTemp, Warning, TEXT("Player detected"));
}

In the HandleBeginOverlap function, we're casting the actor to an APawn since this class has a function to check if we're hitting a player, which is called IsPlayerControlled(). If this function returns false, that means we aren't hitting a player. In that case, we returned the function early to cease further execution.

Changing Particle System Templates

In this lecture, we learned how to store a particle system template. We can do so by adding a variable to our class with the UParticleSystem class.

UPROPERTY(EditAnywhere)
UParticleSystem* HitTemplate;

To swap out a template, we must grab a reference to the UParticleSystemComponent class and then call the SetTemplate function which accepts the template to update.

GetComponentByClass<UParticleSystemComponent>()
	->SetTemplate(HitTemplate);

Next, we stopped the projectile from moving through the UProjectileMovementComponent class. This class has a function called StopMovementImmediately(). However, it's not directly found in this class. Instead, it's a function inherited. Whenever you need to do something from a component, it will not always be available directly from the component. In that case, searching through the class's parent classes may help you do what you want.

GetComponentByClass<UProjectileMovementComponent>()
	->StopMovementImmediately();

In this case, it would be stopping the projectile from moving completely.

Resources

Setting Timers

In this lecture, we learned how to delete an actor after a set time. Unreal has a class for managing world-timers. If we want to delete the projectile after a set amount of time, we must grab the timer manager with the GetWorldTimerManager() function. From this function, we can chain a function called SetTimer() to add a timer.

FTimerHandle DeathTimerHandler;
GetWorldTimerManager().SetTimer(
	DeathTimerHandler,
	this,
	&AEnemyProjectile::DestroyProjectile,
	0.5f
);

This function has four arguments

  • An identifier since multiple timers can be active at once. Helps us identify our timer from other timers.

  • The actor creating the timer.

  • The function to call when the timer runs out.

  • The duration of the timer in seconds.

We defined a function called DestroyProjectile() to destroy the actor with the Destroy() function.

void AEnemyProjectile::DestroyProjectile()
{
	Destroy();
}

When declaring functions for timers, always make sure to add the UFUNCTION() macro. Otherwise, the function may not be called.

UFUNCTION()
void DestroyProjectile();

Resources

Disabling Collision

In this lecture, we decided to disable the collision of our sphere once it hits the player. This is because of a rare bug that can occur where the event can be triggered twice, causing the player to be hit with double the damage. To disable collision, we must first include the following declaration file:

#include "Components/SphereComponent.h"

In the HandleBeginOverlap() function, we grabbed the USphereComponent and then called the SetCollisionEnabled() function to toggle the collision. We can pass in the ECollisionEnabled::NoCollision enum to disable collision thus preventing our event from triggering twice.

FindComponentByClass<USphereComponent>()
	->SetCollisionEnabled(ECollisionEnabled::NoCollision); 

Resources

Applying Damage to Players

In this lecture, we updated our projectile to apply damage to the player when they've been hit. We updated the HandleBeginOverlap() function by calling the TakeDamage() function on the pawn.

FDamageEvent ProjectileAttackEvent{ };

PawnRef->TakeDamage(
	damage,
	ProjectileAttackEvent,
	PawnRef->GetController(),
	this
);

The damage itself has been outsourced as an editable property to allow anyone to customize how much damage it can cause.

Generating Random Values

In this lecture, we learned how to generate a random value. Unreal comes with a few functions to help us generate random values called the UKismetMathLibrary. To use these functions, we must first include the file with this class.

#include "Kismet/KismetMathLibrary.h"

We called the RandomFloat() function to generate a random value. Despite its name, this function does not return a float but a double. So, you should use the double type when storing the return value in a variable.

double RandomVal{ UKismetMathLibrary::RandomFloat() };

The reason we generated a random value was to compare it with a threshold. If the random value is greater than the threshold, we'll switch states. Otherwise, we lower the threshold to increase the likelihood of the random value being greater than the threshold.

if (RandomVal > Threshold)
{
	Threshold = 0.9;

	UE_LOG(LogTemp, Warning, TEXT("Charge!"));
}
else
{
	Threshold -= 0.1;
}

Switching to the Charge State

In this lecture, we created two new states in our EEnemyState enum.

enum EEnemyState
{
	Idle UMETA(DisplayName = "Idle"),
	Combat UMETA(DisplayName = "Combat"),
	Charge UMETA(DisplayName = "Charge"),
	Melee UMETA(DisplayName = "Melee")
};

Next, we updated the ExecuteTask function for the range attack task to update the blackboard with the Charge state like so:

OwnerComp.GetBlackboardComponent()->SetValueAsEnum(
	TEXT("CurrentState"),
	EEnemyState::Charge
);

Lastly, we updated our behavior tree to only play the BTT_ChargeAttack task when the state is set to Charge.

Animations With State Machines

In this lecture, we learned about state machines. Please watch the video to understand them and how to create them.

Playing the Charge Animation

In this lecture, we played the charge animation by updating the animation instance on the boss. To do this, we had to go through several classes.

Since we're in the task, we must grab the AI controller, which is a class responsible for controlling the character's behavior. Since it's associated with a character, we can use it to grab the character. Through the character, we can grab the animation instance. Here's how we did it:

EBTNodeResult::Type UBTT_ChargeAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	ControllerRef = OwnerComp.GetAIOwner();
	CharacterRef = ControllerRef->GetCharacter();
	BossAnim = Cast<UBossAnimInstance>(
		CharacterRef->GetMesh()->GetAnimInstance()
	);

	BossAnim->bIsCharging = true;

	return EBTNodeResult::InProgress;
}

Lastly, we told the behavior tree the task is pending since it may take time for the boss to fully charge at the player by returning the EBTNodeResult::InProgress enum.

AI Move Requests

In this lecture, we learned how to create a move request. A move request contains information about where a character should move. We can create a move request with the FAIMoveRequest type like so:

FAIMoveRequest MoveRequest{ PlayerLoc };
MoveRequest.SetUsePathfinding(true);
MoveRequest.SetAcceptanceRadius(AcceptableRadius);

Creating a variable of this type accepts the location. The other 2 lines of code tell the AI to figure out the best path to avoid obstacles and to allow for a small margin of error when the enemy can't reach their destination exactly.

To initiate a move request, we can do so through the AI controller.

ControllerRef->MoveTo(MoveRequest);
ControllerRef->SetFocus(PlayerRef);

The MoveTo() the function accepts the request. We're also telling the controller to focus the player with the SetFocus() function. This way, they'll rotate toward the player during the attack.

Lastly, we added a NavMeshBoundsVolume actor to our game so that the AI knows which areas are walkable.

Binding a Function to an Event

In this lecture, we decided to bind a function to an event with C++ instead of with blueprints. To do so, we must first create a variable for storing the function with the FScriptDelegate type.

FScriptDelegate MoveCompletedDelegate;

To bind a function to this variable, we can use the BindUFunction() function. It's important to note that the function you add to the variable must have the UFUNCTION() macro.

MoveCompletedDelegate.BindUFunction(
	this, "HandleMoveCompleted"
);

After adding the function to the variable, we can finally bind the function to our event with the AddUnique() function.

ControllerRef->ReceiveMoveCompleted.AddUnique(
	MoveCompletedDelegate
);

Resources

Boosting the Character's Speed

In this lecture, we decided to boost the speed of our character during a charge attack to make it more menacing. To modify a character's speed, you can update the MaxWalkSpeed variable on the character's movement component.

OriginalWalkSpeed = CharacterRef->GetCharacterMovement()
	->MaxWalkSpeed;
CharacterRef->GetCharacterMovement()->MaxWalkSpeed = ChargeWalkSpeed;

Before we updated the speed, we stored the original speed as want to revert the speed when the charge is finished, which we do from the HandleMoveCompleted() function.

CharacterRef->GetCharacterMovement()
	->MaxWalkSpeed = OriginalWalkSpeed;

Finishing Latent Tasks

In this lecture, we finished the task when the enemy reached the player and attacked. To do this, we overrode the TickTask() function because we need access to the behavior tree.

void UBTT_ChargeAttack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	if (!bIsFinished) { return; }

	ControllerRef->ReceiveMoveCompleted
		.Remove(MoveCompletedDelegate);

	FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}

The reason we needed this info was because the FinishLatentTask() function requires it. When calling this function, we must provide the behavior tree and then the new status of our task. In this example, we're telling the task it has succeeded.

In addition, we removed the function we registered with the move request by calling the Remove() function on the event with the variable containing our function.

Resources

Unreal Structures

In this lecture, we learned about structures. Structures are a feature in C++ that are similar to classes with one major difference, all members (variables and functions) are public instead of private by default. Other than that, they're mostly the same. Developers like to use them because it helps them understand that they're dealing with an object that mostly stores and manages data, nothing else.

So, we decided to create a structure for our sockets.

USTRUCT(BlueprintType)
struct ACTIONCOMBATDEMO_API FTraceSockets
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere)
	FName Start;

	UPROPERTY(EditAnywhere)
	FName End;

	UPROPERTY(EditAnywhere)
	FName Rotation;
};

A few things worth noting:

  • Structures are created with the struct keyword.

  • We can expose our struct to the Unreal editor by using the USTRUCT() macro with the BlueprintType specifier.

  • The GENERATED_BODY() macro must also be added so that Unreal can add additional code.

For our trace component, we updated it to use our structure so that it can store an array of socket pairs.

UPROPERTY(EditAnywhere)
TArray<FTraceSockets> Sockets;

Refactoring the Trace Component

In this lecture, we took the time to update our trace component to support multiple weapons. Starting where we left off, we decided to loop through the array of sockets and then updated the socket functions with the information from our struct.

for (const FTraceSockets Socket : Sockets)
{
	FVector StartSocketLocation{ 
		SkeletalComp->GetSocketLocation(Socket.Start) 
	};
	FVector EndSocketLocation{ 
		SkeletalComp->GetSocketLocation(Socket.End) 
	};
	FQuat ShapeRotation{ 
		SkeletalComp->GetSocketQuaternion(Socket.Rotation) 
	};
}

The const keyword is added to each item in the array since we don't want to modify each item. After grabbing the socket information, we decided to store the results in an external array from our trace called AllResults. We looped through the results on each trace and added to this new array by using the Add() function from the TArray type.

for (FHitResult Hit : OutResults)
{
	AllResults.Add(Hit);
}

Other than that, we had to update our components, animation blueprint, and attack animation with the notification to test our work.

Supporting Damage on the Enemy

In this lecture, we added the IFighter interface to the ABossCharacter class and implemented the GetDamage() function.

Creating Behavior Tree Services

In this lecture, we learned how to create a behavior tree service, which is a file that can be attached to a node and run in parallel with a task. When defining our service, we decided to override the TickNode function.

UCLASS()
class ACTIONCOMBATDEMO_API UBTS_PlayeyDistance : public UBTService
{
	GENERATED_BODY()

protected:
	virtual void TickNode(
		UBehaviorTreeComponent& OwnerComp,
		uint8* NodeMemory,
		float DeltaSeconds
	) override;
};

From within this function, we updated our blackboard float value using the SetValueAsFloat function.

OwnerComp.GetBlackboardComponent()->SetValueAsFloat(
	TEXT("Distance"),
	Distance
);

Resources

Aborting a Task

In this lecture, we learned how to properly abort a task. First, we must call the AbortTask() function, which frees any memory related to our task. It requires the behavior tree and node memory as an argument. That information is provided via the ExecuteTask() function.

if (Distance < MeleeRange) 
{
	OwnerComp.GetBlackboardComponent()->SetValueAsEnum(
		TEXT("CurrentState"),
		EEnemyState::Melee
	);

	AbortTask(OwnerComp, NodeMemory);
	return EBTNodeResult::Aborted;
}

Lastly, we returned the EBTNodeResult::Aborted enum. This is important as we should make it clear to anyone that the task did not run successfully but was aborted. Otherwise, you may confuse others when the enemy doesn't shoot a projectile attack.

Switching to the Melee State

In this lecture, we created a melee task and switched to it when the state changed to Melee. During this process, we also updated the charge attack to switch to this state since the enemy will get close enough to the player that they should be in it. We performed this step from the TickTask() function since it's responsible for completing the task.

void UBTT_ChargeAttack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	if (!bIsFinished) { return; }

	ControllerRef->ReceiveMoveCompleted
		.Remove(MoveCompletedDelegate);

	OwnerComp.GetBlackboardComponent()->SetValueAsEnum(
		TEXT("CurrentState"),
		EEnemyState::Melee
	);

	FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}

Actor Move Requests

In this lecture, we learned how to move the enemy toward the player with a move request. We used the FAIMoveRequest type, which actually has multiple constructors. C++ supports constructor overloading, which allows for a class to have multiple constructor functions. One of the constructors allows for a variable to be initialized with an actor instead of a location. By providing an actor, the move request will follow the player.

APawn* PlayerRef{
	GetWorld()->GetFirstPlayerController()->GetPawn()
};

FAIMoveRequest MoveRequest{ PlayerRef };

Resources

Finishing the Melee Task

There are no notes for this lecture.

Selecting Random Attacks

In this lecture, we updated our combat component to be able to play a random animation. We defined a function called RandomAttack(). Inside this function, we're grabbing a random index from the AttackAnimations with the help of the FMath::RandRange() function. When calling this function, we're providing a range of the first index to the last index. To get the last index, we grabbed the number of items in our array and subtracted one.

void UCombatComponent::RandomAttack()
{
	int RandomIndex{ 
		FMath::RandRange(0, AttackAnimations.Num() - 1) 
	};

	CharacterRef->PlayAnimMontage(AttackAnimations[RandomIndex]);
}

After grabbing the index, we called the PlayAnimMontage() function.

Grabbing the Animation Duration

In this lecture, we grabbed the animation duration from the combat component so that we could create a timer from our melee task. During this process, we discovered two bugs. Firstly, the camera would jitter back and forth from the player during attacks. This is because the enemy would slightly obstruct the camera from seeing the player. To prevent that from happening, we modified the collision settings of the capsule and mesh components to change the Trace channel to Ignore.

The other bug we encountered was that the enemy wouldn't be able to properly follow the player if they moved slightly away. This is because our PlayerDistance service only updates every half second. If you click on this service from our behavior tree, there should be a setting to modify the Interval. Changing it to 0.1 fixes the problem.

Switching to the Range Attack

In this lecture, we worked on switching to the range state. During this process, we had to clean up the movement requests made during the task. If the player runs away while the enemy chases them, they'll continue to chase them unless we instruct the AI to stop moving. To do so, we must call the StopMovement() function, and we also cleared the focus by calling the ClearFocus() function.

AIRef->StopMovement();
AIRef->ClearFocus(EAIFocusPriority::Gameplay);

AIRef->ReceiveMoveCompleted.Remove(MoveDelegate);

Lastly, we removed the function bound to our ReceiveMoveCompleted event as we've aborted the task.

Finishing Touches

In this lecture, we fixed two small bugs with our enemy. First, we adjusted the velocity by updating the animation blueprint. Secondly, we made it possible for the enemy to smoothly rotate when suddenly moving. To do this, we first had to disable the Use Controller Rotation Yaw setting, which tells the enemy to not use the rotation assigned by the controller.

Secondly, we enabled the Orient Rotation To Movement setting, which tells the enemy to always rotate in the direction they're moving.

Last updated