Enemies, Enemy Attacks and Attack Blocking
MedievalCombatProject
For now, the Player will face Enemies of only one type - Minion. These Minion assets were sourced from the Paragon: Minions Asset Pack from the Epic Games Store. I will be adding more Enemies very soon from the Infinity Blade: Adversaries Asset Pack.
An Enemy’s Collision Volumes are as below:
All such Collision Volumes use the OnComponentBeginOverlap
and OnComponentEndOverlap
functions to perform actions if an overlap occurs.
-
AgroSphere - If the Character enters this sphere, the Enemy will get agressive and chase the Character. Chasing is done with UE4’s inbuilt MoveToTarget() function, using which the Enemy will follow a path to reach the Character’s location. This functionality only works inside NavMeshBounds Volumes.
-
CombatSphere - When the Enemy reaches close enough to the Character to make it overlap with the CombatSphere, the Enemy will begin attacking.
Additional functionality added is the ability for the Enemy to turn so that attacks hit the Character i.e. if the Character moves when it is attacking, it will turn so that it can look directly at the Character.
- LeftCombatCollision & RightCombatCollision - Used to check if the Enemy’s attack has hit the Character or not. They are box collisions covering the swords of the Enemies completely. By default, they do not respond to any types of overlaps. They are only activated to register overlaps when the Enemy performs an attack.
Attack Animations for Enemies are kept together in an Animation Montage. Out of the set of Animations, one is chosen at random.
Attack()
void AEnemy::Attack()
{
if (Alive() && bHasValidTarget)
{
if (AIController)
{
// As Character is overlapping CombatSphere, there is no need to move
AIController->StopMovement();
SetEnemyMovementStatus(EEnemyMovementStatus::EMS_Attacking);
}
if (!bAttacking)
{
// Turn towards Character
SetInterpToEnemy(true);
bAttacking = true;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance)
{
//Randomly choose between the 4 attack animations
AttackSection = FMath::RandRange(1, 4);
// Append the AttackSection to the FString below
FString AttackName("Attack_");
AttackName.AppendInt(AttackSection);
AnimInstance->Montage_Play(CombatMontage, 1.0f);
AnimInstance->Montage_JumpToSection(FName(*AttackName), CombatMontage);
}
}
}
}
If the CombatCollision overlaps with the Character, a check will be performed to see if the Character was blocking.
If not blocking
If the Character was not blocking, a function InflictDamageOnMain
is called. This function uses UE4’s ApplyDamage() function to decrease the
Character’s health, along with calling functions to emit blood particles and playing hit sounds.
The hit reaction animations are not picked at random, but are related to the AttackSection
shown above AND if the Character is facing the
attacking Enemy. As a result, the appropriate animation plays in reaction to the Enemy’s attack.
InflictDamageOnMain()
void AEnemy::InflictDamageOnMain(AMain* Main, bool bHitFromBehind)
{
// If Character was hit from behind
if (bHitFromBehind)
{
// Play HitFromBehind Animation
Main->Impact(1);
/**
* First 2 Attack Sockets in Main's SocketNames are the names of the Sockets that will be used
* if the Player gets hit by the Enemy facing them. By adding 4, we get the Sockets that will be used if
* the Player gets hit by the Enemy facing their back. Used below to use the appropriate Socket to emit the blood.
*/
AttackSection += 4;
}
// If Character was hit while facing the attacking Enemy
else
{
// Play appropriate Hit Reaction Animation
Main->Impact(AttackSection + 1);
}
// Spawn Blood particles
if (Main->HitParticles)
{
Main->SpawnHitParticles(this);
}
// Play Hit Sound
if (Main->HitSound)
{
UGameplayStatics::PlaySound2D(this, Main->HitSound);
}
// Apply Damage to Main
if (DamageTypeClass)
{
UGameplayStatics::ApplyDamage(Main, Damage, AIController, this, DamageTypeClass);
}
//In case the Player's attack was interrupted
Main->bAttacking = false;
Main->bInterpToEnemy = false;
}
In Action
- Character hit reaction when not blocking
If blocking
Performed using the Right Mouse Button(RMB). The Character can block attacks using a One-Handed Weapon AND a Shield OR with a Two-Handed Weapon. One-Handed Weapons alone cannot be used for blocking.
The Character can walk while blocking. This is done by using UE4’s Layered Blend per bone
node, done before in the
Crouching post.
Walking while Blocking:
- Blocking with Shield
- Blocking with Two-Handed Weapon
To successfully block an Enemy’s attack, the Character has to be blocking AND has to be facing the attacking Enemy, while also having sufficient Stamina available.
Facing an Enemy means that the Enemy should be inside the Character’s Field of View (taken as 120°).
The code snippet below shows how this is done: (Note: The Character’s name is “Main”)
CombatOnOverlapBegin()
void AEnemy::CombatOnOverlapBegin(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep,
const FHitResult& SweepResult)
{
// If OtherActor is valid
if (OtherActor)
{
AMain* Main = Cast<AMain>(OtherActor);
// If the OtherActor is the Character
if (Main)
{
//Check if Character is facing the Enemy
FVector MainLocation = Main->GetActorLocation();
FVector EnemyLocation = GetActorLocation();
FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(MainLocation, EnemyLocation);
FRotator MainRotation = Main->GetActorRotation();
FRotator DeltaRotator = UKismetMathLibrary::NormalizedDeltaRotator(MainRotation, LookAtRotation);
// Acceptable range of rotation = -180 -> -60 degrees OR 60 -> 180 degrees.
// i.e. Character's field of view = 120 degrees
bool bAngleCheck1 = UKismetMathLibrary::InRange_FloatFloat(DeltaRotator.Yaw, -180, -60);
bool bAngleCheck2 = UKismetMathLibrary::InRange_FloatFloat(DeltaRotator.Yaw, 60, 180);
// If Character is Blocking
if (Main->bBlocking)
{
//If facing the Enemy
if (!(bAngleCheck1 || bAngleCheck2))
{
//If Character is blocking with shield and has enough stamina to successfully block an attack
if (Main->EquippedShield && Main->Stamina - Main->EquippedShield->BlockStaminaCost >= 0)
{
// Add Blocking particle effects and Sound
// Decrease Stamina by BlockStaminaCost
}
//If Character is blocking with weapon and has enough stamina to successfully block an attack
else if (Main->bIsWeaponDrawn && Main->Stamina - Main->EquippedWeapon->BlockStaminaCost >= 0)
{
//Weapons don't block attacks completely, so decrease Health by DamageIfBlocked
// Add Blocking particle effects and Sound
// Decrease Stamina by BlockStaminaCost
}
//If Character does not have enough stamina to successfully block an attack
else
{
InflictDamageOnMain(Main, false);
}
}
else
{
//If not facing the Enemy
InflictDamageOnMain(Main, true);
}
}
else
{ //If not blocking
InflictDamageOnMain(Main, (bAngleCheck1 || bAngleCheck2));
}
}
}
}
If the attack was blocked, the Character’s Stamina will reduce by BlockStaminaCost
(declared in the Weapon and Shield classes).
Additionally, if a Two-Handed Weapon was used for blocking, the Character’s Health will decrease by an amount DamageIfBlocked
,
as Weapons cannot block as effectively as Shields.
You can view the code of the project here!
Blocking in Action
- One-Handed Weapon + Shield
- Two-Handed Weapon