애니메이션 몽타주

DevBison·2025년 9월 1일

0) 라이브 코딩과 디버깅

  • F5 디버깅은 실행이 꺼졌다 재시작되므로, 실행 중 수정하려면 라이브 코딩을 켠다.
  • 끄는 법: 편집 > 에디터 개인설정 > 라이브 코딩 > 활성화 체크 해제

1) 공격 몽타주 생성

애니메이션 > 애니메이션 몽타주

사용할 스켈레톤 선택

AMWarriorAttack 몽타주 생성

PrimaryAttack 시리즈 애니메이션을 드래그해 배치하면, 링크되어 있으면 순차 재생된다.


2) 슬롯/그룹과 T-포즈 이슈

창 > 애님 슬롯 매니저

슬롯 추가 > Attack

세팅 후 T-포즈가 보이면

기존 섹션 이름이 Default라서 헷갈릴 수 있다 → 섹션 이름을 바꾸면 정상 재생

추가 팁

  • AnimGraph에 Attack 슬롯 노드가 경로상에 없으면 몽타주가 출력되지 않는다.
  • 몽타주의 슬롯 이름과 AnimGraph Slot 노드 이름이 다르면 재생되지 않는다.
  • 스켈레톤이 다른 애셋을 섞으면 T-포즈가 뜬다.

3) 섹션 분할과 링크

Attack1~4 섹션 생성

섹션 링크 상태 확인

독립 재생하려면 섹션 링크를 끊는다

섹션 이름 가이드

  • 간단: Attack1, Attack2, Attack3, Attack4
  • 확장: Attack_Air, Attack_Down, Attack_Heavy 등 컨텍스트를 이름에 담아두면 유지보수에 유리

4) 슬롯 노드와 AnimGraph 경로

특정 슬롯의 몽타주만 재생하도록 Slot 노드를 사용한다

DefaultSlot 개념

Slot에 재생 중인 몽타주가 없으면 Source 포즈가 그대로 통과한다

AnimGraph에 Attack 슬롯 배치

같은 경로 구조라면 보이는 변화는 없을 수 있다

개선 팁

  • 상체만 공격 모션, 하체는 이동 유지하려면 Slot 위에 Layered blend per bone을 배치하고 Spine_01 기준으로 분기한다.
  • 몽타주가 전신을 잠식해 움직임이 딱딱해지는 문제를 줄일 수 있다.

5) AnimInstance 변수와 에디터 셋업

AttackMontage 변수 선언

클래스 디폴트에서 AMWarriorAttack 할당

입력 처리 위치 예시(엔핸스드 인풋과 연동)

APlayerCharacter.h에 NormalAttack 가상함수

cpp 기본 구현

Warrior.h 오버라이드

Warrior.cpp 구현

APlayerCharacter.cpp NormalAttack 구현 확인

핵심 포인트

  • mAnimInst 타입은 UPlayerDefaultAnimInstance* 로 캐스팅해두면 멤버 함수 접근이 편하다.
  • AttackMontage가 비어 있으면 재생되지 않는다.

6) PlayAttack 구현과 섹션 배열

헤더에 PlayAttack 선언

cpp 정의

AnimInstance 유효 체크

Montage_Play, Montage_JumpToSection 사용

섹션 이름 배열 + 현재 인덱스

Attack Section Name 가시화

Attack1~4 이름 지정

배열에 이름이 잘 들어간 상태

PlayAttack에서 섹션 점프

샘플 코드

// PlayerDefaultAnimInstance.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Attack")
TObjectPtr<UAnimMontage> AttackMontage;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Attack")
TArray<FName> AttackSectionNames = { "Attack1","Attack2","Attack3","Attack4" };

int32 AttackIndex = 0;

UFUNCTION(BlueprintCallable) void PlayAttack();
UFUNCTION() void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted);
// PlayerDefaultAnimInstance.cpp
void UPlayerDefaultAnimInstance::PlayAttack()
{
    if (!AttackMontage || AttackSectionNames.Num() == 0) return;

    if (!Montage_IsPlaying(AttackMontage))
    {
        Montage_Play(AttackMontage, 1.0f);
        Montage_JumpToSection(AttackSectionNames[AttackIndex], AttackMontage);

        FOnMontageEnded End;
        End.BindUObject(this, &UPlayerDefaultAnimInstance::OnMontageEnded);
        Montage_SetEndDelegate(End, AttackMontage);
    }
}

void UPlayerDefaultAnimInstance::OnMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
    if (!bInterrupted) AttackIndex = 0;
}

7) 이동 중 어색함과 지상 조건

이동 중 전신 몽타주가 들어오면 부자연스럽다 → 지상에서만 허용

지상 체크 예시

void APlayerCharacter::NormalAttack()
{
    if (GetCharacterMovement() && GetCharacterMovement()->IsMovingOnGround())
    {
        if (AnimInst) AnimInst->PlayAttack();
    }
}

개선 팁

  • 상체 전용 Layered blend per bone + Attack Slot로 하체 이동 유지
  • 몽타주 Root Motion을 쓰면 이동/회전이 함께 걸릴 수 있으니 프로젝트 목적에 맞춰 비활성/활성 결정

8) 콤보·히트 판정 노티파이

히트 타이밍과 콤보 전환 타이밍이 필요

애니메이션 더블클릭으로 타임라인 열기

공격판정 노티파이 Attack 생성

무기 스윙 위치에 충돌체 또는 트레이스 적용 권장

AttackCombo 노티파이도 각 섹션에 배치

몽타주 종료 콜백 함수 선언

cpp 정의

MontageEnd 호출 시점

Interrupted true/false 의미

AnimNotify 함수 선언

cpp 구현

노티파이 샘플

// PlayerDefaultAnimInstance.h
UFUNCTION() void AnimNotify_Attack();      // 히트 판정
UFUNCTION() void AnimNotify_AttackCombo(); // 콤보 인덱스 증가
// PlayerDefaultAnimInstance.cpp
void UPlayerDefaultAnimInstance::AnimNotify_Attack()
{
    // 무기 소켓 경로로 짧은 스윙 트레이스 추천
    // UKismetSystemLibrary::SphereTraceMultiForObjects(...) 등
}

void UPlayerDefaultAnimInstance::AnimNotify_AttackCombo()
{
    AttackIndex = (AttackIndex + 1) % AttackSectionNames.Num();
}

고급 팁

  • 입력 타이밍을 통제하려면 노티파이 대신 노티파이 윈도우(AnimNotifyState)로 콤보 입력 가능 구간을 열고 닫는다.
  • 사용자가 콤보 키를 해당 구간에 입력하면 다음 섹션을 Montage_SetNextSection으로 자연스럽게 이어줄 수 있다.

9) 입력 시스템 연결(엔핸스드 인풋)

기본 흐름

  • 프로젝트 시작 시 EnhancedInputLocalPlayerSubsystem에 IMC(매핑 컨텍스트) 추가
  • IA_Attack을 액션으로 만들고 캐릭터에서 바인딩
  • 바인딩된 함수에서 NormalAttack 호출

예시

void APlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    if (UEnhancedInputComponent* EIC = Cast<UEnhancedInputComponent>(PlayerInputComponent))
    {
        EIC->BindAction(IA_Attack, ETriggerEvent::Started, this, &APlayerCharacter::Attackkey);
    }
}

void APlayerCharacter::Attackkey(const FInputActionValue& Value)
{
    UE_LOG(LogTemp, Log, TEXT("Attackkey Enter"));
    NormalAttack();
}

10) Super 호출은 언제 필요한가

문제 상황 요약

  • Warrior에서 Super::NormalAttack();를 호출하니 동작했고, 호출하지 않으면 동작하지 않았다.

이유

  • 현재 설계에서 부모 APlayerCharacter의 NormalAttack()이 실제 재생 로직(mAnimInst->PlayAttack())을 가지고 있다.
  • 자식이 부모를 호출하지 않으면 재생 로직이 실행되지 않는다.

정리

  • Super 호출은 항상 필요한 게 아니라, 부모가 필수 로직을 가지고 있을 때만 필요하다.
  • 대안 1: 부모를 순수 가상 함수로 두고 재생 로직을 전부 자식이 갖는다.
  • 대안 2: 재생은 AnimInstance 쪽으로만 몰고, 캐릭터 계층에서는 입력 신호 전달만 한다.

권장 구조

  • 입력 → 캐릭터 → AnimInstance.PlayAttack()
  • 재생의 단일 책임을 AnimInstance로 모아 테스트/유지보수 난이도를 낮춘다.

11) 유지보수 팁과 체크리스트

체크리스트

  • AttackMontage 변수에 실제 몽타주 할당됐나
  • AnimGraph 경로에 Attack 슬롯 노드가 있나
  • 슬롯 이름이 몽타주 슬롯과 정확히 같나(대소문자 주의)
  • 스켈레톤이 일치하나
  • 섹션 이름 오타 없는가
  • 이동 중 공격 자연스럽게 하려면 상체 전용 블렌딩을 썼나
  • 루트모션을 의도대로 켜거나 껐나

성능/안정성

  • 섹션 이름은 FString 대신 FName 사용
  • 델리게이트 바인딩은 중복되지 않도록 관리
  • ComboIndex는 범위를 모듈로 연산으로 안정화

테스트 팁

  • 애님 프리뷰에서 Section Jump 미리보기
  • 노티파이 타이밍은 프레임 스텝으로 미세 조정
  • 디버그용 DrawDebugSphere/Line으로 히트 영역 시각화

12) 마무리

한 개의 몽타주로 4콤보를 깔끔하게 관리하려면

  • 섹션을 잘게 나누고 링크는 필요할 때만
  • 슬롯/AnimGraph 경로를 정확히 배치
  • AnimInstance 중심의 PlayAttack, 콤보 인덱스, EndDelegate, 노티파이를 조합
  • 상체 전용 블렌딩과 콤보 윈도우로 퀄리티 업
profile
응애 개발자

0개의 댓글