오늘은 “공격 버튼을 눌렀을 때 애니메이션이 자연스럽게 재생되고, 정확한 타이밍에 데미지가 들어가게 만드는 구조”를 언리얼 애니메이션 시스템 관점에서 정리했다.
결국 핵심은 상태 기반(이동/점프) 은 ABP가 맡고, 이벤트 기반(공격/스킬) 은 Montage가 맡고, 타격 프레임 동기화는 Notify가 맡는 구조라는 것.
언리얼 애니메이션은 “한 파일”이 아니라 여러 에셋/로직이 결합된 구조다.
역할을 나눠서 생각하면 확장(콤보/스킬/버프/디버프)이 쉬워진다.
요소 역할 오늘 연결된 포인트
Skeleton 뼈대 기준 (리타겟/공유 기준) Mixamo 애니메이션도 결국 Skeleton 기준으로 맞춰야 함
Animation Sequence 단일 클립 Punch 같은 원본 애니메이션
Animation Blueprint (ABP) “상태 기반 자동 재생” 로직 Idle/Walk/Run처럼 계속 평가되는 로직
State Machine 상태 전환 관리 Speed, InAir 같은 조건에 의해 전환
Montage “이벤트 기반 수동 재생” 공격/스킬 버튼 입력 시 실행
Slot ABP에 Montage 끼워 넣는 입구 Slot 없으면 Montage 재생 안 됨
오늘 이해한 포인트는 이거였다:
ABP(State Machine) = “캐릭터 상태에 맞춰 자동으로 틀어주는 기본 애니메이션 엔진”
Montage = “플레이어 입력/이벤트로 순간적으로 끼어드는 액션 애니메이션”
왜 둘 다 필요할까?
이동은 계속 변하니까 매 프레임 “조건 평가”가 필요하고
공격/스킬은 버튼을 눌렀을 때 “한 번 실행”이 필요하다.
항목 Animation Blueprint Animation Montage
재생 방식 자동 (State Machine) 수동 (Play Montage)
대표 예시 Idle/Walk/Run/Jump Attack/Skill/Reload
트리거 속도/공중/상태 변수 입력/이벤트
중첩 기본적으로 한 상태만 Slot로 레이어 중첩 가능
제어 주체 ABP(조건) 코드/블루프린트(명령)
✅ 오늘 결론: Blueprint는 “상태 기반”, Montage는 “이벤트 기반”.
오늘 실습에서 가장 중요한 함정 포인트:
ABP AnimGraph에 Slot 노드가 없으면 Montage는 재생되지 않는다.
구조를 그리면 이렇게 된다.
State Machine (Idle/Walk/Run)
↓
Slot(DefaultSlot) ← 몽타주가 이 지점에서 들어와서 섞임
↓
Output Pose
FullBody 공격이면 DefaultSlot
달리면서 상체 공격 같은 걸 하려면 UpperBodySlot 같은 식으로 분리 가능
오늘은 DefaultSlot만 사용해서 전신 공격을 구현.
가장 큰 깨달음은 이거였다:
공격 버튼 누르자마자 데미지 주면
애니메이션이랑 타이밍이 안 맞아서 “헛손질인데 데미지 들어감” 같은 이상한 느낌이 생긴다.
그래서 AnimNotify를 “타격 프레임”에 심어두고, 그 순간에 데미지를 적용한다.
AnimNotify vs AnimNotifyState
종류 타이밍 사용 예시
AnimNotify 한 순간 타격 판정, 발소리, 이펙트 트리거
AnimNotifyState 구간(시작~끝) 무적, 슈퍼아머, 히트박스 지속
오늘 실습은 AnimNotify(한 순간) 기반으로 데미지 적용.
Mixamo는 “애니메이션 라이브러리 + 리깅 포함”이라 실습 속도가 압도적으로 빠르다.
흐름
Mixamo에서 애니 다운로드
UE로 FBX import → Animation Sequence 생성
Sequence 우클릭 → Animation Montage 생성
ABP AnimGraph에서 Slot 확인
입력(IA_LMB) 바인딩 → Play Montage
BP 방식(핵심 개념만)
IA_LMB 입력 받으면
Play Montage(AM_Punch) 실행
➡️ 이건 애니메이션 “재생”까지만.
C++ 방식(핵심 코드 패턴)
헤더
UPROPERTY(EditDefaultsOnly, Category = "Combat")
TObjectPtr<UAnimMontage> AttackMontage;
UPROPERTY(EditDefaultsOnly, Category = "Input")
TObjectPtr<UInputAction> AttackAction;
void Attack();
바인딩 + 재생
void AMyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
if (UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
if (AttackAction)
{
EnhancedInput->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AMyCharacter::Attack);
}
}
}
void AMyCharacter::Attack()
{
if (AttackMontage)
{
PlayAnimMontage(AttackMontage);
}
}
✅ 오늘 정리: 입력 → Montage 실행은 코드/블루프린트 어디서든 동일한 패턴.
오늘 구현은 “Custom AnimNotify”를 만들어서 Montage 타임라인 특정 프레임에서 실행되게 했다.
핵심 아이디어
타격 프레임에 AN_DealDamage를 놓는다
Notify가 호출되는 순간
전방 SphereTrace로 적 탐색
GAS ASC를 통해 GameplayEffect로 데미지 적용
Notify 클래스 뼈대
UCLASS()
class MYPROJECT_API UAN_DealDamage : public UAnimNotify
{
GENERATED_BODY()
public:
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
float TraceRadius = 50.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
float TraceStartDistance = 50.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
float TraceEndDistance = 150.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
TSubclassOf<UGameplayEffect> DamageEffectClass;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bShowDebug = false;
};
Notify 본체 로직 흐름(오늘 구현의 의미)
Owner(공격자) 가져오기
Forward 기준 Start/End 계산
SphereTrace로 맞은 대상들 찾기
공격자 ASC, 타겟 ASC 가져오기
GameplayEffectSpec 만들어서 적용
이 부분은 오늘 “GAS 데미지 처리의 기본 뼈대”로 기억해둘 만하다.
FGameplayEffectContextHandle Context = ASC->MakeEffectContext();
Context.AddSourceObject(Attacker);
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(EffectClass, Level, Context);
if (Spec.IsValid())
{
ASC->ApplyGameplayEffectSpecToTarget(*Spec.Data.Get(), TargetASC);
}