1️⃣ 시각
🔹AI Controller 코드
더보기
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionTypes.h"
#include "CoolGuyAIController.generated.h"
UENUM(BlueprintType)
enum class ESearchStateInternal : uint8
{
Idle = 0,
Investigating = 1,
Chasing = 2,
Searching = 3
};
UCLASS()
class SCC_UEAI_LECTURE_API ACoolGuyAIController : public AAIController
{
GENERATED_BODY()
public:
bool bPatrolPointsReady = false;
ACoolGuyAIController();
UPROPERTY(EditAnywhere, Category = "AI")
UBehaviorTree* BehaviorTree;
UPROPERTY(BlueprintReadWrite, Category = "AI")
UBehaviorTreeComponent* BehaviorTreeComponent;
UPROPERTY(BlueprintReadWrite, Category = "AI")
UBlackboardComponent* BlackboardComponent;
TArray<FVector> PatrolPoints;
UBlackboardComponent* GetBlackboard() const;
//AI 인식 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI|Perception")
UAIPerceptionComponent* CoolGuyPerceptionComponent;
//시야설정
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI|Perception")
UAISenseConfig_Sight* SightConfig;
protected:
virtual void BeginPlay() override;
virtual void OnPossess(APawn* InPawn) override;
static const FName PatrolLocationKey;
//인식 업데이트 콜백
UFUNCTION()
void StartChasingPlayer(AActor* PlayerActor);
//플레이어 추적 중지 함수
UFUNCTION()
void StopChasingPlayer();
//마지막 위치 수색 함수
UFUNCTION()
void StartSeachingLastLocation();
//핸들러
UFUNCTION()
void OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
private:
FVector CurrentTargetLocation;
void InitializePatrolPoints();
//블랙보드 참조
UBlackboardComponent* BB = nullptr;
static const FName TargetPlayerKey;
static const FName CanSeePlayerKey;
static const FName LastSeenLocationKey;
static const FName SearchStateKey;
};
CoolGuyAIController.h
#include "CoolGuyAIController.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/TargetPoint.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "GameFramework/Character.h"
//블랙보드 키 이름 정의
const FName ACoolGuyAIController::PatrolLocationKey = "PatrolLocation";
const FName ACoolGuyAIController::TargetPlayerKey = "TargetPlayer";
const FName ACoolGuyAIController::CanSeePlayerKey = "CanSeePlayer";
const FName ACoolGuyAIController::LastSeenLocationKey = "LastSeenLocation";
const FName ACoolGuyAIController::SearchStateKey = "SearchState";
const FName ACoolGuyAIController::LastHeardLocationKey = "LastHeardLocation";
const FName ACoolGuyAIController::CanHearPlayerKey = "CanHearPlayer";
ACoolGuyAIController::ACoolGuyAIController()
{
//AI 인식 컴포넌트 설정
CoolGuyPerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("PerceptionComponent"));
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
//시야 설정
SightConfig->SightRadius = 1500.0f;
SightConfig->LoseSightRadius = 2000.0f;
SightConfig->PeripheralVisionAngleDegrees = 70.0f;
SightConfig->SetMaxAge(5.0f);
//감지 설정
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
//인식 컴포넌트에 시야 설정 적용
CoolGuyPerceptionComponent->ConfigureSense(*SightConfig);
CoolGuyPerceptionComponent->SetDominantSense(SightConfig->GetSenseImplementation());
}
void ACoolGuyAIController::BeginPlay()
{
Super::BeginPlay();
//인식 업데이트 이벤트 설정
CoolGuyPerceptionComponent->OnTargetPerceptionUpdated.AddDynamic(this, &ACoolGuyAIController::OnTargetPerceptionUpdated);
//초기 상태 설정
GetBlackboardComponent()->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Idle);
}
void ACoolGuyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (BehaviorTree)
{
UseBlackboard(BehaviorTree->BlackboardAsset, BlackboardComponent);
RunBehaviorTree(BehaviorTree);
}
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &ACoolGuyAIController::InitializePatrolPoints, 0.5f, false);
}
//플레이어 추적 시작 함수
void ACoolGuyAIController::StartChasingPlayer(AActor* PlayerActor)
{
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Chasing);
BB->SetValueAsObject(TargetPlayerKey, PlayerActor);
}
}
//플레이어 추적 중지 함수
void ACoolGuyAIController::StopChasingPlayer()
{
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Idle);
BB->SetValueAsObject(TargetPlayerKey, nullptr);
BB->SetValueAsBool(CanSeePlayerKey, false);
}
}
//마지막 위치 수색 함수
void ACoolGuyAIController::StartSeachingLastLocation()
{
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Searching);
}
}
//인식 업데이트 콜백 구현
void ACoolGuyAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
//플레이어 감지 확인
ACharacter* PlayerCharacter = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
if (!PlayerCharacter) return;
//Actor와 Character를 단순비교할 수 없으므로 Actor를 ACharacter로 임시 캐스팅하여 비교
if (Cast<ACharacter>(Actor) == PlayerCharacter)
{
//시각 자극인지 확인
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
{
bool bCanSeePlayer = Stimulus.WasSuccessfullySensed();
//블랙보드 초기화
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsObject(TargetPlayerKey, bCanSeePlayer ? Actor : nullptr);
BB->SetValueAsBool(CanSeePlayerKey, bCanSeePlayer);
if (bCanSeePlayer)
{
//플레이어 발견(추적 시작)
BB->SetValueAsVector(LastSeenLocationKey, Stimulus.StimulusLocation);
StartChasingPlayer(Actor);
UE_LOG(LogTemp, Warning, TEXT("AI can see the player! Starting chase..."));
}
else
{
//플레이어 시야에서 사라지면 마지막 위치 검색
ESearchStateInternal CurrentState = (ESearchStateInternal)BB->GetValueAsEnum(SearchStateKey);
if (CurrentState == ESearchStateInternal::Chasing)
{
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Investigating);
BB->SetValueAsVector(LastSeenLocationKey, Stimulus.StimulusLocation);
}
UE_LOG(LogTemp, Display, TEXT("Player lost! Searching last known location..."));
FTimerHandle StopTimer;
GetWorldTimerManager().SetTimer(StopTimer, this, &ACoolGuyAIController::StopChasingPlayer, 10.0f, false);
}
}
}
}
}
}
void ACoolGuyAIController::InitializePatrolPoints()
{
PatrolPoints.Empty();
TArray<AActor*> FoundTargetPoints;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATargetPoint::StaticClass(), FoundTargetPoints);
for (AActor* TargetPoint : FoundTargetPoints)
{
PatrolPoints.Add(TargetPoint->GetActorLocation());
}
UE_LOG(LogTemp, Warning, TEXT("PatrolPoints Count is : %d"), PatrolPoints.Num());
if (PatrolPoints.Num() > 0)
{
Blackboard->SetValueAsVector("PatrolLocation", PatrolPoints[0]);
Blackboard->SetValueAsInt("PatrolIndex", 0);
}
bPatrolPointsReady = true;
}
CoolGuyAIController.cpp
🔹블랙보드, 비헤이비어 설계


블랙보드 키를 생성해주고 비헤이비어 트리를 이미지처럼 설계해준다.
❗비헤이비어 트리 실행 순서는 왼쪽부터
🔹블랙보드 키 Enum

C++에서 생성한 Enum은 비헤이비어 트리 키 타입에 안 뜨기 때문에
블루프린트로 열거형 또 만들어줘야 한다.


🔹MoveTo의 블랙보드 키의 대상 Actor

MoveTo의 블랙보드 키는 Actor여야 옵션에 뜰 수 있다.


2️⃣ 청각
🔹코드
더보기
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Logging/LogMacros.h"
#include "NavigationInvokerComponent.h"
#include "AIController.h"
#include "Engine/TargetPoint.h"
#include "Components/PawnNoiseEmitterComponent.h"
#include "SCC_UEAI_LectureCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
UCLASS(config=Game)
class ASCC_UEAI_LectureCharacter : public ACharacter
{
GENERATED_BODY()
/** Camera boom positioning the camera behind the character */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
USpringArmComponent* CameraBoom;
/** Follow camera */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
UCameraComponent* FollowCamera;
UPROPERTY(BlueprintReadWrite, Category = Navigation, meta = (AllowPrivateAccess = "true"))
UNavigationInvokerComponent* NavInvoker;
/** MappingContext */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputMappingContext* DefaultMappingContext;
/** Jump Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* JumpAction;
/** Move Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* MoveAction;
/** Look Input Action */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
UInputAction* LookAction;
public:
ASCC_UEAI_LectureCharacter();
float NavGenerationRadius;
float NavRemovalRadius;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
bool bIsSucceeded;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
AActor* Target;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
AActor* Target2;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI Movement")
float AcceptanceRadius;
UFUNCTION(BlueprintCallable, Category = "AI Movement")
void MoveToTarget();
UFUNCTION()
void OnMoveCompleted(FAIRequestID RequestID, EPathFollowingResult::Type Result);
UFUNCTION(BlueprintCallable, Category ="AI Movement")
void StartMoving();
UFUNCTION(BlueprintCallable, Category = "AI Movement")
void FindTargetPoints();
//노이즈 이미터 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Noise")
class UPawnNoiseEmitterComponent* NoiseEmitterComponent;
//점프할 때 노이즈 발생
virtual void Jump() override;
protected:
/** Called for movement input */
void Move(const FInputActionValue& Value);
/** Called for looking input */
void Look(const FInputActionValue& Value);
virtual void BeginPlay() override;
virtual void NotifyControllerChanged() override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
private:
UPROPERTY()
AAIController* AIController;
UPROPERTY()
bool bIsMoving;
//소음 발생
void MakeNoise(float Loudness, FVector NoiseLocation);
public:
/** Returns CameraBoom subobject **/
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
/** Returns FollowCamera subobject **/
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
FORCEINLINE class UNavigationInvokerComponent* GetNavInvoker() const { return NavInvoker; }
};
SCC_UEAICharacter.h
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SCC_UEAI_LectureCharacter.h"
#include "Engine/LocalPlayer.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/Controller.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputActionValue.h"
#include "Kismet/GameplayStatics.h"
#include "NavigationSystem.h"
#include "Navigation/PathFollowingComponent.h"
DEFINE_LOG_CATEGORY(LogTemplateCharacter);
//////////////////////////////////////////////////////////////////////////
// ASCC_UEAI_LectureCharacter
ASCC_UEAI_LectureCharacter::ASCC_UEAI_LectureCharacter()
{
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
// Don't rotate when the controller rotates. Let that just affect the camera.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// Configure character movement
GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); // ...at this rotation rate
// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
// instead of recompiling to adjust them
GetCharacterMovement()->JumpZVelocity = 700.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
// Create a camera boom (pulls in towards the player if there is a collision)
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f; // The camera follows at this distance behind the character
CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller
// Create a follow camera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm
NavGenerationRadius = 10.0f;
NavRemovalRadius = 15.0f;
NavInvoker = CreateDefaultSubobject<UNavigationInvokerComponent>(TEXT("NavInvoker"));
NavInvoker->SetGenerationRadii(NavGenerationRadius, NavRemovalRadius);
bIsSucceeded = false;
bIsMoving = false;
AcceptanceRadius = 50.0f;
// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character)
// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)
//노이즈 이미터 컴포넌트 생성
NoiseEmitterComponent = CreateDefaultSubobject<UPawnNoiseEmitterComponent>(TEXT("NoiseEmitterComponent"));
}
//////////////////////////////////////////////////////////////////////////
// Input
void ASCC_UEAI_LectureCharacter::BeginPlay()
{
Super::BeginPlay();
AIController = Cast<AAIController>(GetController());
if (AIController)
{
AIController->ReceiveMoveCompleted.RemoveDynamic(this, &ASCC_UEAI_LectureCharacter::OnMoveCompleted);
AIController->ReceiveMoveCompleted.AddDynamic(this, &ASCC_UEAI_LectureCharacter::OnMoveCompleted);
FindTargetPoints();
StartMoving();
}
}
void ASCC_UEAI_LectureCharacter::NotifyControllerChanged()
{
Super::NotifyControllerChanged();
// Add Input Mapping Context
if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
{
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
else
{
AIController = Cast<AAIController>(Controller);
if (AIController)
{
AIController->ReceiveMoveCompleted.AddDynamic(this, &ASCC_UEAI_LectureCharacter::OnMoveCompleted);
}
}
}
void ASCC_UEAI_LectureCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ASCC_UEAI_LectureCharacter::Move);
// Looking
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASCC_UEAI_LectureCharacter::Look);
}
else
{
UE_LOG(LogTemplateCharacter, Error, TEXT("'%s' Failed to find an Enhanced Input component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this));
}
}
void ASCC_UEAI_LectureCharacter::MakeNoise(float Loudness, FVector NoiseLocation)
{
//소음 발생
if (NoiseEmitterComponent)
{
NoiseEmitterComponent->MakeNoise(this, Loudness, NoiseLocation);
UE_LOG(LogTemp, Warning, TEXT("Noise created at %s with loudness %f"),
*NoiseLocation.ToString(), Loudness);
}
}
void ASCC_UEAI_LectureCharacter::Jump()
{
Super::Jump();
//점프 시 노이즈 생성
MakeNoise(5.0f, GetActorLocation());
}
void ASCC_UEAI_LectureCharacter::StartMoving()
{
FindTargetPoints();
MoveToTarget();
}
void ASCC_UEAI_LectureCharacter::FindTargetPoints()
{
if (!Target || !Target2)
{
TArray<AActor*> FoundTargets;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATargetPoint::StaticClass(), FoundTargets);
if (FoundTargets.Num() >= 2)
{
Target = FoundTargets[0];
Target2 = FoundTargets[1];
UE_LOG(LogTemplateCharacter, Display, TEXT("Found target points: %s and %s"), *Target->GetName(), *Target2->GetName());
}
else
{
UE_LOG(LogTemplateCharacter, Warning, TEXT("Not enough target points found!"));
}
}
}
void ASCC_UEAI_LectureCharacter::MoveToTarget()
{
if (!AIController)
{
UE_LOG(LogTemplateCharacter, Error, TEXT("AIController is null!"));
return;
}
if (bIsMoving)
{
return;
}
AActor* SelectedTaget = bIsSucceeded ? Target : Target2;
if (SelectedTaget)
{
bIsMoving = true;
FVector TargetLocation = SelectedTaget->GetActorLocation();
EPathFollowingRequestResult::Type MoveResult = AIController->MoveToLocation(
TargetLocation,
AcceptanceRadius,
true,
true,
false,
true
);
if (MoveResult == EPathFollowingRequestResult::Failed)
{
UE_LOG(LogTemplateCharacter, Warning, TEXT("Failed to move to target location!"));
bIsMoving = false;
}
else
{
UE_LOG(LogTemplateCharacter, Display, TEXT("Moving to %s (IsSucceeded: %s)"), *SelectedTaget->GetName(), bIsSucceeded ? TEXT("True") : TEXT("False"));
}
}
else
{
UE_LOG(LogTemplateCharacter, Display, TEXT("Selected target is null!"));
}
}
void ASCC_UEAI_LectureCharacter::OnMoveCompleted(FAIRequestID RequestID, EPathFollowingResult::Type Result)
{
bIsMoving = false;
if (Result == EPathFollowingResult::Success)
{
bIsSucceeded = !bIsSucceeded;
UE_LOG(LogTemplateCharacter, Display, TEXT("Move completed successfully. IsSucceeded toggled to: %s"),
bIsSucceeded ? TEXT("True") : TEXT("False"));
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &ASCC_UEAI_LectureCharacter::MoveToTarget, 0.5f, false);
}
else
{
UE_LOG(LogTemplateCharacter, Warning, TEXT("Move failed with result: %d"), static_cast<int32>(Result));
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &ASCC_UEAI_LectureCharacter::MoveToTarget, 1.0f, false);
}
}
void ASCC_UEAI_LectureCharacter::Move(const FInputActionValue& Value)
{
// input is a Vector2D
FVector2D MovementVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
// find out which way is forward
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
// get forward vector
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
// get right vector
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// add movement
AddMovementInput(ForwardDirection, MovementVector.Y);
AddMovementInput(RightDirection, MovementVector.X);
}
}
void ASCC_UEAI_LectureCharacter::Look(const FInputActionValue& Value)
{
// input is a Vector2D
FVector2D LookAxisVector = Value.Get<FVector2D>();
if (Controller != nullptr)
{
// add yaw and pitch input to controller
AddControllerYawInput(LookAxisVector.X);
AddControllerPitchInput(LookAxisVector.Y);
}
}
SCC_UEAICharacter.cpp
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionTypes.h"
#include "Perception/AISense_Hearing.h"
#include "Perception/PawnSensingComponent.h"
#include "CoolGuyAIController.generated.h"
UENUM(BlueprintType)
enum class ESearchStateInternal : uint8
{
Idle = 0,
Investigating = 1,
Chasing = 2,
Searching = 3
};
UCLASS()
class SCC_UEAI_LECTURE_API ACoolGuyAIController : public AAIController
{
GENERATED_BODY()
public:
bool bPatrolPointsReady = false;
ACoolGuyAIController();
UPROPERTY(EditAnywhere, Category = "AI")
UBehaviorTree* BehaviorTree;
UPROPERTY(BlueprintReadWrite, Category = "AI")
UBehaviorTreeComponent* BehaviorTreeComponent;
UPROPERTY(BlueprintReadWrite, Category = "AI")
UBlackboardComponent* BlackboardComponent;
TArray<FVector> PatrolPoints;
UBlackboardComponent* GetBlackboard() const;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI|Perception")
UAIPerceptionComponent* CoolGuyPerceptionComponent;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "AI|Perception")
UAISenseConfig_Sight* SightConfig;
//청각 사용하기 위한 컴포넌트
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI|Sensing")
UPawnSensingComponent* PawnSensingComponent;
protected:
virtual void BeginPlay() override;
virtual void OnPossess(APawn* InPawn) override;
static const FName PatrolLocationKey;
UFUNCTION()
void StartChasingPlayer(AActor* PlayerActor);
UFUNCTION()
void StopChasingPlayer();
UFUNCTION()
void StartSeachingLastLocation();
UFUNCTION()
void OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
//들었을 때 실행할 함수
UFUNCTION()
void OnHearNoise(APawn* PawnInstigator, const FVector& Location, float Volume);
void TestHearing();
private:
FVector CurrentTargetLocation;
void InitializePatrolPoints();
UBlackboardComponent* BB = nullptr;
static const FName TargetPlayerKey;
static const FName CanSeePlayerKey;
static const FName LastSeenLocationKey;
static const FName SearchStateKey;
static const FName LastHeardLocationKey;
static const FName CanHearPlayerKey;
};
CoolGuyAIController.h
#include "CoolGuyAIController.h"
#include "Kismet/GameplayStatics.h"
#include "Engine/TargetPoint.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "GameFramework/Character.h"
const FName ACoolGuyAIController::PatrolLocationKey = "PatrolLocation";
const FName ACoolGuyAIController::TargetPlayerKey = "TargetPlayer";
const FName ACoolGuyAIController::CanSeePlayerKey = "CanSeePlayer";
const FName ACoolGuyAIController::LastSeenLocationKey = "LastSeenLocation";
const FName ACoolGuyAIController::SearchStateKey = "SearchState";
const FName ACoolGuyAIController::LastHeardLocationKey = "LastHeardLocation";
const FName ACoolGuyAIController::CanHearPlayerKey = "CanHearPlayer";
ACoolGuyAIController::ACoolGuyAIController()
{
CoolGuyPerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("PerceptionComponent"));
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
SightConfig->SightRadius = 1500.0f;
SightConfig->LoseSightRadius = 2000.0f;
SightConfig->PeripheralVisionAngleDegrees = 70.0f;
SightConfig->SetMaxAge(5.0f);
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
CoolGuyPerceptionComponent->ConfigureSense(*SightConfig);
CoolGuyPerceptionComponent->SetDominantSense(SightConfig->GetSenseImplementation()); //감각 우선순위 설정
//PawnSensing 컴포넌트 설정(Sound는 Perception이 아닌 PawnSensing으로 접근)
PawnSensingComponent = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensingComponent"));
PawnSensingComponent->HearingThreshold = 1500.0f;
PawnSensingComponent->LOSHearingThreshold = 3000.0f;
PawnSensingComponent->SensingInterval = 0.25f;
PawnSensingComponent->bOnlySensePlayers = false;
PawnSensingComponent->SightRadius = 1500.0f;
}
UBlackboardComponent* ACoolGuyAIController::GetBlackboard() const
{
return Blackboard;
}
void ACoolGuyAIController::BeginPlay()
{
Super::BeginPlay();
//블랙보드 초기화
BB = GetBlackboardComponent();
CoolGuyPerceptionComponent->OnTargetPerceptionUpdated.AddDynamic(this, &ACoolGuyAIController::OnTargetPerceptionUpdated);
//초기 상태 설정
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Idle);
}
//PawnSensing 이벤트 바인딩
if (PawnSensingComponenㅇt)
{
PawnSensingComponent->OnHearNoise.RemoveAll(this);
PawnSensingComponent->OnHearNoise.AddDynamic(this, &ACoolGuyAIController::OnHearNoise);
UE_LOG(LogTemp, Warning, TEXT("PawnSensingComponent OnHearNoise delegate bound"));
}
}
void ACoolGuyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (BehaviorTree)
{
UseBlackboard(BehaviorTree->BlackboardAsset, BlackboardComponent);
RunBehaviorTree(BehaviorTree);
}
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &ACoolGuyAIController::InitializePatrolPoints, 0.5f, false);
}
void ACoolGuyAIController::StartChasingPlayer(AActor* PlayerActor)
{
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Chasing);
BB->SetValueAsObject(TargetPlayerKey, PlayerActor);
}
}
void ACoolGuyAIController::StopChasingPlayer()
{
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Idle);
BB->SetValueAsObject(TargetPlayerKey, nullptr);
BB->SetValueAsBool(CanSeePlayerKey, false);
}
}
void ACoolGuyAIController::StartSeachingLastLocation()
{
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Searching);
}
}
void ACoolGuyAIController::OnTargetPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
ACharacter* PlayerCharacter = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
if (!PlayerCharacter) return;
if (Cast<ACharacter>(Actor) == PlayerCharacter)
{
if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>())
{
bool bCanSeePlayer = Stimulus.WasSuccessfullySensed();
BB = GetBlackboardComponent();
if (BB)
{
BB->SetValueAsObject(TargetPlayerKey, bCanSeePlayer ? Actor : nullptr);
BB->SetValueAsBool(CanSeePlayerKey, bCanSeePlayer);
if (bCanSeePlayer)
{
BB->SetValueAsVector(LastSeenLocationKey, Stimulus.StimulusLocation);
StartChasingPlayer(Actor);
UE_LOG(LogTemp, Warning, TEXT("AI can see the player! Starting chase..."));
}
else
{
ESearchStateInternal CurrentState = (ESearchStateInternal)BB->GetValueAsEnum(SearchStateKey);
if (CurrentState == ESearchStateInternal::Chasing)
{
if (BB)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Investigating);
BB->SetValueAsVector(LastSeenLocationKey, Stimulus.StimulusLocation);
}
UE_LOG(LogTemp, Display, TEXT("Player lost! Searching last known location..."));
FTimerHandle StopTimer;
GetWorldTimerManager().SetTimer(StopTimer, this, &ACoolGuyAIController::StopChasingPlayer, 10.0f, false);
}
}
}
}
}
}
void ACoolGuyAIController::OnHearNoise(APawn* PawnInstigator, const FVector& Location, float Volume)
{
ACharacter* PlayerCharacter = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
if (!PlayerCharacter || !BB) return;
if (PawnInstigator == PlayerCharacter)
{
UE_LOG(LogTemp, Warning, TEXT("AI heard player noise via PawnSensing"));
BB->SetValueAsBool(CanHearPlayerKey, true);
BB->SetValueAsVector(LastHeardLocationKey, Location);
//현재 상태가 Idle이면 Investigating으로 변경
ESearchStateInternal CurrentState = (ESearchStateInternal)BB->GetValueAsEnum(SearchStateKey);
if (CurrentState == ESearchStateInternal::Idle)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Investigating);
UE_LOG(LogTemp, Warning, TEXT("AI state changed to Investigating due to noise"));
}
//타이머 설정으로 소리 감지 상태 초기화
FTimerHandle TimerHandle;
GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this]()
{
if (BB)
{
BB->SetValueAsBool(CanHearPlayerKey, false);
//플레이어를 추적 중이 아니라면 상태 초기화
ESearchStateInternal CurrentState = (ESearchStateInternal)BB->GetValueAsEnum(SearchStateKey);
if (CurrentState == ESearchStateInternal::Investigating)
{
BB->SetValueAsEnum(SearchStateKey, (uint8)ESearchStateInternal::Idle);
UE_LOG(LogTemp, Warning, TEXT("AI returned to Idle state after investigating"));
}
}
}, 5.0f, false);
}
}
void ACoolGuyAIController::InitializePatrolPoints()
{
PatrolPoints.Empty();
TArray<AActor*> FoundTargetPoints;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATargetPoint::StaticClass(), FoundTargetPoints);
for (AActor* TargetPoint : FoundTargetPoints)
{
PatrolPoints.Add(TargetPoint->GetActorLocation());
}
UE_LOG(LogTemp, Warning, TEXT("PatrolPoints Count is : %d"), PatrolPoints.Num());
if (PatrolPoints.Num() > 0)
{
Blackboard->SetValueAsVector("PatrolLocation", PatrolPoints[0]);
Blackboard->SetValueAsInt("PatrolIndex", 0);
}
bPatrolPointsReady = true;
}
CoolGuyAIController.cpp
❗ReportNoiseEvent vs Make Noise
강의에는 ReportNoiseEvent 코드가 나오지만 적용이 안되었는지 Make Noise로 소리를 만드는 듯 하다.
알아보니 둘 다 소리 자극을 생성하는 기능을 수행하지만
MakeNosie는 간단하게 ReportNoiseEvent는 세부적으로 설정할 수 있는 함수이다.
(MakeNosie는 내부적으로 ReportNoiseEvent를 호출)
🔹블랙보드, 비헤이비어 트리 설계


'Unreal' 카테고리의 다른 글
| Unreal5 드래그 앤 드랍 구현 (0) | 2025.06.02 |
|---|---|
| Unreal5 위젯 NativeOn 함수 (0) | 2025.06.02 |
| Unreal5 AI제작3_AI NPC에 행동 부여하기 (0) | 2025.04.23 |
| Unreal5 AI 제작2_상태 머신을 활용한 AI 행동 패턴 설계 (0) | 2025.04.22 |
| Unreal5 AI 제작1_언리얼 AI 제작 (0) | 2025.04.21 |