작업 개요
스킬별 기본 수치를 코드에 하드코딩하지 않고 DataTable로 외부화하는 기반 구조를 추가했다. FNSAbilityBaseStatRow와 UNSCombatStatComponent를 새로 추가해, Ability에서 AbilityTag + StatTag 기준으로 수치를 조회할 수 있는 흐름을 만들었다.
이번 작업의 최종 검증 대상은 GA_RangerProjectileShot의 ExplosionRadius였다. DataTable 값 변경이 실제 Projectile 스플래시 반경에 반영되는 것을 로그로 확인했다.
구현 범위
FNSAbilityBaseStatRow 구조
스킬별 기본 수치를 Row 단위로 관리하기 위해 FTableRowBase를 상속하는 구조체를 추가했다. AbilityTag와 StatTag 조합이 캐시 키가 된다.
USTRUCT(BlueprintType) struct FNSAbilityBaseStatRow : public FTableRowBase { GENERATED_BODY() UPROPERTY(EditAnywhere) FGameplayTag AbilityTag; // ex) Ability.Ranger.ProjectileShot UPROPERTY(EditAnywhere) FGameplayTag StatTag; // ex) CombatStat.ExplosionRadius UPROPERTY(EditAnywhere) float BaseValue = 0.f; UPROPERTY(EditAnywhere) bool bModifiable = true; // 증강 / 파츠로 변경 가능 여부 };
▲ 스킬 수치 데이터 계층 구조 — AbilityBaseStats DataTable이 이번 작업의 대상
AbilityBaseStats DataTable 구성
Row Name을 AbilityTag__StatTag 조합으로 짓는 규칙을 사용했다. 에디터에서 어떤 스킬의 어떤 수치인지 한눈에 파악할 수 있다.
💡 Effect.Damage.Base 분리 이유
CombatStat.Damage는 DataTable/증강으로 계산되는 스킬 공격력 스탯. Effect.Damage.Base는 GE에 SetByCaller로 전달하는 피해 입력값. 두 개념이 다르므로 태그도 분리했다.
bModifiable 컬럼으로 증강/파츠 변경 가능 여부를 DataTable에서 함께 관리한다. CombatStat.ProjectileSpeed는 false로 설정해 게임플레이 중 변경되지 않도록 했다.
UNSCombatStatComponent 캐싱 구조
컴포넌트 초기화 시 DataTable을 한 번만 읽어 TMap 캐시에 저장한다. 이후 Ability 조회는 캐시만 참조하므로 DataTable 접근 비용이 없다.
// 초기화 시 DataTable → 캐시 빌드 void UNSCombatStatComponent::BuildCache() { if (!IsValid(AbilityBaseStatsTable)) { return; } TArray<FNSAbilityBaseStatRow*> Rows; AbilityBaseStatsTable->GetAllRows(TEXT(""), Rows); for (const FNSAbilityBaseStatRow* Row : Rows) { if (!Row->AbilityTag.IsValid() || !Row->StatTag.IsValid()) { continue; } StatCache.Add({Row->AbilityTag, Row->StatTag}, Row->BaseValue); ModifiableCache.Add({Row->AbilityTag, Row->StatTag}, Row->bModifiable); } } // 기본 수치 조회 bool UNSCombatStatComponent::TryGetBaseAbilityStat( const FGameplayTag& AbilityTag, const FGameplayTag& StatTag, float& OutValue) const { if (const float* Found = StatCache.Find({AbilityTag, StatTag})) { OutValue = *Found; return true; } return false; }
PlayerState / ASC / SkillBase 조회 흐름
개별 Ability가 PlayerState를 직접 캐스팅하지 않도록 ASC 래퍼와 GA_SkillBase 편의 함수 계층을 추가했다.
▲ Ability → ASC 래퍼 → PlayerState → CombatStatComponent 조회 흐름
bool UNSAbilitySystemComponent::TryGetBaseAbilityStat( const FGameplayTag& AbilityTag, const FGameplayTag& StatTag, float& OutValue) const { const auto* PS = GetOwnerPlayerState<ANSPlayerState>(); if (!IsValid(PS)) { return false; } return PS->GetCombatStatComponent()->TryGetBaseAbilityStat(AbilityTag, StatTag, OutValue); }
// 실패 시 DefaultValue 반환 float UGA_SkillBase::GetBaseAbilityStatOrDefault( const FGameplayTag& StatTag, float DefaultValue) const { float OutValue = DefaultValue; if (const auto* ASC = GetNSAbilitySystemComponent()) { ASC->TryGetBaseAbilityStat(GetAbilityTag(), StatTag, OutValue); } return OutValue; }
ProjectileShot ExplosionRadius 연동
// DataTable 조회 실패 시 DefaultExplosionRadius fallback 사용 const float ExplosionRadius = GetBaseAbilityStatOrDefault( TAG_CombatStat_ExplosionRadius, DefaultExplosionRadius); UE_LOG(LogNS, Log, TEXT("[ProjectileShot] ExplosionRadius: %.1f"), ExplosionRadius); Projectile->InitProjectile(ExplosionRadius);
✅ 로그 검증 결과
DataTable의 ExplosionRadius 값을 400 → 800으로 변경했을 때, 다음 실행에서 로그가 [ProjectileShot] ExplosionRadius: 800.0으로 바뀌는 것을 확인했다.
증강 시스템 역할 분리 설계
기존 StackEffectClass GE와 새로 설계 중인 CombatStatModifier의 역할을 명확히 분리하기로 했다. 같은 수치를 양쪽에 넣으면 중복 적용 위험이 있다.
- MaxHealth
- Defense
- MoveSpeed
- 전역 Attribute 변화
- ProjectileShot Damage
- ProjectileShot ExplosionRadius
- Cooldown / FireRate
- MaxAmmo
- 스킬별 수치 변화
⚠ 후속 작업 예정
Augment Modifier Row / DataTable 구조를 추가하고, AugmentInventory의 보유 증강 + 스택 정보 기준으로 최종값을 계산하는 흐름을 연동한다.
Damage 구조 후속 설계 정리
Damage 리팩터링은 이번 작업에서 진행하지 않고 후속으로 분리했다.
▲ Damage 처리 흐름 — 이번 작업에서는 구현 없이 방향만 정리
테스트
- 테스트 GameMode에서 PlayerState를 BP PlayerState로 변경
- BP PlayerState의 CombatStatComponent에 AbilityBaseStats DataTable 연결
- ProjectileShot ExplosionRadius가 DataTable 값에 따라 변경되는지 확인
- ExplosionRadius 400 → 800 변경 시 로그 반영 확인
- 인게임 GameMode fallback 값 사용 상태 확인 (공통 PlayerState 미설정)
⚠ 미완료
인게임 GameMode의 PlayerStateClass 공통 설정이 아직 적용되지 않았다. 몬스터 대상 테스트도 레벨 / GameMode 파일 Lock 상황으로 보류했다.
오늘 배운 점
핵심 성과
스킬 수치를 코드에서 분리해 DataTable로 외부화하는 기반 구조가 만들어졌다. 이제 디자이너나 팀원이 코드 수정 없이 에디터에서 수치를 조정할 수 있다.
- AbilityTag + StatTag 기준 캐시 구조로 런타임 DataTable 접근 비용 없음
- Ability가 PlayerState를 직접 캐스팅하지 않도록 ASC 래퍼 계층 분리
TryGet(실패 직접 처리)과OrDefault(fallback 사용) 역할 분리- StackEffectClass GE와 CombatStatModifier 역할을 명확히 구분
- Damage 흐름 설계 방향 정리 (CombatStat → SetByCaller → Meta → Health)
이슈 / 확인된 문제
- 인게임 GameMode / PlayerState 공통 설정이 팀원 작업과 맞물려 있어 후속 정리 필요
- 증강 Modifier 반영, Damage SetByCaller, Cooldown SetByCaller, MaxAmmo 연동 미구현
- 팀 / 아군 판정, 파츠, 영구 강화와의 통합은 후속 시스템 정리 이후 진행
다음 작업
- Augment Modifier Row / DataTable 구조 추가
- AugmentInventory Owned / Stacks 이벤트와 CombatStatComponent 연동
- 증강으로 ProjectileShot ExplosionRadius 증가 흐름 검증
- Damage SetByCaller + Damage Meta Attribute 구조 리팩터링
- Cooldown SetByCaller 기반 계산 정리
- AutoFire MaxAmmo / CurrentAmmo Attribute 연동 검토
- 인게임 GameMode PlayerStateClass 공통 설정 정리
'프로그래밍 > 개발일지' 카테고리의 다른 글
| [TIL] 연발사격 처리 아키텍쳐 결정 (0) | 2026.05.29 |
|---|---|
| [TIL] Listen Server 기반 프로토타입 빌드 패키징 정리 (0) | 2026.05.27 |
| [TIL] Unreal 프로젝트 에셋 관리를 Google Drive + rclone + bat 파일로 분리하기 (2) | 2026.05.21 |
| [UnrealEngine] 멀티플레이어 디펜스 `SagoMagic` 프로젝트 KPT 회고 (0) | 2026.04.24 |
| [개발전반] Ureal Engine 5를 이용해 한 달동안 진행했던 팀프로젝트 TPS게임 개발 KRP (0) | 2026.03.05 |