GameFeature Plugin, ISS, Data Registry 활용
언리얼의 Lyra 샘플 프로젝트를 분석하여, GameFeature Plugin, Initialize State System(ISS), Data Registry를 활용한 모듈형 게임 플레이 프레임워크를 구축했습니다.
이 프로젝트의 목표는 플러그인 단위로 개발해 게임의 코어 로직과 콘텐츠를 완전히 분리하고, 협업에서 발생할 수 있는 문제들을 해결하여 생산성을 높이는 것입니다.
추가적으로, GameFeature와 DataRegistry를 활용해 DLC 콘텐츠를 쉽게 추가할 수 있는 확장성을 확보했습니다.
| 문제 유형 | 상세 내용 | 해결책 |
|---|---|---|
| 팀내 코드 충돌 | 코어 모듈 직접 수정으로 인한 Merge Conflict 발생 | GameFeature Plugin: 각 기능을 독립된 플러그인으로 분리하여 코어 모듈 수정 불필요 |
| 의존 객체 준비 시점 불명확 | Tick/Timer 반복 체크 등 비효율적인 타이밍 관리 | Initialize State System (ISS): 로 의존성 명시, 필요한 객체가 준비되면 자동으로 초기화 |
| 초기화 로직 분산 | 초기화 코드가 여러 클래스에 흩어져 있어 흐름 파악 곤란 | Initialize State System (ISS): 각 컴포넌트의 초기화 로직이 해당 클래스 내부에만 존재, 다른 클래스에서 Init 함수 호출 X |
| 초기화 흐름 파악 곤란 | 시스템 전체 초기화 과정 추적 어려움 | 중앙 관리자 (PawnStateManager): 전체 초기화 흐름을 한 곳에서 파악, 디버깅 용이 |
게임 피처 플러그인은
하나의 기능을 독립적인 부품으로 분리하여 관리하고,
런타임에 추가/제거 가능한 모듈 시스템 입니다.
팀원 간 코드 충돌을 줄이고, 모듈형 게임 플레이 개발의 핵심입니다.
기존 게임에서는 코어 모듈에 기능들을 모두 포함하고, 콘텐츠만 따로 분리시켰다면 기능, 콘텐츠 모두 독립적으로 개발할 수 있습니다.
게임은 피처에 의존 되지 않아, 플러그 앤 플레이 방식이 가능합니다.
각 기능들을 독립적인 GameFeature Plugin 으로 개발했습니다.
게임 피처들을 필요할 때, 로드하며 레고 블록처럼 기능을 조립하는 방식입니다.
GameFeature 들을 묶고, 폰을 변경할 수 있는 Character Definition 이라는 PrimaryAsset 을 구현해
새로운 캐릭터 추가 시 기존 코드 수정 없이 해당 PrimaryAsset 만 생성하면 되어 확장성을 높였습니다.
예시: 스파이더맨 (Pawn - BP_Spidy / GameFeature - WebSwing, MovementStandard)
Lyra는 GameFeature를 하나의 장르(예: ShooterCore)로서 접근하여 여러 시스템(카메라, 무기 등)을 포함했지만,
범위가 넓으므로 여러 명의 팀원이 같이 개발할 가능성이 높아 보였습니다.
그러므로 팀원 간 코드 충돌 문제를 해결하기 어려울 수 있다고 판단했고, 다음과 같이 개선했습니다.
GameFeature_ItemShop, GameFeature_WebSwing)
ISS는 초기화에 필요한 의존성을 명확화하고, 자동으로 초기화하여,
기존의 수동적인 초기화 방식을 피하고 타이밍 이슈를 제거합니다.
// ===== 기존 방식 (수동 초기화) ===== void AMyCharacter::BeginPlay() { Super::BeginPlay(); AbilitySystemComponent->Initialize(); // 먼저 초기화되어야 함 HealthComponent->Initialize(); // ASC에 의존하므로 ASC 이후에 초기화 InputComponent->Initialize(); InventoryComponent->Initialize(); } // ===== ISS 적용 후 (각 컴포넌트 내부에서 자동 초기화) ===== void ABSCharacter::BeginPlay() { Super::BeginPlay(); }
void UBSHealthComponent::HandleChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) const { if (DesiredState == BSGamePlayTags::InitState_DataInitialized) { const auto PS = Cast<APawn>(GetOwningActor())->GetPlayerState(); if (!PS) return false; // DataInitialized 상태가 되려면 PlayerState, ASC가 필요해 if (Manager->HasFeatureReachedInitState( PS, ABSPlayerState::NAME_PLAYERSTATE, BSGamePlayTags::InitState_DataInitialized) && Manager->HasFeatureReachedInitState( PS, UBSAbilitySystemComponent::NAME_ABILITYSYSTEMCOMPONENT, BSGamePlayTags::InitState_DataInitialized)) { return true; } return false; } return true; } bool UBSHealthComponent::CanChangeInitState(UGameFrameworkComponentManager* Manager, FGameplayTag CurrentState, FGameplayTag DesiredState) { if (DesiredState == BSGamePlayTags::InitState_DataInitialized) { const auto BSCharacter = Cast<ABSCharacter>(GetOwningActor()); if (BSCharacter) { // DataInitialized 상태가 될 때, 초기화 InitializeWithAbilitySystem(BSCharacter->GetBSAbilitySystemComponent()); } } }
CanChangeInitState() 함수로 의존 관계 표현Lyra 는 컴포넌트들 중 일부만 ISS 를 적용했고, LyraHealthComponent의 Initialize 는 수동으로 호출하고 있습니다.
이는 일관성을 유지하기 힘들고, 후에 코드를 유지보수에도 영향을 끼칠 수 있다고 판단했습니다.
이어서 컴포넌트들의 초기화 상태가 어떻게 진행되고 있는지 파악하기 힘들었습니다.
따라서, 현재 프로젝트에는 다음과 같이 개선했습니다.
Data Registry를 활용하여 게임의 정적 데이터를 관리하고, 추가적인 코드 작성 없이
GameFeatureAction 으로 Data Registry Source를 추가하여 코어 시스템 수정 없이 DLC 콘텐츠를 확장했습니다.
DR_ShopItems 레지스트리 추가 및 기본 데이터 테이블(DT_Default_Consumable) 등록.UDataRegistrySubsystem을 이용해 ShopItems 레지스트리의 모든 아이템 조회 및 UI 초기화.