// Fill out your copyright notice in the Description page of Project Settings. #include "Enemy.h" #include "Blueprint/UserWidget.h" #include "Kismet/GameplayStatics.h" #include "Sound/SoundCue.h" #include "Particles/ParticleSystemComponent.h" #include "Kismet/KismetMathLibrary.h" #include "DrawDebugHelpers.h" #include "EnemyController.h" #include "BehaviorTree/BlackboardComponent.h" #include "Components/SkeletalMeshComponent.h" #include "Components/SphereComponent.h" #include "ShooterCharacter.h" #include "Components/BoxComponent.h" #include "Components/CapsuleComponent.h" #include "Engine/SkeletalMeshSocket.h" #include "TimerManager.h" #include "Animation/AnimInstance.h" // Sets default values AEnemy::AEnemy() : Health(100.f), MaxHealth(100.f), HealthBarDisplayTime(4.f), HitReactTimeMin(.5f), HitReactTimeMax(1.f), bCanHitReact(true), HitNumberDestroyTime(1.5f), bStunned(false), StunChance(0.8f), AttackLFast(TEXT("AttackLFast")), AttackRFast(TEXT("AttackRFast")), AttackL(TEXT("AttackL")), AttackR(TEXT("AttackR")), BaseDamage(20.f), LeftWeaponSocket(TEXT("FX_Trail_L_02")), RightWeaponSocket(TEXT("FX_Trail_R_02")), bCanAttack(true), AttackWaitTime(1.f), bDying(false), DeathTime(4.f) { // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; // Create the Agro Sphere AgroSphere = CreateDefaultSubobject(TEXT("AgroSphere")); AgroSphere->SetupAttachment(GetRootComponent()); // Create the Combat Range Sphere CombatRangeSphere = CreateDefaultSubobject(TEXT("CombatRangeSphere")); CombatRangeSphere->SetupAttachment(GetRootComponent()); // Construct left and right weapon collision boxes LeftWeaponCollision = CreateDefaultSubobject(TEXT("Left Weapon Box")); LeftWeaponCollision->SetupAttachment(GetMesh(), FName("LeftWeaponBone")); RightWeaponCollision = CreateDefaultSubobject(TEXT("Right Weapon Box")); RightWeaponCollision->SetupAttachment(GetMesh(), FName("RightWeaponBone")); } // Called when the game starts or when spawned void AEnemy::BeginPlay() { Super::BeginPlay(); AgroSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::AgroSphereOverlap); CombatRangeSphere->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::CombatRangeOverlap); CombatRangeSphere->OnComponentEndOverlap.AddDynamic(this, &AEnemy::CombatRangeEndOverlap); // Bind functions to overlap events for weapon boxes LeftWeaponCollision->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::OnLeftWeaponOverlap); RightWeaponCollision->OnComponentBeginOverlap.AddDynamic(this, &AEnemy::OnRightWeaponOverlap); // Set collision presets for weapon boxes LeftWeaponCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); LeftWeaponCollision->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic); LeftWeaponCollision->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); LeftWeaponCollision->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap); RightWeaponCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); RightWeaponCollision->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic); RightWeaponCollision->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); RightWeaponCollision->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Overlap); GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block); GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore); GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore); // Get the AI Controller EnemyController = Cast(GetController()); if (EnemyController) { EnemyController->GetBlackboardComponent()->SetValueAsBool(TEXT("CanAttack"), true); } const FVector WorldPatrolPoint = UKismetMathLibrary::TransformLocation(GetActorTransform(), PatrolPoint); //DrawDebugSphere(GetWorld(), WorldPatrolPoint, 25.f, 12, FColor::Red, true); const FVector WorldPatrolPoint2 = UKismetMathLibrary::TransformLocation(GetActorTransform(), PatrolPoint2); //DrawDebugSphere(GetWorld(), WorldPatrolPoint2, 25.f, 12, FColor::Blue, true); if (EnemyController) { EnemyController->GetBlackboardComponent()->SetValueAsVector("PatrolPoint", WorldPatrolPoint); EnemyController->GetBlackboardComponent()->SetValueAsVector("PatrolPoint2", WorldPatrolPoint2); EnemyController->RunBehaviorTree(BehaviorTree); } } void AEnemy::ShowHealthBar_Implementation() { GetWorldTimerManager().ClearTimer(HealthBarTimer); GetWorldTimerManager().SetTimer(HealthBarTimer, this, &AEnemy::HideHealthBar, HealthBarDisplayTime); } void AEnemy::Die() { if (bDying) return; bDying = true; HideHealthBar(); if (EnemyController) { EnemyController->GetBlackboardComponent()->SetValueAsBool("Dead", true); EnemyController->StopMovement(); } GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldDynamic, ECollisionResponse::ECR_Ignore); GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore); GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Ignore); if (!DeathMontage) return; UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance(); if (!AnimInstance) return; AnimInstance->Montage_Play(DeathMontage); EnemyDeadDelegate.Broadcast(); if (DeadSound) UGameplayStatics::PlaySoundAtLocation(this, DeadSound, GetActorLocation()); } void AEnemy::FinishDeath() { GetMesh()->bPauseAnims = true; GetWorldTimerManager().SetTimer(DeathTimer, this, &AEnemy::DestroyEnemy, DeathTime); } void AEnemy::DestroyEnemy() { Destroy(); } void AEnemy::PlayHitMontage(FName Section, float PlayRate) { if (!bCanHitReact) return; UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance(); if (AnimInstance && HitMontage) { AnimInstance->Montage_Play(HitMontage, PlayRate); AnimInstance->Montage_JumpToSection(Section, HitMontage); bCanHitReact = false; const float HitReactTime{ FMath::FRandRange(HitReactTimeMin, HitReactTimeMax) }; GetWorldTimerManager().SetTimer(HitReactTimer, this, &AEnemy::ResetHitReactTimer, HitReactTime); } } void AEnemy::ResetHitReactTimer() { bCanHitReact = true; } void AEnemy::StoreHitNumber(UUserWidget* HitNumber, FVector Location) { HitNumbers.Add(HitNumber, Location); FTimerHandle HitNumberTimer; FTimerDelegate HitNumberDelegate; HitNumberDelegate.BindUFunction(this, FName("DestroyHitNumber"), HitNumber); GetWorldTimerManager().SetTimer(HitNumberTimer, HitNumberDelegate, HitNumberDestroyTime, false); } void AEnemy::DestroyHitNumber(UUserWidget* HitNumber) { HitNumbers.Remove(HitNumber); HitNumber->RemoveFromParent(); } void AEnemy::UpdateHitNumbers() { for (auto& HitPair : HitNumbers) { UUserWidget* HitNumber{ HitPair.Key }; const FVector Location{ HitPair.Value }; FVector2D ScreenPosition; UGameplayStatics::ProjectWorldToScreen(GetWorld()->GetFirstPlayerController(), Location, ScreenPosition); HitNumber->SetPositionInViewport(ScreenPosition); } } void AEnemy::AgroSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (!OtherActor) return; auto Character = Cast(OtherActor); if (!Character) return; // Set the value of the Target blackboard key if (!EnemyController) return; if (!EnemyController->GetBlackboardComponent()) return; EnemyController->GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), Character); } void AEnemy::SetStunned(bool Stunned) { bStunned = Stunned; if (!EnemyController) return; EnemyController->GetBlackboardComponent()->SetValueAsBool(TEXT("Stunned"), Stunned); } void AEnemy::CombatRangeOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (!OtherActor) return; if (Cast(OtherActor) == nullptr) return; bInAttackRange = true; if (!EnemyController) return; EnemyController->GetBlackboardComponent()->SetValueAsBool(TEXT("InAttackRange"), true); //UE_LOG(LogTemp, Warning, TEXT("overlap player: %d"), static_cast(bInAttackRange)); } void AEnemy::CombatRangeEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { if (!OtherActor) return; if (Cast(OtherActor) == nullptr) return; bInAttackRange = false; if (!EnemyController) return; EnemyController->GetBlackboardComponent()->SetValueAsBool(TEXT("InAttackRange"), false); //UE_LOG(LogTemp, Warning, TEXT("end overlap player")); } void AEnemy::PlayAttackMontage(FName Section, float PlayRate) { if (bDying) return; UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance(); if (AnimInstance && AttackMontage) { AnimInstance->Montage_Play(AttackMontage, PlayRate); AnimInstance->Montage_JumpToSection(Section, AttackMontage); } bCanAttack = false; GetWorldTimerManager().SetTimer(AttackWaitTimer, this, &AEnemy::ResetCanAttack, AttackWaitTime); if (EnemyController) { EnemyController->GetBlackboardComponent()->SetValueAsBool(TEXT("CanAttack"), false); } } FName AEnemy::GetAttackSectionName() const { switch (FMath::RandRange(1, 4)) { case 1: return AttackLFast; case 2: return AttackRFast; case 3: return AttackL; case 4: return AttackR; default: return AttackLFast; } } void AEnemy::DoDamage(AShooterCharacter* Victim) { if (!Victim) return; UGameplayStatics::ApplyDamage(Victim, BaseDamage, EnemyController, this, UDamageType::StaticClass()); if (USoundCue* MeleeImpactSound = Victim->GetMeleeImpactSound()) { UGameplayStatics::PlaySoundAtLocation(this, MeleeImpactSound, GetActorLocation()); } } void AEnemy::SpawnBlood(AShooterCharacter* Victim, FName WeaponSocket) { if (!Victim) return; UParticleSystem* Particles = Victim->GetBloodParticles(); if (!Particles) return; const USkeletalMeshSocket* TipSocket{ GetMesh()->GetSocketByName(WeaponSocket) }; if (!TipSocket) return; const FTransform SocketTransform{ TipSocket->GetSocketTransform(GetMesh()) }; UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), Particles, SocketTransform); } void AEnemy::StunCharacter(AShooterCharacter* Victim) { if (!Victim) return; const float Stun{ FMath::RandRange(0.f,1.f) }; if (Stun <= Victim->GetStunChance()) { Victim->Stun(); } } void AEnemy::ResetCanAttack() { bCanAttack = true; if (!EnemyController) return; EnemyController->GetBlackboardComponent()->SetValueAsBool(TEXT("CanAttack"), true); } void AEnemy::OnLeftWeaponOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { auto const Character = Cast(OtherActor); if (Character) { DoDamage(Character); SpawnBlood(Character, LeftWeaponSocket); StunCharacter(Character); } } void AEnemy::OnRightWeaponOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { auto const Character = Cast(OtherActor); if (Character) { DoDamage(Character); SpawnBlood(Character, RightWeaponSocket); StunCharacter(Character); } } void AEnemy::ActivateLeftWeapon() { LeftWeaponCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly); } void AEnemy::DeactivateLeftWeapon() { LeftWeaponCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); } void AEnemy::ActivateRightWeapon() { RightWeaponCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly); } void AEnemy::DeactivateRightWeapon() { RightWeaponCollision->SetCollisionEnabled(ECollisionEnabled::NoCollision); } // Called every frame void AEnemy::Tick(float DeltaTime) { Super::Tick(DeltaTime); UpdateHitNumbers(); } // Called to bind functionality to input void AEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) { Super::SetupPlayerInputComponent(PlayerInputComponent); } void AEnemy::BulletHit_Implementation(FHitResult HitResult, AActor* Shooter, AController* ShooterController) { if (ImpactSound) { UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation()); } if (ImpactParticles) { UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles, HitResult.Location, FRotator(0.0), true); } } float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) { // Set the Target blackboard key to aggro the character if (EnemyController) { EnemyController->GetBlackboardComponent()->SetValueAsObject(FName("Target"), DamageCauser); } float DamageInflicted = DamageAmount; if (Health - DamageAmount <= 0.f) { DamageInflicted = Health; Health = 0.f; Die(); } else { Health -= DamageAmount; } if (bDying) return DamageInflicted; ShowHealthBar(); // Determine actual Stun Chance based on health and damage inflicted float ActualStunChance = DamageInflicted / (Health / 10.f) * StunChance; // Determine whether bullet hit stuns const float Stunned = FMath::FRandRange(0.f, 1.f); if (Stunned <= ActualStunChance) { // Stun the Enemy PlayHitMontage(FName("HitReactFront")); SetStunned(true); } return DamageInflicted; }