복잡한 전투 시스템을 하드코딩하는 시대는 지났습니다. 언리얼 엔진의 강력한 Gameplay Ability System(GAS)과 Data Registry를 결합하여 확장 가능하고 유지보수가 용이한 인디 액션 RPG 전투 시스템을 설계하는 아키텍처를 제시합니다.
도입 및 개요
언리얼 엔진의 Gameplay Ability System(GAS)은 에픽게임즈가 포트나이트(Fortnite)나 파라곤(Paragon) 같은 게임을 개발하며 축적한 전투 시스템의 노하우가 집약된 궁극의 프레임워크입니다. 액션 RPG나 MOBA 장르의 게임을 개발하는 인디 팀이라면, 캐릭터의 체력, 마나, 스태미나 같은 속성(Attribute)을 관리하고 공격, 스킬 사용, 상태 이상(버프/디버프)을 처리하기 위해 자체적인 시스템을 구축하려다 스파게티 코드의 늪에 빠진 경험이 있을 것입니다. GAS는 이러한 복잡한 상호작용을 Ability(스킬), AttributeSet(능력치 세트), GameplayEffect(효과 및 상태 이상), GameplayTag(태그 기반 상태 관리)라는 네 가지 핵심 기둥을 통해 깔끔하게 분리하고 모듈화합니다. 여기에 대량의 데이터를 비동기적으로 효율적으로 관리하는 Data Registry를 결합하면, 인디 스튜디오라도 수십 개의 캐릭터와 수백 개의 스킬을 유연하게 추가하고 밸런싱할 수 있는 AAA급 데이터 주도형 아키텍처를 완성할 수 있습니다.
실무적인 적용 사례로, 플레이어가 '화염구' 스킬을 사용하여 적을 맞췄을 때 5초간 지속적인 화상 피해(DoT, Damage over Time)를 입히는 상황을 가정해 보겠습니다. 과거에는 이를 구현하기 위해 적 캐릭터 클래스 내부에 화상 상태를 체크하는 불리언(Boolean) 변수를 두고, Tick 함수 안에서 타이머를 돌리며 체력을 깎는 복잡한 로직을 작성했습니다. 이는 새로운 디버프가 추가될 때마다 캐릭터 코드를 수정해야 하는 끔찍한 유지보수 지옥을 낳았습니다. 하지만 GAS를 사용하면, 캐릭터 코드는 전혀 수정할 필요가 없습니다. '화상 효과'를 정의하는 GameplayEffect 블루프린트를 생성하고, Duration을 5초로 설정한 뒤 1초마다 체력 Attribute를 -10씩 깎도록 Modifier를 추가하면 끝입니다. 화염구 Ability는 적에게 적중했을 때 이 GameplayEffect를 타겟의 AbilitySystemComponent에 적용(ApplyGameplayEffectToTarget)하기만 하면 됩니다. 여기에 'State.Debuff.Burn'이라는 GameplayTag를 부여하면, 다른 스킬들이 이 태그를 읽어 화상 상태인 적에게 추가 피해를 입히는 식의 복잡한 콤보 시스템도 태그 판정 하나로 쉽게 구현할 수 있습니다.
이러한 GAS의 진가는 Data Registry와 결합될 때 더욱 빛을 발합니다. 레벨 디자인이나 밸런싱 과정에서 스킬의 데미지 수치, 마나 소모량, 쿨다운 등을 수정하기 위해 블루프린트를 열고 하드코딩된 값을 변경하는 것은 매우 비효율적입니다. Data Registry는 엑셀이나 구글 스프레드시트에서 작성한 CSV 데이터를 JSON이나 언리얼 데이터 테이블로 임포트한 뒤, 게임 런타임에 메모리 부하 없이 필요한 데이터만 비동기적으로 로드하여 사용할 수 있게 해주는 시스템입니다. 예를 들어, 무기 데이터베이스나 스킬 데미지 테이블을 Data Registry로 구축합니다. 그리고 GameplayEffect에서 데미지 수치를 직접 입력하는 대신, Data Registry에서 'Skill_Fireball_Lv1'의 데미지 값을 동적으로 읽어오도록(CurveTable 기반 또는 Set By Caller 기법 활용) 설정합니다.
이러한 데이터 주도적 설계(Data-Driven Design)가 가져다주는 이점은 명확합니다. 기획자는 프로그래머의 도움 없이 엑셀 파일의 수치만 수정하여 전체 게임의 밸런스를 테스트할 수 있습니다. 한 인디 로그라이크 액션 게임 팀의 경우, 약 150종의 아이템과 50종의 몬스터 스킬 밸런스를 맞추는 과정에서 Data Registry와 GAS 시스템 덕분에 밸런스 패치 주기를 하루 단위로 단축시킬 수 있었다고 합니다. 아이템의 등급에 따른 능력치 스케일링, 복잡한 조건부 능력치 상승(예: 밤 시간에 치명타 확률 20% 증가) 등의 기획적 요구사항을 GameplayEffect의 Magnitude Calculation 클래스와 Data Registry 매핑으로 분리하여 구현함으로써, 코드의 결합도를 획기적으로 낮췄습니다. 결론적으로, GAS와 Data Registry의 통합은 개발 초기에 셋업 시간이 소요되지만, 프로젝트가 확장될수록 인디 개발팀의 시간과 비용을 절약해주는 황금빛 아키텍처라 할 수 있습니다.
// AttributeSet에서 특정 데미지가 들어올 때 처리하는 기본적인 로직 예시
void UCustomAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if (Data.EvaluatedData.Attribute == GetDamageAttribute())
{
// 입은 데미지 캐싱
const float LocalDamageDone = GetDamage();
SetDamage(0.f); // 데미지 속성은 처리가 끝나면 0으로 초기화
if (LocalDamageDone > 0.0f)
{
// 실제 체력(Health)에서 데미지 차감
const float NewHealth = GetHealth() - LocalDamageDone;
SetHealth(FMath::Clamp(NewHealth, 0.0f, GetMaxHealth()));
if (GetHealth() <= 0.0f)
{
// 캐릭터 사망 처리 로직 호출 (예: 태그 추가 등)
FGameplayEventData EventData;
EventData.EventTag = FGameplayTag::RequestGameplayTag(FName("Event.Character.Death"));
Data.Target.HandleGameplayEvent(EventData.EventTag, &EventData);
}
}
}
}