내배캠/GAS(Gameplay Ability System)

GAS_02_기본 애니메이션

동그래님 2025. 5. 2. 17:15

📌 Animation Instance 구조

코드 분리와 재사용성을 높이기 위해, 공용/전용 애니메이션 로직을 명확히 구분하여 확장성과 유지보수를 개선하기 위한 구조

 

✅ Locomotion 구현

기본적인 Locomotion 이동에 필요한 로직 구현

 

🔹CharacterAnimInstance.h

#pragma once

#include "CoreMinimal.h"
#include "AnimIstances/RPGBaseAnimInstance.h"
#include "CharacterAnimInstance.generated.h"

class ARPGBaseCharacter;
class UCharacterMovementComponent;

UCLASS()
class RPG_API UCharacterAnimInstance : public URPGBaseAnimInstance
{
	GENERATED_BODY()
	
public:
	// 초기화 시점 처리
	virtual void NativeInitializeAnimation() override; 
    // Tick 대신 쓰는 업데이트 함수(성능 최적화 ver)
	virtual void NativeThreadSafeUpdateAnimation(float DeltaSeconds) override; 

protected:
	UPROPERTY()
	ARPGBaseCharacter* OwningCharacter;

	UPROPERTY()
	UCharacterMovementComponent* OwningMovementComponent;

	UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "AnimData|LocomotionData")
	float GroundSpeed;

	UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "AnimData|LocomotionData")
	bool bHasAcceleration;
};

 

🔹CharacterAnimInstance.cpp

#include "AnimIstances/CharacterAnimInstance.h"
#include "Characters/RPGBaseCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"

void UCharacterAnimInstance::NativeInitializeAnimation()
{
	OwningCharacter = Cast<ARPGBaseCharacter>(TryGetPawnOwner());

	if (OwningCharacter)
	{
		OwningMovementComponent = OwningCharacter->GetCharacterMovement();
	}
}

void UCharacterAnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds)
{
	if (!OwningCharacter || !OwningMovementComponent)
	{
		return;
	}

	GroundSpeed = OwningCharacter->GetVelocity().Size2D();
	bHasAcceleration = OwningMovementComponent->GetCurrentAcceleration().SizeSquared2D() > 0.0f;
}

AnimInstance.h에 선언되어 있는 함수이다.
override해서 사용할 때, 세부 구현이 없는 가상함수여서 Super 키워드 불필요하다.
Character가 스폰되고 SkeletalMeshComponent에 AnimIstance가 생성되고 바인딩 되었을 때, 이 함수가 호출된다.
그래서 Owner 캐릭터 및 Component 캐싱 작업 등 초기화 작업을 처리하기 적합하다.

 

이 함수 역시 AnimInstance.h에 선언되어있고, 세부 구현이 없는 함수이다.
게임의 Tick( ) 대신 실행되는 함수이다.
Worker Thread(멀티스레드)에서 호출되고, 애니메이션 계산을 병렬화하여 성능 최적화가 가능하다.

다만 함수 이름처럼 멀티스레드 환경에서도 데이터 충돌, 크래시 없이 안전하게 실행될 수 있도록 유의해야한다.
이 함수는 게임 스레드가 아닌 워커 스레드에서 실행되기 때문에, GC에 영향을 받는 UPROPERTY 값을 수정하여 한 스레드에서는 읽고 다른 스레드에서 쓰는 상황이 생기게 된다면, 데이터 레이스나 크래시가 발생할 수 있다. 또한 UObject(Actor, Component 등)에 직접 접근하면 안된다.

따라서 NativeInitializeAnimation 함수에서 UObject를 캐싱해두고, 이 함수에서는 캐싱된 포인터에서 값을 읽고 AnimBP에 사용할 목적의 변수 값 저장하는 용도로만 사용하는 것이 안전하다.

 

에디터에서 "Blend Space 1D"를 생성해준 뒤, 속성 설정 및 Idle과 Jog 애니메이션을 할당

 

AnimationBP를 생성해주고, State Machine을 생성해 Output Pose에 연결한다.
AnimInstance에서 캐싱해둔, Ground Speed와 Has Acceleration 변수를 확인할 수 있다.

 

맨 손 상태의 "Idle"과 "Jog" state를 연결해주고 앞서 만들었던 Blend Space를 연결해주었다.
이제 기본적으로 Idle과 걷기 애니메이션이 동작한다.

 

✅ 대기 상태 구현

Idle 상태가 5초 동안 유지되면 대기 상태로 전환되고 그에 맞는 애니메이션 출력.
CharacterAnimInstance를 상속받는 플레이어 전용 WarriorHeroAnimInstance에 구현.

🔹WarriorHeroAnimInstance.h

#pragma once

#include "CoreMinimal.h"
#include "AnimIstances/CharacterAnimInstance.h"
#include "WarriorHeroAnimInstance.generated.h"

class AWarriorHeroCharacter;

UCLASS()
class RPG_API UWarriorHeroAnimInstance : public UCharacterAnimInstance
{
	GENERATED_BODY()
	
public:
	virtual void NativeInitializeAnimation() override;
	virtual void NativeThreadSafeUpdateAnimation(float DeltaSeconds) override;

protected:
	UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "AnimData|References")
	AWarriorHeroCharacter* OwningHeroCharacter;

	// 대기 상태인지 여부 
	UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "AnimData|LocomotionData")
	bool bShouldEnterRelaxState = false;

	// 대기 상태로 돌입하기 위한 시간
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "AnimData|LocomotionData")
	float EnterRelaxStateThreshold = 5.0f;

	// 대기 상태 지속 시간
	float IdleElpasedTime = 0.0f;
};

 

🔹WarriorHeroAnimInstance.cpp

#include "AnimIstances/Hero/WarriorHeroAnimInstance.h"
#include "Characters/WarriorHeroCharacter.h"
#include "WarriorDebugHelper.h"

void UWarriorHeroAnimInstance::NativeInitializeAnimation()
{
	Super::NativeInitializeAnimation();

	if (OwningCharacter)
	{
		OwningHeroCharacter = Cast<AWarriorHeroCharacter>(OwningCharacter);
	}
}

void UWarriorHeroAnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeThreadSafeUpdateAnimation(DeltaSeconds);

	if (bHasAcceleration)
	{
		IdleElpasedTime = 0.0f;
		bShouldEnterRelaxState = false;
	}
	else
	{
		IdleElpasedTime += DeltaSeconds;
		bShouldEnterRelaxState = (IdleElpasedTime >= EnterRelaxStateThreshold);
	}
	Debug::Print(FString::SanitizeFloat(IdleElpasedTime));
}

 

ABP에서 "Relax" state를 생성해 "Idle"에 연결시켜 주었다.
C++ 코드에서 Idle 상태가 5초 동안 지속되면 bShouldEnterRelaxState 변수가 true로 설정되고, 이에 따라 대기 상태 애니메이션이 재생되도록 설정하였다.
Random Sequence Player 노드를 통해 두 개의 대기 상태 애니메이션 중 하나가 재생되도록 설정하였다.

"Idle"상태로 5초가 지나면 "Relax" 상태로 전환되며, 주위를 둘러보는 애니메이션이 재생된다.