김하연 튜터님 강의 - ‘AI 특강 1강 - 이동 & 감지’
AI 특강 - 이동 & 감지
김하연 튜터님의 Notion 자료를 바탕으로 강의를 들으며
일부를 수정 및 재작성한 블로깅용 글
1. AI 시스템의 전체 구조 🧠
플레이어 vs AI의 차이점
- 플레이어 캐릭터: 키보드/마우스 입력 → Player Controller → Character → 움직임
- AI 캐릭터: 프로그래밍된 로직 → AI Controller → Character → 움직임
언리얼 AI의 핵심 요소들
- AI Controller (뇌)
- 무엇을 할지 결정하는 곳
- 의사 결정을 담당
- 무엇을 할지 결정하는 곳
- Pawn/Character (몸)
- 실제로 맵에서 움직이는 메시
- Controller의 명령을 받아 실행
- 실제 World에 존재하는 Mesh 같은 것
- Player/AI 인지에 따라 그 역할이 달라짐
- 실제로 맵에서 움직이는 메시
- Navigation System (지도)
- AI가 “어디로 갈 수 있는지” 알려주는 시스템
- 장애물을 피해가는 경로 계산
- AI가 “어디로 갈 수 있는지” 알려주는 시스템
- Perception System (감각)
- 시각, 청각 등 감지 시스템
(오늘은 여기까지 할 예정)
- 시각, 청각 등 감지 시스템
- Blackboard (기억)
- AI의 단기 기억 저장소
- “플레이어 위치”, “경계 상태” 등 저장
- AI의 단기 기억 저장소
- Behavior Tree (행동 결정)
- IF-THEN 로직을 시각적으로 표현
- 만약 플레이어가 보이면 → 쫓아가기
- IF-THEN 로직을 시각적으로 표현
2. 기본 AI 생성 - 아무것도 안하고 서있는 AI 😴
1. SpartaAIController 클래스 생성 및 작성
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "SpartaAIController.generated.h"
UCLASS()
class SPARTAPROJECT_API ASpartaAIController : public AAIController
{
GENERATED_BODY()
public:
ASpartaAIController();
protected:
virtual void OnPossess(APawn* InPawn) override;
};
#include "SpartaAIController.h"
ASpartaAIController::ASpartaAIController()
{
}
void ASpartaAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (InPawn)
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] AI Controller is controlling %s."), *InPawn->GetName());
}
}
2. SpartaAICharacter 클래스 생성 및 작성
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaAICharacter.generated.h"
UCLASS()
class SPARTAPROJECT_API ASpartaAICharacter : public ACharacter
{
GENERATED_BODY()
public:
ASpartaAICharacter();
protected:
virtual void BeginPlay() override;
};
#include "SpartaAICharacter.h"
#include "SpartaAIController.h"
ASpartaAICharacter::ASpartaAICharacter()
{
AIControllerClass = ASpartaAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
void ASpartaAICharacter::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Warning, TEXT("[Sparta] AI character has been spawned."));
}
3. Navigation 추가 - 랜덤하게 돌아다니는 AI 🎲
1. SpartaAIController 클래스 수정
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "SpartaAIController.generated.h"
UCLASS()
class SPARTAPROJECT_API ASpartaAIController : public AAIController
{
GENERATED_BODY()
public:
ASpartaAIController();
protected:
virtual void BeginPlay() override;
virtual void OnPossess(APawn* InPawn) override;
private:
void MoveToRandomLocation();
FTimerHandle RandomMoveTimer;
UPROPERTY(EditAnywhere, Category = "AI")
float MoveRadius = 1000.0f;
};
#include "SpartaAIController.h"
#include "TimerManager.h"
#include "NavigationSystem.h"
ASpartaAIController::ASpartaAIController()
{
}
void ASpartaAIController::BeginPlay()
{
Super::BeginPlay();
GetWorldTimerManager().SetTimer(
RandomMoveTimer,
this,
&ASpartaAIController::MoveToRandomLocation,
3.0f,
true,
1.0f
);
}
void ASpartaAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (InPawn)
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] AI Controller is controlling %s."), *InPawn->GetName());
}
}
void ASpartaAIController::MoveToRandomLocation()
{
APawn* MyPawn = GetPawn();
if (!MyPawn)
{
UE_LOG(LogTemp, Error, TEXT("[Sparta] No Pawn to control."));
}
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
if (!NavSystem)
{
UE_LOG(LogTemp, Error, TEXT("[Sparta] Could not find Navigation System."));
}
FNavLocation RandomLocation;
bool bFoundLocation = NavSystem->GetRandomReachablePointInRadius(
MyPawn->GetActorLocation(),
MoveRadius,
RandomLocation
);
if (bFoundLocation)
{
MoveToLocation(RandomLocation.Location);
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Move target: %s"), *RandomLocation.Location.ToString());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Could not find a reachable location."));
}
}
-
MoveToLocation 같은 Nav 함수는
AI로 조작하기에
Get Current Accelation 같은 ‘Input’ 기반의 함수로 ABP에서 검사하는 경우는
ABP가 망가질 수 있음 -
ABP 쪽에서 Player 와 AI 가 컨트롤하는지에 따라 분기를 나누는 것을 권장
2. AI 이동 속도 제어
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaAICharacter.generated.h"
UCLASS()
class SPARTAPROJECT_API ASpartaAICharacter : public ACharacter
{
GENERATED_BODY()
public:
ASpartaAICharacter();
void SetMovementSpeed(float NewSpeed);
UPROPERTY(EditAnywhere, Category = "AI")
float WalkSpeed = 300.0f;
UPROPERTY(EditAnywhere, Category = "AI")
float RunSpeed = 600.0f;
protected:
virtual void BeginPlay() override;
};
#include "SpartaAICharacter.h"
#include "SpartaAIController.h"
#include "GameFramework/CharacterMovementComponent.h"
ASpartaAICharacter::ASpartaAICharacter()
{
AIControllerClass = ASpartaAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
UCharacterMovementComponent* Movement = GetCharacterMovement();
Movement->MaxWalkSpeed = WalkSpeed;
Movement->bOrientRotationToMovement = true;
Movement->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
}
void ASpartaAICharacter::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Warning, TEXT("[Sparta] AI character has been spawned."));
}
void ASpartaAICharacter::SetMovementSpeed(float NewSpeed)
{
if (UCharacterMovementComponent* Movement = GetCharacterMovement())
{
Movement->MaxWalkSpeed = NewSpeed;
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Speed changed: %.1f"), NewSpeed);
}
}
4. Perception 추가 - AI에게 눈을 달아주기 👀
1. SpartaAIController 클래스 수정
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionTypes.h"
#include "SpartaAIController.generated.h"
class UAIPerceptionComponent;
class UAISenseConfig_Sight;
UCLASS()
class SPARTAPROJECT_API ASpartaAIController : public AAIController
{
GENERATED_BODY()
public:
ASpartaAIController();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
UAIPerceptionComponent* AIPerception;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
UAISenseConfig_Sight* SightConfig;
UFUNCTION()
void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
virtual void BeginPlay() override;
virtual void OnPossess(APawn* InPawn) override;
private:
void MoveToRandomLocation();
FTimerHandle RandomMoveTimer;
UPROPERTY(EditAnywhere, Category = "AI")
float MoveRadius = 1000.0f;
};
#include "SpartaAIController.h"
#include "TimerManager.h"
#include "NavigationSystem.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
ASpartaAIController::ASpartaAIController()
{
AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
SetPerceptionComponent(*AIPerception);
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
SightConfig->SightRadius = 1500.0f;
SightConfig->LoseSightRadius = 2000.0f;
SightConfig->PeripheralVisionAngleDegrees = 90.0f;
SightConfig->SetMaxAge(5.0f);
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
AIPerception->ConfigureSense(*SightConfig);
AIPerception->SetDominantSense(SightConfig->GetSenseImplementation());
}
void ASpartaAIController::BeginPlay()
{
Super::BeginPlay();
if (AIPerception)
{
AIPerception->OnTargetPerceptionUpdated.AddDynamic(
this,
&ASpartaAIController::OnPerceptionUpdated
);
}
GetWorldTimerManager().SetTimer(
RandomMoveTimer,
this,
&ASpartaAIController::MoveToRandomLocation,
3.0f,
true,
1.0f
);
}
void ASpartaAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (InPawn)
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] AI Controller is controlling %s."), *InPawn->GetName());
}
}
void ASpartaAIController::MoveToRandomLocation()
{
APawn* MyPawn = GetPawn();
if (!MyPawn)
{
UE_LOG(LogTemp, Error, TEXT("[Sparta] No Pawn to control."));
}
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
if (!NavSystem)
{
UE_LOG(LogTemp, Error, TEXT("[Sparta] Could not find Navigation System."));
}
FNavLocation RandomLocation;
bool bFoundLocation = NavSystem->GetRandomReachablePointInRadius(
MyPawn->GetActorLocation(),
MoveRadius,
RandomLocation
);
if (bFoundLocation)
{
MoveToLocation(RandomLocation.Location);
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Move target: %s"), *RandomLocation.Location.ToString());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Could not find a reachable location."));
}
}
void ASpartaAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (Stimulus.WasSuccessfullySensed())
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Saw something! %s"), *Actor->GetName());
DrawDebugString(
GetWorld(),
Actor->GetActorLocation() + FVector(0, 0, 100),
FString::Printf(TEXT("Saw: %s"), *Actor->GetName()),
nullptr,
FColor::Green,
2.0f,
true
);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Missed it! %s"), *Actor->GetName());
DrawDebugString(
GetWorld(),
Actor->GetActorLocation() + FVector(0, 0, 100),
FString::Printf(TEXT("Missed: %s"), *Actor->GetName()),
nullptr,
FColor::Red,
2.0f,
true
);
}
}
-
감지 조건은 다양하기에
(현재는 ‘시야’에 기반한 Sight 지정) -
우선시하는 ‘감지 조건’(Dominant) 설정도 가능
-
아군/중립/적군 같은 감지 조건도 가능
-
이렇게 AI에 대한 ‘감지’ 기능만 추가해도 플레이어는 감지 X
Player가 ‘감지 될 수 있는’ 기능 또한 넣어주어야 함
(아마도 불필요한 감지를 피하기 위해?)
// ... 기존 코드에 추가
#include "Perception/AIPerceptionStimuliSourceComponent.h"
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
UAIPerceptionStimuliSourceComponent* StimuliSource;
// ... 기존 코드에 추가
#include "Perception/AISense_Sight.h"
AThirdPersonCharacter::AThirdPersonCharacter()
{
StimuliSource = CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("StimuliSource"));
}
void AThirdPersonCharacter::BeginPlay()
{
Super::BeginPlay();
if (StimuliSource)
{
StimuliSource->RegisterForSense(TSubclassOf<UAISense_Sight>());
StimuliSource->RegisterWithPerceptionSystem();
}
}
- UAIPerceptionStimuliSourceComponent 를 통해
감지 시스템에 등록되도록 설정
(Sight 와 같이 감지될 조건에 추가 가능)
5. 추적 기능 - 플레이어를 쫓아가는 AI 🏃🏻
1. SpartaAIController 클래스 수정
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionTypes.h"
#include "SpartaAIController.generated.h"
class UAIPerceptionComponent;
class UAISenseConfig_Sight;
UCLASS()
class SPARTAPROJECT_API ASpartaAIController : public AAIController
{
GENERATED_BODY()
public:
ASpartaAIController();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
UAIPerceptionComponent* AIPerception;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
UAISenseConfig_Sight* SightConfig;
UPROPERTY()
AActor* CurrentTarget = nullptr;
UFUNCTION()
void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
bool bIsChasing = false;
FTimerHandle ChaseTimer;
virtual void BeginPlay() override;
virtual void OnPossess(APawn* InPawn) override;
void StartChasing(AActor* Target);
void StopChasing();
void UpdateChase();
private:
void MoveToRandomLocation();
FTimerHandle RandomMoveTimer;
UPROPERTY(EditAnywhere, Category = "AI")
float MoveRadius = 1000.0f;
};
#include "SpartaAIController.h"
#include "TimerManager.h"
#include "NavigationSystem.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "SpartaAICharacter.h"
#include "Kismet/GameplayStatics.h"
ASpartaAIController::ASpartaAIController()
{
AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
SetPerceptionComponent(*AIPerception);
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
SightConfig->SightRadius = 1500.0f;
SightConfig->LoseSightRadius = 2000.0f;
SightConfig->PeripheralVisionAngleDegrees = 90.0f;
SightConfig->SetMaxAge(5.0f);
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
AIPerception->ConfigureSense(*SightConfig);
AIPerception->SetDominantSense(SightConfig->GetSenseImplementation());
}
void ASpartaAIController::BeginPlay()
{
Super::BeginPlay();
if (AIPerception)
{
AIPerception->OnTargetPerceptionUpdated.AddDynamic(
this,
&ASpartaAIController::OnPerceptionUpdated
);
}
GetWorldTimerManager().SetTimer(
RandomMoveTimer,
this,
&ASpartaAIController::MoveToRandomLocation,
3.0f,
true,
1.0f
);
}
void ASpartaAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
if (InPawn)
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] AI Controller is controlling %s."), *InPawn->GetName());
}
}
void ASpartaAIController::MoveToRandomLocation()
{
APawn* MyPawn = GetPawn();
if (!MyPawn)
{
UE_LOG(LogTemp, Error, TEXT("[Sparta] No Pawn to control."));
}
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
if (!NavSystem)
{
UE_LOG(LogTemp, Error, TEXT("[Sparta] Could not find Navigation System."));
}
FNavLocation RandomLocation;
bool bFoundLocation = NavSystem->GetRandomReachablePointInRadius(
MyPawn->GetActorLocation(),
MoveRadius,
RandomLocation
);
if (bFoundLocation)
{
MoveToLocation(RandomLocation.Location);
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Move target: %s"), *RandomLocation.Location.ToString());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Could not find a reachable location."));
}
}
void ASpartaAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
if (Actor != PlayerPawn)
{
return;
}
if (Stimulus.WasSuccessfullySensed())
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Saw something! %s"), *Actor->GetName());
DrawDebugString(
GetWorld(),
Actor->GetActorLocation() + FVector(0, 0, 100),
FString::Printf(TEXT("Saw: %s"), *Actor->GetName()),
nullptr,
FColor::Green,
2.0f,
true
);
StartChasing(Actor);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("[Sparta] Missed it! %s"), *Actor->GetName());
DrawDebugString(
GetWorld(),
Actor->GetActorLocation() + FVector(0, 0, 100),
FString::Printf(TEXT("Missed: %s"), *Actor->GetName()),
nullptr,
FColor::Red,
2.0f,
true
);
StopChasing();
}
}
void ASpartaAIController::StartChasing(AActor* Target)
{
if (bIsChasing && CurrentTarget == Target) return;
CurrentTarget = Target;
bIsChasing = true;
GetWorldTimerManager().ClearTimer(RandomMoveTimer);
if (ASpartaAICharacter* AIChar = Cast<ASpartaAICharacter>(GetPawn()))
{
AIChar->SetMovementSpeed(AIChar->RunSpeed);
}
UpdateChase();
GetWorldTimerManager().SetTimer(
ChaseTimer,
this,
&ASpartaAIController::UpdateChase,
0.25f,
true
);
}
void ASpartaAIController::StopChasing()
{
if (!bIsChasing) return;
CurrentTarget = nullptr;
bIsChasing = false;
GetWorldTimerManager().ClearTimer(ChaseTimer);
StopMovement();
if (ASpartaAICharacter* AIChar = Cast<ASpartaAICharacter>(GetPawn()))
{
AIChar->SetMovementSpeed(AIChar->WalkSpeed);
}
GetWorldTimerManager().SetTimer(
RandomMoveTimer,
this,
&ASpartaAIController::MoveToRandomLocation,
3.0f,
true,
2.0f
);
}
void ASpartaAIController::UpdateChase()
{
if (CurrentTarget && bIsChasing)
{
MoveToActor(CurrentTarget, 100.0f);
}
}
댓글남기기