프로젝트 회고 / / 2025. 12. 2. 23:08

[UE5 액션] 데이터 에셋 기반 무기별 전투 스타일 관리

반응형

🎮 구현 목표

무기마다 다른 전투 경험을 제공하되, 코드 수정 없이 새로운 무기를 추가할 수 있는 확장 가능한 구조를 구축하고자 했습니다.
특히 무기마다 다른 콤보 체계, 스킬 구성, 공격력, 요구 스태미나 등을 데이터로 분리하여, 언리얼 에디터에서 수정할 수 있는 환경을 목표로 했습니다.

 

🚨 문제 상황

 

하드코딩의 한계: 무기 추가가 곧 코드 수정

 

초기에는 무기 데이터를 캐릭터 클래스에 직접 하드코딩했습니다.

// 초기 구조 - 모든 무기 정보를 캐릭터가 보유
void ACharacter::Attack()
{
    if (CurrentWeapon == EWeaponType::Sword)
    {
        PlayAnimMontage(SwordCombo[ComboIndex]);
    }
    else if (CurrentWeapon == EWeaponType::Axe)
    {
        PlayAnimMontage(AxeCombo[ComboIndex]);
    }
    // 무기가 추가될 때마다 else if 증가...
}

 

무기 추가 시 개발 사이클 증가

 

 

새 무기 추가 프로세스:
1. C++ 코드에 무기 타입 Enum 추가
2. 애니메이션 배열 선언
3. 조건문 분기 추가
4. C++ 컴파일
5. 에디터 재시작
6. 테스트 → 수정 시 1번부터 반복

 

무기 간 차별화된 로직 구현 어려움

  • 검은 빠른 3타 콤보, 도끼는 느린 2타 강공격 같은 차이를 조건문으로만 구현
  • 코드가 무기별 특수 케이스로 가득 차서 가독성 저하

기획자 의존도 증가

  • 밸런싱, 콤보 수 조정 같은 단순 작업도 프로그래머 필요

 

이와 관련한 작업을 C++ 코드가 아닌 에디터에서 유연하게 처리하고 싶었습니다.

 

 

💭 해결 방안 고민

 

"무기를 데이터로 분리하되, 단순 수치뿐만 아니라 행동까지 정의할 수는 없을까?"

 

Data Asset

  • Blueprint에서 무기별 고유 로직 구현 가능
  • 애니메이션, 수치, 조건을 하나의 Asset에 캡슐화
  • Primary Data Asset 사용 시 비동기 로딩으로 메모리 최적화

고려사항:

무기 = 데이터 집합체
- 무슨 공격인가? (AttackTypeTag)
- 어떤 애니메이션을 재생하는가? (Montages)
- 언제 재생하는가? (ConditionTags)

 

 

🔧 구현

정말 필요한 데이터만 남기기

무기를 정의하기 위해 최소한으로 필요한 데이터만 추려내는 데 집중했습니다.

 

처음엔 "혹시 필요할까봐" 이것저것 넣다가, 실제 사용하지 않는 데이터가 쌓이는 걸 발견했습니다. 여러 시행착오 끝에 정말 사용하는 다섯 가지로 압축했습니다:

  1. 공격/동작 타입 (AttackTypeTag)
  2. 몽타주 배열 (Animations)
  3. 실행 조건 태그 (ConditionTags)
  4. 실행 조건 검사 범위 (ConditionCheckDistance)
  5. 루트모션 스케일 (RootMotionScales)
USTRUCT(BlueprintType)
struct FMontageGroup
{
    GENERATED_BODY()
public:
    // 이 그룹이 재생되기 위한 조건 태그
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<FGameplayTagContainer> ConditionTags;

    // 조건 체크 거리(예: 잡기 검사 최대 거리)
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float ConditionCheckDistance = 300.f;

    // 무기마다 다른 이동 거리 보정
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<float> RootMotionScales;

    // 실제 재생할 애니메이션 배열
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<TObjectPtr<UAnimMontage>> Animations;
};
UCLASS()
class SOUL_API UMontageActionData : public UPrimaryDataAsset
{
    GENERATED_BODY()

public:
    // Tag와 인덱스로 특정 몽타주 가져오기
    UAnimMontage* GetMontageForTag(const FGameplayTag& GroupTag, const int32 Index = 0) const;

    // 그룹 내 랜덤 선택 (몬스터)
    UAnimMontage* GetRandomMontageForTag(const FGameplayTag& GroupTag) const;

protected:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayName = "Montage Groups"))
    TMap<FGameplayTag, FMontageGroup> MontageGroups;
};

 

데이터 소유권 고민: 누가 이 데이터를 가져야 하나?

 

처음엔 "캐릭터가 데이터를 가지고, 무기 교체 시 참조만 바꾸면 되지 않을까?"라고 생각했습니다.

하지만 다음 이유로 무기가 데이터를 소유하도록 결정했습니다:

  • ✅ 같은 종류의 무기는 동일한 전투 스타일 (한손검끼리, 폴암끼리 일관성)
  • ✅ 무기의 정보는 무기 자체가 가지고 있는 게 객체지향 원칙에 부합
  • ✅ 새 무기 추가 시 무기 Blueprint만 만들면 끝 (캐릭터 수정 불필요)

 

무기 클래스에 Data Asset 적용:

// 무기 header
class SOUL_API ASoulWeapon : public ASoulEquipment
{
protected:
    UPROPERTY(EditDefaultsOnly, Category = "Setting|Animation")
    TObjectPtr<UMontageActionData> MontageActionData;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Setting")
    ECombatType CombatType = ECombatType::SwordShield;
}

 

무기 장착 시 전투 모드 자동 전환

 

무기를 장착하면 AnimInstance의 전투 자세가 자동으로 변경됩니다.

void ASoulWeapon::EquipItem(int32 SlotIndex)
{
    if (UCombatComponent* CombatComp = CharacterBase->GetCombatComponent())
    {
        // 장착한 무기의 CombatType으로 AnimInstance 업데이트
        // → Idle/Walk/Run 애니메이션이 무기 타입에 맞게 자동 전환
        if (USoulAnimInstance* AnimInstance = Cast<USoulAnimInstance>(CharacterBase->GetMesh()->GetAnimInstance()))
        {
            AnimInstance->UpdateCombatMode(CombatType);
        }
    }
}

 

공격 실행: 무기에서 데이터 조회

 

공격 시점에 현재 무기에서 적절한 몽타주를 가져와 재생합니다.

void ASoulCharacterBase::DoAttack(const FGameplayTag& AttackTypeTag)
{
   // 1. 현재 장착한 무기에서 Tag + ComboCounter로 몽타주 검색
    UAnimMontage* Montage = Weapon->GetMontageForTag(NewAttackTypeTag, ComboCounter);

    // 2. 콤보 끝에 도달했다면 (더 이상 몽타주가 없음)
    if (false == IsValid(Montage)) {/*...*/}

    // 3. 몽타주 재생
    AnimInstance->Montage_Play(Montage);
    AnimInstance->Montage_SetEndDelegate(OnMontageEnded, Montage);
}

 

 

데이터 흐름:

무기 장착
  ↓
Data Asset 로드 + CombatType 전달
  ↓
AnimInstance 전투 모드 변경
  ↓
공격 입력 (Tag: Character.Attack.Light)
  ↓
무기의 Data Asset에서 Tag + Index로 몽타주 검색
  ↓
애니메이션 재생

 

 

✅ 결과

 

코드 수정 없이 확장 가능한 무기 시스템 완성

  • 새 무기 추가: Data Asset 생성 + 애니메이션 할당만으로 완료
  • 밸런싱: 에디터에서 수치 조정 → 즉시 테스트 (컴파일 불필요)
  • 각 무기가 Data Asset을 통해 독립적으로 정의
  • 무기 타입 추가 시 코드 수정 0줄

Before vs After:

항목 하드코딩 Data Asset 기반
무기 추가 시간 C++ 수정 + 컴파일 Data Asset 생성
밸런스 조정 프로그래머 필요 에디터에서 즉시 가능
콤보 체계 변경 조건문 수정 Asset에서 배열 조정
빌드 크기 모든 무기 포함 필요 시 로딩
코드 복잡도 무기마다 분기문 Tag 기반 통합 로직
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유