반응형
🎮 구현 목표
무기마다 다른 전투 경험을 제공하되, 코드 수정 없이 새로운 무기를 추가할 수 있는 확장 가능한 구조를 구축하고자 했습니다.
특히 무기마다 다른 콤보 체계, 스킬 구성, 공격력, 요구 스태미나 등을 데이터로 분리하여, 언리얼 에디터에서 수정할 수 있는 환경을 목표로 했습니다.
🚨 문제 상황
하드코딩의 한계: 무기 추가가 곧 코드 수정
초기에는 무기 데이터를 캐릭터 클래스에 직접 하드코딩했습니다.
// 초기 구조 - 모든 무기 정보를 캐릭터가 보유
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)
🔧 구현
정말 필요한 데이터만 남기기
무기를 정의하기 위해 최소한으로 필요한 데이터만 추려내는 데 집중했습니다.
처음엔 "혹시 필요할까봐" 이것저것 넣다가, 실제 사용하지 않는 데이터가 쌓이는 걸 발견했습니다. 여러 시행착오 끝에 정말 사용하는 다섯 가지로 압축했습니다:
- 공격/동작 타입 (AttackTypeTag)
- 몽타주 배열 (Animations)
- 실행 조건 태그 (ConditionTags)
- 실행 조건 검사 범위 (ConditionCheckDistance)
- 루트모션 스케일 (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 기반 통합 로직 |
반응형
'프로젝트 회고' 카테고리의 다른 글
| [UE5 팀 프로젝트] 지연 초기화: 런타임 액터 스폰, BeginPlay 호출 전 DataTable 데이터 무결성 확보 (0) | 2025.12.02 |
|---|---|
| [UE5 액션] 델리게이트(Delegate)를 활용한 유연한 인벤토리 시스템 (0) | 2025.12.02 |
| [UE5 액션] 루트 모션 기반 자연스러운 대시 구현 Motion Warping (0) | 2025.12.02 |
| [UE5 액션] 유연한 콤보 시스템: 이벤트 기반 Perfect/Mercy 구간을 통한 공격 후딜레이 캔슬 및 콤보 연계 (0) | 2025.12.02 |
| [DirectX 11] 다중 좌표계 간 위치 동기화 (0) | 2025.12.02 |
