[DAY55] Shooter Project(5) Dynamic Attack Rotation

베리투스·2025년 10월 28일
0

Shooter Project

목록 보기
6/10

어제 구현한 AI의 전투 기능에 디테일을 더하는 작업을 진행했다. 공격 애니메이션이 재생되는 동안에도 플레이어를 향해 몸을 회전시켜, 보다 역동적이고 위협적인 느낌을 주도록 개선했다. 이 과정에서 회전 각도를 제한하여 AI가 팽이처럼 도는 것을 방지했고, C++의 클래스 형변환(Casting)과 컨트롤러의 회전 제어 방식에 대한 중요한 실수를 바로잡으며 깊이 이해할 수 있었다. 🤺


📌 목표

  • AI 공격 중 플레이어 방향으로 동적 회전 기능 구현
  • 회전 반경(각도)을 제한하여 자연스러운 움직임 연출
  • 컨트롤러 회전 및 클래스 형변환(Casting) 개념 실습

📖 이론

1. 컨트롤러 회전과 캐릭터 회전

언리얼 엔진에서 캐릭터의 움직임과 회전은 분리되어 제어될 수 있다. 특히 AI의 경우, "AI 컨트롤러"가 바라봐야 할 방향을 결정하면, "캐릭터(Pawn)"가 그 방향을 따라가도록 설정하는 것이 정석이다.

  • bUseControllerRotationYaw: 캐릭터의 Yaw(Z축) 회전을 컨트롤러의 회전 값과 일치시킬지 결정하는 Pawn의 속성이다.
  • SetControlRotation(): AI 컨트롤러의 회전 값을 실제로 변경하는 함수이다. 이 함수를 Tick에서 계속 호출하면, 컨트롤러가 특정 대상을 계속 주시하게 만들 수 있다.

2. 클래스 형변환 (Casting)

부모 클래스 타입의 포인터가 실제로는 자식 클래스의 인스턴스를 가리키고 있을 때, 이 포인터를 다시 자식 클래스 타입으로 변환하는 것을 의미한다. 자식 클래스에만 선언된 고유한 변수나 함수에 접근하기 위해 반드시 필요하다.

  • 상황: BTTask 노드는 범용성을 위해 기본 AAIController 포인터를 가져온다.
  • 문제: 우리가 만든 AttackStartRotation 변수는 AAIC_Monster에 선언되어 있다.
  • 해결: Cast<AAIC_Monster>(OwnerComp->GetAIOwner()) 코드를 통해 기본 컨트롤러를 우리가 만든 컨트롤러 타입으로 변환하여, AttackStartRotation에 접근 권한을 얻는다.

💻 코드

AIC_Monster.h

#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:
	// ... 기존 코드 ...
};

BTT_Attack.cpp

#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;
}

// ...

AIC_Monster.cpp

#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_HearingUAISenseConfig_Damage를 설정했지만, 정작 플레이어 캐릭터에 AIPerceptionStimuliSource 컴포넌트를 추가하고 AISense_Hearing을 등록하는 것을 잊어버렸다. AI는 감지할 준비가 되어 있었지만, 플레이어는 소리나 데미지 자극을 월드에 방출하지 않고 있었던 것이다. AI가 플레이어를 인지하지 못하는 경우, AI 자체의 문제뿐만 아니라 "AI가 감지해야 할 대상(플레이어)이 자극을 방출하고 있는지"도 반드시 확인해야 한다는 것을 배웠다.


✅ 핵심 요약

개념설명비고
bUseControllerRotationYaw캐릭터(Pawn)의 Yaw 회전을 컨트롤러의 회전과 동기화시키는 설정.CharacterMovementComponent의 관련 설정과 함께 사용해야 부드럽다.
SetControlRotation()컨트롤러의 실제 회전값을 변경하는 유일하고 올른 함수.Get 함수는 값을 읽어올 뿐, 그 반환값을 수정해도 원본은 불변.
FMath::Clamp / RInterpTo값의 범위를 제한하거나 두 값 사이를 부드럽게 보간하는 데 사용하는 수학 함수.게임 내 동적인 움직임을 자연스럽게 만드는 데 필수적인 도구이다.
profile
Shin Ji Yong // Unreal Engine 5 공부중입니다~

0개의 댓글