BulletAnt 개발일지 (2) - PrimaryDataAsset, StateTree+GAS

김펭귄·2026년 5월 13일

Today What I Learned (TIL)

목록 보기
117/139

PrimaryDataAsset 기반의 리소스 관리

나중에 AssetManager를 통해 에셋을 동적으로 로드하거나 특정 번들로 묶어 최적화할 수 있도록 일반 DataAsset이 아닌 PrimaryDataAsset을 상속받아 구현했습니다.

공유 메모리 참조 (Shared Reference)

  • 디폴트 어빌리티(Default Ability), 애님 몽타주와 같이 중간에 변하지 않는 초기값들은 모든 객체가 하나의 데이터 에셋을 직접 참고하게 했습니다.

  • 같은 클래스의 여러 객체가 하나의 메모리 주소를 공유하므로, 수백 마리의 적이 소환되어도 불필요한 메모리 낭비가 발생하지 않습니다.

가변 데이터 복사 (Value Copy)

  • 체력(HP), 공격력, 이동 속도 등 게임 플레이 중 실시간으로 값이 바뀌어야 하는 항목들은 에셋으로부터 초기값을 복사받아 개별 객체가 소유하도록 설계했습니다.

설계 시 고려한 장점

  • 메모리 최적화: 모든 객체가 동일한 데이터를 각각 들고 있지 않고 하나의 원본(Source)을 바라보는 형식을 취해 메모리 사용량을 최소화했습니다.

  • 유지보수 효율: 적의 외형이나 공통 스탯을 수정할 때, 개별 블루프린트를 일일이 건드리지 않고 데이터 에셋 하나만 수정하면 모든 인스턴스에 즉시 반영됩니다.

  • 확장성: PrimaryDataAsset 구조를 채택함으로써, 향후 대규모 최적화 작업 시 자산 로드/언로드 관리를 체계적으로 수행할 수 있는 기반을 마련했습니다.

UCLASS()
class BULLETANT_API UBaseEnemyDataAsset : public UPrimaryDataAsset
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, Category = "GAS|Abilities")
	TArray<TSubclassOf<UGameplayAbility>> DefaultAbilities;

	UPROPERTY(EditDefaultsOnly, Category = "BaseStat")
	int32 Health = 100.f;
    
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Death")
	TSubclassOf<UGameplayEffect> DeathEffect;

	UPROPERTY(EditDefaultsOnly, Category = "Death")
	FGameplayTag DeathStateTag;

	UPROPERTY(EditDefaultsOnly, Category = "Death")
	TObjectPtr<UAnimMontage> DieAnimMontage;
    
    // ... //
};

StateTree와 GAS의 역할 분담

설계 배경

AI 시스템을 구성하면서 가장 먼저 정리하려 했던 부분은
“상태(State)를 어느 시스템이 관리할 것인가”였다.

초기에는 GAS의 GameplayTag를 기반으로 상태를 전환하는 구조도 고려했다.
하지만 이 방식은 상태 변경의 흐름이 Tag 변화에 의존하게 되면서,
AI의 전체 흐름을 추적하기 어려워지는 문제가 있었다.

특히 아래와 같은 부분이 불편했다.

  • 상태 전환 위치가 여러 곳으로 분산됨
  • 현재 상태가 변경된 이유를 추적하기 어려움
  • AI의 의사결정 흐름이 명확하게 보이지 않음

결국 상태 전환의 책임을 하나의 시스템으로 통일할 필요가 있다고 판단했다.

역할 분리 방향

최종적으로는 아래와 같이 역할을 분리했다.

시스템역할
StateTree상태 판단 및 상태 전환
Task상태 내부 로직 수행
GAS애니메이션 / 사운드 / 이펙트 실행

핵심은:

“AI의 상태 결정은 StateTree가 담당한다”

는 구조였다.

상태 전환 구조

상태 전환은 모두 StateTree 내부에서 처리하도록 구성했다.

예를 들어 공격 상태로 전환해야 하는 경우:

  1. StateTree에서 조건 판단
  2. Task 내부에서 전환 로직 수행
  3. StateTree가 직접 다음 State로 전환

이 흐름으로 통일했다.

이를 통해 상태 변경 책임이 외부 시스템으로 분산되지 않도록 했다.

GAS의 역할 제한

GAS는 상태를 관리하지 않는다.

대신 실제 Gameplay 실행만 담당하도록 역할을 제한했다.

  • GameplayEffect 적용
  • GameplayTag 부여
  • GameplayAbility 실행
  • 애니메이션 재생
  • 사운드 재생
  • 이펙트 출력

즉:

StateTree가 “무엇을 할지” 결정하고,
GAS가 “실제 행동”을 수행하는 구조다.

결과

구조를 분리한 이후 가장 크게 개선된 부분은 디버깅이었다.

이전에는:

  • Tag 변경 때문인지
  • Ability 종료 때문인지
  • 외부 이벤트 때문인지

상태 변경 원인을 추적하기 어려웠다.

반면 현재 구조에서는:

“상태 변화는 StateTree에서만 발생한다”

는 기준이 생기면서
AI 흐름을 훨씬 쉽게 추적할 수 있게 되었다.

또한 AI 로직 자체도 읽기 쉬워졌고,
추후 상태가 추가되더라도 구조가 쉽게 무너지지 않도록 만들 수 있었다.

profile
반갑습니다

0개의 댓글