UE5 이동속도 동기화와 모션매칭 안정화 트러블슈팅

2026. 3. 25. 19:00·Dev./UE 언리얼 엔진

 

이 글은 UE5 프로젝트에서 기획 스탯 → 캐릭터 이동속도 → 애니메이션 모션매칭이라는 파이프라인 위에서 터진 문제를 분석하고 해결한 기록이다. 추가로, 해결 과정에서 생긴 코드 중복을 정리한 리팩토링까지 다룬다.

 

GAS(Gameplay Ability System)를 활용해 캐릭터 스탯을 관리하거나, UE5의 모션매칭(Pose Search)으로 로코모션을 구축하고 있다면 분명 한 번쯤 마주칠 문제들이다.

 

캐릭터가 화면 위에서 자연스럽게 뛰어다니려면 사실 세 개의 층이 정확하게 맞물려야 한다.

  • 첫 번째 층: 데이터 층
    • 데이터 테이블에 "이 캐릭터는 민첩 1.2배"라고 기입하면, 시스템을 타고 실제 이동속도에 반영되어야 한다.
  • 두 번째 층: 물리 층
    • 데이터가 반영된 이동속도로 캐릭터의 캡슐(충돌체)이 월드 위를 실제로 움직인다.
  • 세 번째 층: 애니메이션 층
    • 캡슐이 움직이는 속도에 딱 맞는 걷기나 달리기 모션이 재생되어야 한다.

이 세 층 중 하나라도 어긋나면 눈에 바로 보인다.

첫 번째 층이 깨지면 속도가 아예 안 바뀌고, 두 번째 층과 세 번째 층 사이가 어긋나면 발이 미끄러지거나 다리가 꼬인다.


이슈 1 — 기획 스탯이 런타임 이동속도에 반영되지 않는다


증상

데이터 테이블(DT_PlayerStat)에 캐릭터별 민첩(AGL) 값을 기입해두었다.

AGL이란 이동속도에 곱해지는 보정 배율로, 1.0이면 기본 속도, 1.1이면 10% 빠른 캐릭터를 의미한다.

그런데 인게임에서 테스트하면 AGL 수치가 적용되지 않았다.


원인

코드를 추적해보니 문제의 뿌리는 "기준 속도가 어디에 있는가"에 대한 합의가 없었다는 점이었다.

 

블루프린트(BP_SeekerCharacter)에서는 MaxWalkSpeed를 600으로 설정해두었다.

그런데 코드에서는 GS_PlayerState와 MovementAttributeSet(GAS에서 이동 관련 수치를 관리하는 어트리뷰트 묶음)에는 500.f라는 전혀 다른 숫자가 하드코딩되어 있었다.

 

초기화 과정에서 AGL 값을 절대치로 덮어씌우는 곳도 있었고, 배율로 곱하는 곳도 있었다.

같은 변수를 두 군데서 서로 다른 방식으로 건드리니, 결과는 예측 불가능해진다.

 

비유하자면 이렇다.

요리사 두 명이 하나의 냄비를 놓고 한 명은 "소금 5g을 넣자"고 하고, 다른 한 명은 "지금 들어 있는 양의 1.1배로 맞추자"고 한다. 둘 다 자기 방식대로 손을 대면 맛은 아무도 책임질 수 없다.


해결

MovementAttributes->MoveSpeed가 들고 있는 값의 성격을 바꿨다.

이전에는 절대 속도값(500, 600 같은 cm/s 단위)을 직접 저장했지만,

이제는 순수한 배율(1.0, 1.1, 0.8 같은 비율)만 저장하도록 구조를 변경했다.

그리고 실제 이동속도를 계산하는 공식을 하나로 통일했다.

최종 이동속도 = 블루프린트 원본 MaxWalkSpeed × MoveSpeed(배율)

C++ 초기화 단계(InitAbilityActorInfo)와 버프 적용 단계(PostGameplayEffectExecute) 모두 이 하나의 공식만 사용하도록 정리했다.

 

하드코딩된 500.f는 완전히 제거했다.

블루프린트에 직접 세팅한 MaxWalkSpeed 원본 수치를 가져와서, 데이터 테이블의 AGL 배율을 곱하는 것이 전부다.

 

이렇게 하면 기획자는 블루프린트에서 기준 속도를 조절하고, 데이터 테이블에서 캐릭터별 배율을 조절한다.

C++은 그 둘을 곱하기만 한다. 역할이 깔끔하게 분리된다.


이슈 2 — 속도가 빨라지니 캐릭터가 미끄러진다

증상

이슈 1을 해결하자 AGL이 정상 반영되기 시작했다. 그런데 AGL 1.5짜리 빠른 캐릭터를 달리게 하니,

캐릭터의 캡슐은 초속 900~1000cm로 질주하는데 발은 초속 600cm 기준의 달리기 모션 그대로였다.

몸은 슝슝 나아가는데 발은 제자리걸음 수준이라, 마치 아이스스케이팅을 타는 것처럼 보였다.


원인

BEFORE

UE5의 모션매칭은 데이터베이스에 없는 속도 구간의 모션은 만들어낼 수 없다.

데이터베이스에 들어 있는 가장 빠른 달리기 애니메이션이 600cm/s 기준이라면,

900cm/s로 달리는 캐릭터에게도 그 600cm/s 모션을 억지로 재생한다.

 

모션매칭은 "가장 잘 맞는 것"을 고를 뿐이지, 없는 것을 창조하지는 않는다.


해결

이벤트 그래프
애님 그래프

모든 속도 구간에 대응하는 애니메이션을 일일이 제작하는 것은 현실적이지 않다.

대신 Stride Warping(보폭 워핑, 기존 애니메이션의 다리 벌림 폭을 실시간으로 늘리거나 줄여서, 실제 이동속도에 발걸음을 맞추는 프로시저럴 기법)을 도입했다.

⭐ Bone 세팅 (디테일 패널)
- 골반 본 (Pelvis Bone): pelvisIK
- 발 루트 본 (IK Foot Root Bone): ik_foot_root
- 발 정의 (Foot Definitions):
▶ 인덱스 [0] (왼발 세팅)
└ IK 발 본: ik_foot_l
└ FK 다리 본: foot_l
└ 허벅지 본: thigh_l

▶ 인덱스 [1] (오른발 세팅)
└  IK 발 본: ik_foot_r
└  FK 다리 본: foot_r
└  허벅지 본: thigh_r

 

AnimBP에 Stride Warping 노드를 추가하고, 이 노드에 "지금 캐릭터가 기준 속도 대비 몇 배로 달리고 있는지"를 알려주면 된다. 1.5배 속도라면 보폭도 1.5배로 벌려주는 식이다.

 

이 배율을 계산하는 전용 함수를 C++ 측에 만들었다.

GetAnimStrideScale()이라는 이름의 브릿지 함수로, 내부 로직은 단순하다.

Stride Scale = 현재 실제 속도 ÷ 블루프린트 원본 MaxWalkSpeed

이 값을 적절한 범위로 클램프해서 Stride Warping 노드에 넘긴다.

이전에는 AnimBP 안에서 블루프린트 노드를 여러 개 연결해 같은 계산을 하고 있었는데,

이를 C++ 함수 하나로 대체하면서 AnimBP 그래프도 깔끔해지고 프레임당 연산 비용도 줄었다.

 

결과적으로, 캐릭터가 아무리 빨라져도 발걸음이 속도에 맞게 자동으로 늘어나면서 아이스스케이팅 현상이 사라졌다.


이슈 3 — 달리는 도중 다리가 어기적거린다

증상

이슈 2까지 잡고 나서 테스트를 돌리는데, 이번에는 캐릭터가 달리다가 미세하게 멈칫거리는 현상이 나타났다.

신나게 달리다가 갑자기 감속 모션이 끼어들었다가 다시 달리기로 돌아오는...


원인

이번 범인은 모션매칭 데이터베이스 내부에 있었다.

모션매칭 데이터베이스(PSD_..._Loops)에는 순수한 달리기 루프(Run_Loop) 외에도 M_Neutral_Transition_Sprint_to_Run_... 같은 감속 트랜지션이 포함되어 있었다.

 

이 트랜지션 애니메이션들은 원래 자연스러운 속도 변화를 표현하기 위해 만들어진 것이다.


해결

해결은 의외로 단순했다. 모션매칭 데이터베이스에서 문제를 일으키는 Sprint_to_Run 계열 감속 트랜지션 애니메이션들을 비활성화했다.

 

"애니메이션이 많을수록 자연스럽지 않나?"라고 생각하기 쉽지만, 모션매칭에서는 애매한 트랜지션 데이터가 오히려 품질을 떨어뜨리는 함정이 된다. 모션매칭 알고리즘은 데이터베이스에 있는 모든 후보 중 “가장 비용이 낮은” 모션을 고르는데, 애매한 속도 구간을 커버하는 트랜지션이 존재하면 미세한 입력 변화마다 그쪽으로 튀었다가 돌아오기를 반복한다.

 

잡다한 감속 트랜지션을 덜어내자, 모션매칭은 오직 순수한 달리기 루프만 일관되게 재생했다.


후속 리팩토링 — CDO 접근 중복 제거와 캐싱

세 가지 이슈를 해결하고 나서 코드를 다시 살펴보니, 과정에서 생긴 부산물이 눈에 들어왔다.

문제

이슈 1과 2를 고치면서 "블루프린트 원본 MaxWalkSpeed"를 가져오는 코드가 프로젝트 내 4곳에 복사·붙여넣기 되어 있었다.

float BP_BaseSpeed = Character->GetClass()
    ->GetDefaultObject<ACharacter>()
    ->GetCharacterMovement()->MaxWalkSpeed;

이 코드는 CDO(Class Default Object, 블루프린트 클래스의 "공장 출하 시 기본값"을 보관하는 객체)에 접근해서 원본 MaxWalkSpeed를 읽어온다. 같은 코드가 4곳에 흩어져 있으면 유지보수가 어렵다.

 

더 큰 문제는 GetAnimStrideScale() 함수 내부에도 이 코드가 들어 있었다는 점이다.

이 함수는 AnimBP에서 매 프레임 호출된다.

매 프레임마다 CDO를 조회하는 것은 결과가 항상 같은 값인데도 불필요한 비용을 치르는 셈이다.


해결

AGS_BaseCharacter에 GetBPBaseSpeed()라는 헬퍼 함수를 하나 만들었다.

// GS_BaseCharacter.h
float GetBPBaseSpeed() const;
mutable float CachedBPBaseSpeed = 0.f;

// GS_BaseCharacter.cpp
float AGS_BaseCharacter::GetBPBaseSpeed() const
{
    if (CachedBPBaseSpeed <= 0.f)
        CachedBPBaseSpeed = GetClass()
            ->GetDefaultObject<ACharacter>()
            ->GetCharacterMovement()->MaxWalkSpeed;
    return CachedBPBaseSpeed;
}

첫 호출 시에만 CDO에 접근해서 값을 캐싱하고, 이후에는 캐싱된 값을 즉시 반환한다.

블루프린트의 기본 MaxWalkSpeed는 런타임 중에 변하지 않으므로 이 캐싱은 안전하다.

 

기존에 CDO 접근 코드가 흩어져 있던 GS_PlayerCharacter.cpp 2곳과 GS_MovementAttributeSet.cpp 2곳을 모두 GetBPBaseSpeed() 호출로 교체했다. MovementAttributeSet 내부에서는 기존의 Cast<ACharacter>를 Cast<AGS_BaseCharacter>로 변경해서 헬퍼 함수에 접근할 수 있게 했다.

 

정리하면, GetDefaultObject 호출 지점은 4곳에서 1곳으로 줄었다.

GetAnimStrideScale()의 프레임당 CDO 조회는 매 프레임에서 0회로 바뀌었다.

'Dev. > UE 언리얼 엔진' 카테고리의 다른 글

UE5 Kawaii Physics로 머리카락 물리를 구현하며 마주친 문제  (0) 2026.03.27
[캐릭터] Ears 모프 타겟을 바꿨는데 에디터에서 왜 안보이지?  (0) 2026.03.27
AI와 함께 Unreal Engine 플러그인을 만드는 워크플로우 — 삽질을 시스템으로 바꾸는 법  (0) 2026.03.19
UE5 Motion Matching 5편 :: 다른 프로젝트에 이 시스템을 옮기는 방법  (0) 2026.03.15
UE5 Motion Matching 4편 :: Strafe 전환에서 만난 문제들과 해결 과정  (0) 2026.03.15
'Dev./UE 언리얼 엔진' 카테고리의 다른 글
  • UE5 Kawaii Physics로 머리카락 물리를 구현하며 마주친 문제
  • [캐릭터] Ears 모프 타겟을 바꿨는데 에디터에서 왜 안보이지?
  • AI와 함께 Unreal Engine 플러그인을 만드는 워크플로우 — 삽질을 시스템으로 바꾸는 법
  • UE5 Motion Matching 5편 :: 다른 프로젝트에 이 시스템을 옮기는 방법
raindrovvv
raindrovvv
raindrovvv 님의 블로그 입니다.
  • raindrovvv
    raindrovvv 님의 블로그
    raindrovvv
  • 전체
    오늘
    어제
    • 분류 전체보기 (170) N
      • Dev. (163) N
        • AI 인공지능 (27)
        • UE 언리얼 엔진 (81) N
        • Unity 유니티 (0)
        • Wwise 와이즈 (7)
        • 게임 네트워크 (8)
        • 그래픽스 Graphics (22)
        • 프로젝트 (8)
        • 기타 개발 관련 (10)
      • Computer Science (0)
        • 하드웨어 HW (0)
        • 소프트웨어 SW (0)
        • 통신 (0)
        • 데이터 (0)
      • 블로그 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Unreal
    Wwise
    트러블슈팅
    에이전트
    인디게임
    그래픽스
    devlog
    Git
    바이브코딩
    언리얼엔진
    생산성
    UE
    dev
    깃
    unrealengine
    TA
    네트워크
    언리얼
    AI
    게임개발
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
raindrovvv
UE5 이동속도 동기화와 모션매칭 안정화 트러블슈팅
상단으로

티스토리툴바