개인프로젝트/AFO_Refactor

[AFO_Refactor #2] 매 프레임 Cast 체인 제거 - bIsSprinting 베이스 클래스 이동

Client Side 2026. 6. 15. 15:18

AFO_Refactor 개발일지 #2

매 프레임 Cast 체인 제거

bIsSprinting 베이스 클래스 이동 · AnimInstance Cast 3번 → 0번 · 파생 클래스 정리

🔍 문제 — 매 프레임 Cast 3번

AFAnimInstance.cppNativeUpdateAnimation()매 프레임 실행되는 함수다. 여기서 캐릭터 타입을 판별하기 위해 Cast를 3번 연속으로 시도하고 있었다.

BEFORE — NativeUpdateAnimation() 내부

// ❌ 매 프레임 Cast 3번 실행
if (AAFPlayerCharacter* BaseChar = Cast<AAFPlayerCharacter>(OwnerCharacter))
{
    if (AAFDarkKnight* DK = Cast<AAFDarkKnight>(BaseChar))
        bIsSprinting = DK->bIsSprinting;
    else if (AAFMage* Mage = Cast<AAFMage>(BaseChar))
        bIsSprinting = Mage->bIsSprinting;
    else if (AAFAurora* Aurora = Cast<AAFAurora>(BaseChar))
        bIsSprinting = Aurora->bIsSprinting;
    else
        bIsSprinting = false;
}

bIsSprinting = bIsSprinting && (GroundSpeed > 3.f);

문제는 두 가지였다.

① 성능

Cast는 런타임 타입 체크(RTTI)다. 60fps 기준 초당 180번 실행된다. 캐릭터 수가 늘어날수록 비례해서 증가한다.

② 확장성

캐릭터가 추가될 때마다 AnimInstance 코드도 수정해야 한다. AnimInstance가 모든 캐릭터 클래스에 의존하는 구조 — 전형적인 OCP(개방-폐쇄 원칙) 위반이다.

🔎 원인 분석

bIsSprinting이 각 파생 클래스에 따로 선언되어 있었다.

// AFDarkKnight.h
bool bIsSprinting = false;

// AFMage.h
bool bIsSprinting = false;

// AFAurora.h
bool bIsSprinting = false;

베이스 클래스인 AFPlayerCharacter에는 없고, 파생 클래스마다 따로 선언했기 때문에 AnimInstance에서 이 값을 읽으려면 각 타입으로 Cast해야만 했다.

✅ 해결 — bIsSprinting 베이스 클래스로 이동

Cast 체인이 필요한 근본 이유를 제거했다. bIsSprintingAFPlayerCharacter로 올리고, 파생 클래스의 중복 선언을 전부 제거했다.

Step 1 — AFPlayerCharacter.h에 bIsSprinting 추가

// AFPlayerCharacter.h
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Movement")
bool bIsSprinting = false;

Step 2 — AFPlayerCharacter.cpp StartSprint/StopSprint에 플래그 설정

void AAFPlayerCharacter::StartSprint(const FInputActionValue& value)
{
    if (!bCanSprint) return;
    if (GetCharacterMovement())
        GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
    bUseControllerRotationYaw = false;
    GetCharacterMovement()->bOrientRotationToMovement = true;
    bIsSprinting = true;
}

void AAFPlayerCharacter::StopSprint(const FInputActionValue& value)
{
    if (!bCanSprint) return;
    if (GetCharacterMovement())
        GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;
    bIsSprinting = false;
}

Step 3 — AFAnimInstance Cast 체인 제거

AFTER

// Cast 체인 전체 삭제, 한 줄로 교체
bIsSprinting = OwnerCharacter ? OwnerCharacter->bIsSprinting : false;

bIsSprinting = bIsSprinting && (GroundSpeed > 3.f);

Step 4 — 파생 클래스 정리

AFMage, AFAurora에서 이제 불필요해진 코드를 제거했다.

// AFMage.h / AFAurora.h — 제거
bool bIsSprinting = false;  // ← 삭제 (베이스로 올라감)

// AFMage.cpp / AFAurora.cpp — StartSprint/StopSprint override 제거
// bIsSprinting 설정만 하던 override는 더 이상 필요 없음

📊 Before / After 비교

Before After
매 프레임 Cast 횟수 3번 0번
캐릭터 추가 시 AnimInstance 수정 필요 불필요
bIsSprinting 선언 위치 파생 클래스 3곳에 중복 베이스 클래스 1곳
AnimInstance의 캐릭터 클래스 의존성 DarkKnight, Mage, Aurora 직접 참조 AFPlayerCharacter만 참조

📌 이번 작업에서 배운 것

Cast 남발은 의존성 문제의 신호다.

AnimInstance가 DarkKnight, Mage, Aurora를 직접 알고 있다는 건 설계가 잘못됐다는 신호다. AnimInstance는 캐릭터의 구체적인 타입을 몰라야 한다. 공통 데이터를 베이스로 올리는 것만으로 의존성이 끊기고, Cast도 사라진다.

NativeUpdateAnimation은 비용에 민감하다.

60fps 기준 초당 60번 실행된다. 여기서 Cast를 하면 캐릭터 수 × 60번이 된다. 모바일 환경에서는 이 비용이 PC보다 크게 느껴진다. Tick/Update 계열 함수에서는 항상 "꼭 필요한가?"를 먼저 물어봐야 한다.

📌 다음 포스팅

[AFO_Refactor #3] GAS 도입 — AbilitySystemComponent + AttributeSet 셋업

기존 UAFAttributeComponent 기반 HP/Mana 처리를 GAS AttributeSet으로 교체하는 첫 단계. ASC를 PlayerState에 붙이는 구조 설계부터 시작.