UE5/Devlog

[UE5] Ragdoll Fight Devlog 2 - Animation asset

sulfurman 2025. 6. 7. 23:59

 


프로젝트에 다양한 에셋을 도입했으나, 그중 가장 핵심적인 것은 캐릭터 애니메이션 에셋이다. 개인 프로젝트이므로 모델링이나 사운드 같은 세부 요소에 많은 시간과 리소스를 할애하기 어려웠다. 그때 Unreal Engine에서 최근 공개한 Game Animation Sample Project | Motion Matching이라는 무료 에셋을 발견했다.

Game Animation Sample Project | Motion Matching | Unreal Engine

 

 이 에셋은 모션 매칭 시스템을 활용해 사용자의 입력에 따라 500여 가지 애니메이션 중 최적의 동작을 실시간으로 선택·재생한다. 걷기·달리기·웅크리기 같은 기본 동작부터 벽 넘기·오르기·매달리기 등 파쿠르 수준의 고난이도 액션까지 모두 포함된다. 더불어 모든 애니메이션이 네트워크 환경에서도 복제(Replication)되어 멀티플레이어 게임에 그대로 적용된다는 점이 이 프로젝트를 완성하는 데 결정적 역할을 했다.

 

에셋 적용 영상

 

 이전 글에서 언급했듯이, 멀티플레이 환경에서 가장 중요한 점 중 하나인 게임 내 모든 정보는 서버를 거쳐야한다는 점을 위한 네트워킹 작업이 적어도 애니메이션 부분에서는 끝나있다는 점이라고 할 수 있다. 그 점에서 모든 애니메이션에서 Replication이 적용된 에셋이라는 점은 프로젝트를 진행할 때 기능 구현쪽 네트워킹 시스템에만 집중할 수 있다는 것을 의미했다.

 

 그래서 게임 기능 + 네트워킹 시스템을 실제로 문제없이 구현할 수 있는지 테스트하기 위해 슈팅 게임의 핵심인 '총 발사'를 구현을 진행해 보았다.

 

애니메이션 퀄리티에 비례한 복잡한 블루프린트 구성. 오직 캐릭터의 Input만 관리하는 블루프린트가 이 정도이다...

 

 이를 위해 먼저 애니메이션 에셋의 작동 방식을 면밀히 분석해야 했다. 해당 에셋은 언리얼 엔진 블루프린트를 기반으로 제작되었으며, CharacterBP와 CharacterABP(애니메이션 BP)를 중심으로 구현되어 있다. 따라서 애니메이션 관련 로직은 블루프린트에서 처리하고, 그 외 모든 핵심 로직은 C++ 클래스에서 구현하기로 결정했다. 그 이유는 다음과 같다.

  1. 핵심 애니메이션 에셋 자체가 블루프린트로 구성되어 있다.
  2. 애니메이션 처리 부하가 크지 않다고 판단되었다.
  3. 이후 새로운 애니메이션을 추가할 때 별도의 네트워킹 시스템을 추가 구성할 필요가 없다.

분석을 완료하고, '총 발사' 구현을 위한 로직은 크게 두가지로 나누어 졌다.

 

1. C++ 인풋 관리

 구현 목표는 투사체(Projectile)를 이용해 캐릭터가 총을 발사하도록 만드는 것이다. 이를 위해 가장 먼저 해결해야 할 과제는

C++ 클래스에서 인풋을 관리하는 것이다.

 

 게임에서 키를 한 가지만 누르는 경우는 오히려 드물다. 뛰어다니면서 총을 쏘고, 재장전하거나 아이템을 사용하는 등 복합적인 동작이 동시에 일어나기 때문이다. 이때 단순히 키 입력이 들어왔다고 모든 동작을 수행할 수는 없다. 조준 중에는 달릴 수 없어야 하고, 파쿠르 동작 중에는 사격이 불가능해야 한다. 이런 얽힌 입력 상태를 관리하기 위해

Enhanced InputCharacterInputState를 도입했다.

  1. Enhanced Input
    • 각 액션마다 입력 조건(키, 축 등)을 정의
    • 입력이 발생하면 자동으로 CharacterInputState 업데이트
  2. CharacterInputState
    • 최종 확정된 입력 상태를 담는 C++ USTRUCT
    • 네트워크 복제를 통해 서버·클라이언트 모두 동일한 상태 유지
    • 여섯 개의 bool 멤버로 구성:
      • WantsToAim
      • WantsToSprint
      • WantsToWalk
      • WantsToStrafe
      • WantsToFire
      • WantsToTraversal (파쿠르)

 수정 전 대부분의 인풋 처리는 블루프린트에서 이루어지고 있었다. 이 상태에서는 C++ 클래스가 캐릭터의 인풋을 직접 받아올 수 없어, 발사 트리거도 구현할 수 없으며 인풋에 따른 효과를 C++에서 제어할 수도 없다. 블루프린트에서 받은 인풋을 C++로 동기화하려 시도했으나, 이는 구조적 제약으로 실패했다.  

 따라서 블루프린트가 사용하는 모든 인풋을 C++ USTRUCT CharacterInputState_Cpp(이하 CharacterInputState)로 관리하도록 시스템을 재설계했다. 

//PUBGCharacter.h
USTRUCT(BlueprintType)
struct FCharacterInputState_Cpp
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	bool WantsToSprint;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	bool WantsToWalk;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	bool WantsToStrafe;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	bool WantsToAim;

};

public:

//...

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated)
	FCharacterInputState_Cpp CharacterInputStatecpp;
	
	
//...

	UFUNCTION(BlueprintCallable, Category = "Input")
	FORCEINLINE void SetCharacterInputState_cpp(FCharacterInputState_Cpp State) { CharacterInputStatecpp = State; }

	UFUNCTION(BlueprintPure, Category = "Input")
	FORCEINLINE FCharacterInputState_Cpp GetCharacterInputState_cpp() const { return CharacterInputStatecpp; }

 

 원래 에셋의 블루프린트 구조는 Enhanced Input으로 인풋을 수집한 뒤, UpdateInputState 함수를 통해 서버와 모든 클라이언트에 동기화하는 방식이다. 동기화된 인풋을 기반으로 속력·방향 등의 움직임을 계산하고, 그에 알맞은 애니메이션을 호출하는 흐름이다.

내가 수행한 작업은 CharacterInputState를 C++로 정의해 블루프린트 노드를 전부 교체한 것이다.

기존에 블루프린트에 배치된 CharacterInputState의 Setter/Getter 노드를 제거하고, C++로 작성한 CharacterInputState 구조체로 대체했다.

 

정리하면

1. Enhanced Input 이벤트가 PUBGCharacterBP(블루프린트)에서 트리거되어,

CharacterInputState(C++ USTRUCT)의 6개 bool 멤버를 업데이트한다.

 

2. 업데이트된 CharacterInputState가 서버/클라이언트에 복제(Replication) 된다.

 

3. 블루프린트, 내부 C++ 클래스들이 6개 bool 값을 읽은 뒤
이동·애니메이션·사격(Projectile 스폰)·재장전 등의 로직을 실행한다.

 

 

 

이런식으로 진행하면 블루프린트, C++ 모두에게 동일한 InputState를 가지게되면서 애니메이션과 실제 로직이 동기화 된다!

Fire Input 시스템

 

 

개편한 인풋 시스템으로 구체적인 상황으로 예시를 들어보겠다. 예시 상황은 다음과 같다.

(발사 애니메이션은 PUBGCharacterABP에 구현을 마쳤고, 발사, 투사체 관련 내부로직은 C++ 내부에 구현을 마친 상태)

캐릭터가 총을 발사하는 동안 앞으로 이동하며, 장애물이 있을시 파쿠르를 이용해 넘어간다.

 

 

1. IA_Fire, IA_Aim, IA_Move 액션(해당 키를 누르면)이 PUBGCharacterBP의 Enhanced Input에서 트리거된다.

 

2. Enhanced Input에서 트리거된 액션에 따라, CharacterInputState USTRUCT의 WantsToFire, WantsToAim, WantsToWalk 멤버 값을 업데이트(UpdateInputState 함수 호출)한다.

 

3. Replication된 CharacterInputState가 서버와 모든 클라이언트에 동기화된다. (네트워킹 시스템)

 

4. PUBGCharacterABP(Animation BP)에서 WantsToAimWantsToFire를 읽어, 조준 및 발사 애니메이션을 재생한다.

 

5. C++ 쪽도 마찬가지로 WantsToFire 로 인해 Fire() 함수, Projectile 관련 클래스들WantsToFire가 true일 때만 활성화되면서, 투사체의 생명주기(스폰 → 이동 → 파괴)를 처리한다.

 

6. 이때 발사중인 캐릭터가 파쿠르를 진행하면 Enhanced Input의 IA_Fire 액션에 포함되어있는 조건에 의해 WantsToFire가 비활성화 되면서, 파쿠르 중에는 총이 발사되지 않는다.

 

 

결과

 

'총 발사' 를 위한 두번째 구현인 발사 애니메이션과 총 핵심 로직은 분량상 다음 제작기에...