어제 구현한 AI의 전투 기능에 디테일을 더하는 작업을 진행했다. 공격 애니메이션이 재생되는 동안에도 플레이어를 향해 몸을 회전시켜, 보다 역동적이고 위협적인 느낌을 주도록 개선했다. 이 과정에서 회전 각도를 제한하여 AI가 팽이처럼 도는 것을 방지했고, C++의 클래스 형변환(Casting)과 컨트롤러의 회전 제어 방식에 대한 중요한 실수를 바로잡으며 깊이 이해할 수 있었다. 🤺
언리얼 엔진에서 캐릭터의 움직임과 회전은 분리되어 제어될 수 있다. 특히 AI의 경우, "AI 컨트롤러"가 바라봐야 할 방향을 결정하면, "캐릭터(Pawn)"가 그 방향을 따라가도록 설정하는 것이 정석이다.
bUseControllerRotationYaw: 캐릭터의 Yaw(Z축) 회전을 컨트롤러의 회전 값과 일치시킬지 결정하는 Pawn의 속성이다.SetControlRotation(): AI 컨트롤러의 회전 값을 실제로 변경하는 함수이다. 이 함수를 Tick에서 계속 호출하면, 컨트롤러가 특정 대상을 계속 주시하게 만들 수 있다.부모 클래스 타입의 포인터가 실제로는 자식 클래스의 인스턴스를 가리키고 있을 때, 이 포인터를 다시 자식 클래스 타입으로 변환하는 것을 의미한다. 자식 클래스에만 선언된 고유한 변수나 함수에 접근하기 위해 반드시 필요하다.
BTTask 노드는 범용성을 위해 기본 AAIController 포인터를 가져온다.AttackStartRotation 변수는 AAIC_Monster에 선언되어 있다.Cast<AAIC_Monster>(OwnerComp->GetAIOwner()) 코드를 통해 기본 컨트롤러를 우리가 만든 컨트롤러 타입으로 변환하여, AttackStartRotation에 접근 권한을 얻는다.#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Perception/AIPerceptionTypes.h"
#include "AIC_Monster.generated.h"
UCLASS()
class SPARTA_TPROJECT_02_API AAIC_Monster : public AAIController
{
GENERATED_BODY()
public:
AAIC_Monster();
virtual void Tick(float DeltaSeconds) override;
// 공격 시작 시점의 회전값을 저장할 변수
FRotator AttackStartRotation;
protected:
virtual void OnPossess(APawn* InPawn) override;
private:
// ... 기존 코드 ...
};
#include "BTT_Attack.h"
#include "AIC_Monster.h" // 캐스팅을 위해 헤더 포함
#include "AIController.h"
#include "AIMonsterBase.h"
#include "Animation/AnimInstance.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
// ...
EBTNodeResult::Type UBTT_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// ...
// 기본 AAIController를 AAIC_Monster로 캐스팅
AAIC_Monster* MonsterController = Cast<AAIC_Monster>(OwnerComp->GetAIOwner());
if (MonsterController == nullptr)
{
return EBTNodeResult::Failed;
}
AAIMonsterBase* Monster = Cast<AAIMonsterBase>(MonsterController->GetPawn());
// ...
// 캐스팅된 컨트롤러를 통해 AttackStartRotation 변수에 접근
MonsterController->AttackStartRotation = MonsterController->GetControlRotation();
Monster->bIsAttacking = true;
// ...
return EBTNodeResult::InProgress;
}
// ...
#include "AIC_Monster.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Sparta_TProject_02/Sparta_TProject_02Character.h"
#include "AIMonsterBase.h"
#include "Kismet/KismetMathLibrary.h"
// ...
void AAIC_Monster::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
AAIMonsterBase* Monster = Cast<AAIMonsterBase>(GetPawn());
if (Monster == nullptr) return;
// 몬스터가 공격 중일 때만 회전 로직 실행
if (Monster->bIsAttacking)
{
AActor* TargetActor = Cast<AActor>(GetBlackboardComponent()->GetValueAsObject(TEXT("TargetActor")));
if (TargetActor)
{
// 1. 목표 방향 계산
FRotator TargetRotation = UKismetMathLibrary::FindLookAtRotation(Monster->GetActorLocation(), TargetActor->GetActorLocation());
// 2. 시작 방향 기준으로 각도 차이 계산 및 정규화
FRotator DeltaRot = (TargetRotation - AttackStartRotation).GetNormalized();
// 3. 최대 회전 각도(좌우 45도)로 제한
float MaxYaw = 45.0f;
DeltaRot.Yaw = FMath::Clamp(DeltaRot.Yaw, -MaxYaw, MaxYaw);
// 4. 제한된 최종 목표 회전값 계산
FRotator ClampedTargetRotation = AttackStartRotation + DeltaRot;
// 5. 현재 방향에서 최종 목표 방향으로 부드럽게 보간
FRotator NewRotation = FMath::RInterpTo(GetControlRotation(), ClampedTargetRotation, DeltaSeconds, 5.0f);
// 6. 실제 컨트롤러 회전값 변경
SetControlRotation(FRotator(0.f, NewRotation.Yaw, 0.f));
}
}
}
// ...
AAIController의 멤버가 아니라는 오류 (C2039): BTT_Attack에서 AAIController 타입의 포인터로 AttackStartRotation에 접근하려고 시도했다. 이 변수는 우리가 직접 만든 AAIC_Monster에만 존재하므로, Cast<AAIC_Monster>를 통해 포인터의 타입을 명확히 알려줘야만 접근이 가능했다. "상속 관계와 자식 클래스 고유 멤버"에 대한 중요한 교훈이었다.
회전이 적용되지 않는 문제: Tick 함수에서 GetControlRotation().Yaw = NewRotation.Yaw; 와 같이 코드를 작성했었다. 하지만 GetControlRotation() 함수는 컨트롤러의 현재 회전값 "복사본"을 반환하는 것이었다. 복사본의 값을 아무리 바꿔도 원본은 변하지 않는 당연한 원리를 잠시 잊었다. 반드시 SetControlRotation() 함수를 사용해야 실제 컨트롤러의 회전이 변경된다.
청각/피격 감지 후 AI가 반응하지 않는 문제: AIC_Monster.cpp에서 UAISenseConfig_Hearing과 UAISenseConfig_Damage를 설정했지만, 정작 플레이어 캐릭터에 AIPerceptionStimuliSource 컴포넌트를 추가하고 AISense_Hearing을 등록하는 것을 잊어버렸다. AI는 감지할 준비가 되어 있었지만, 플레이어는 소리나 데미지 자극을 월드에 방출하지 않고 있었던 것이다. AI가 플레이어를 인지하지 못하는 경우, AI 자체의 문제뿐만 아니라 "AI가 감지해야 할 대상(플레이어)이 자극을 방출하고 있는지"도 반드시 확인해야 한다는 것을 배웠다.
| 개념 | 설명 | 비고 |
|---|---|---|
bUseControllerRotationYaw | 캐릭터(Pawn)의 Yaw 회전을 컨트롤러의 회전과 동기화시키는 설정. | CharacterMovementComponent의 관련 설정과 함께 사용해야 부드럽다. |
SetControlRotation() | 컨트롤러의 실제 회전값을 변경하는 유일하고 올른 함수. | Get 함수는 값을 읽어올 뿐, 그 반환값을 수정해도 원본은 불변. |
FMath::Clamp / RInterpTo | 값의 범위를 제한하거나 두 값 사이를 부드럽게 보간하는 데 사용하는 수학 함수. | 게임 내 동적인 움직임을 자연스럽게 만드는 데 필수적인 도구이다. |