개요

언리얼 엔진 5의 GameFeature Plugin 시스템을 활용하면, 게임의 핵심 로직을 건드리지 않고도 새로운 컨텐츠를 런타임에 추가하거나 제거할 수 있다. 이 글에서는 Data Registry와 결합하여 완전히 모듈화된 상점 및 DLC 시스템을 구축한 사례를 다룬다.

1. 요약

핵심은 코어 게임 로직과 콘텐츠를 완전히 분리하고, Data Registry Source만으로 확장하는 것.


1. ItemShop 게임 피처 플러그인 (상점 시스템)

  • Add Data Registry : DR_ShopItems 레지스트리 생성 및 기본 아이템 데이터 등록
  • Add Widgets : 상점 UI 활성화 (커스텀 GameFeatureAction)
  • 프로젝트 코어에서 상점 시스템을 제거하고 플러그인으로 관리 가능

2. DLC_SpecialEdition 게임 피처 플러그인 (DLC 콘텐츠)

  • Add Data Registry Source : 기존 DR_ShopItems에 DLC 아이템 데이터 소스 추가
  • 추가 콘텐츠를 완전히 독립적으로 관리


2. 동작 흐름

1. 기본 상점

1. ItemShop 게임 피처 플러그인 활성화

  • DR_ShopItems 레지스트리 추가 (DT_Default_Consumable 데이터 테이블이 등록되있음)
  • Add Widgets 액션(커스텀 액션) 실행

2. 상점 UI 표시

  • UDataRegistrySubsystem 을 이용해 ShopItems 레지스트리 모든 아이템 조회
  • 조회된 아이템 데이터로 UI 초기화
  • 기본 소비 아이템들이 상점에 표시됨

2. DLC 추가

1. DLC_SpecialEdition 게임 피처 플러그인 활성화

  • 기존 ShopItems Data Registry에 Data Registry Source로 DT_DLC_Consumable 추가
  • 코어 시스템 수정 없이 순수하게 데이터만 추가

2. 자동으로 상점의 콘텐츠 확장

  • ShopItems 레지스트리가 자동으로 DLC 아이템 포함
  • 상점 UI 오픈 시 기본 아이템 + DLC 아이템 모두 표시
  • 런타임 소스 확인 시 데이터 테이블이 추가된 것을 확인 가능

3. 핵심 메커니즘

0. Add Data Registry Source 는 대상 레지스트리에 Meta Source 가 필요

Meta Source 가 필요한 이유 (언리얼 공식 문서 참고)

  • 런타임에 다른 데이터 소스를 생성하고 소유
  • 모든 데이터 소스에 규칙을 적용하여 해당되는 에셋을 찾습니다
  • Meta Source가 발견한 데이터 소스는 런타임에 데이터 레지스트리에 로드됨

먼저 데이터 레지스트리 소스를 등록하고 후에 레지스트리를 등록해도

Meta Source 가 찾아서 로드할 수 있기 때문에 문제가 없다!

DataTable 을 DataRegistry에 등록하기 위해 DataSource 로 생성해 런타임에 로드해줌

런타임에 등록할 데이터 소스들을 관리해주는 역할!


1. 게임 피처에 Data Registry 가 등록되는 타이밍은?

기본 GameFeatureAction_Add DataRegistry 은 Registered 단계에서 이미 게임에 등록된다.

그래서 Activate 단계에서 데이터에 접근해도 문제가 생기지 않는다.

Installed ->Registered → Loaded → Activated
                            →
                Data Registry 등록 완료

1-1. DLC 게임 피처는 Installed 상태로 시작하는 것이 좋음.

Registered 단계에서 데이터 레지스트리나 소스가 이미 엔진에 등록되어 검색과 데이터 접근이

가능한 상태이기 때문에, 원치 않게 DLC 콘텐츠가 바로 노출될 수 있음.


1-2. 게임 피처를 DeActivate 해도 데이터 레지스트리에 접근 가능한가?

Registered, Loaded 상태에서도 데이터 레지스트리가 검색과 접근이 가능하다고 했었다.

그럼 게임 피처를 비활성화해도 똑같다면, 비활성화하는 의미가 없는 것 아닐까?

아래 코드를 보자


메모리에는 남아 있지만, Deactivating 시점에 해당 레지스트리 경로를 검색하지 못하게 한다.

void UGameFeatureAction_DataRegistry::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
    Super::OnGameFeatureDeactivating(Context);

    if (FoundRegistry)  // false
{
    // 실행 안됨
}
  
return false  // 실패
  
UE_LOG(LogGameFeatures, Log, TEXT("failed to unregister"))
```

결과:

  • UnregisterSpecificAsset 실패
  • 로그 출력: "DT_DLC_Consumable failed to unregister"

구체적인 코드는 해당 언리얼 소스파일을 확인하면 된다.


하지만, 추가한 데이터 레지스트리 소스도 DeInitialize 가 되므로, 플레이에서 문제는 발생하지 않는다.

(단지 "사용 불가" 상태 마킹)

void UDataRegistrySource::Deinitialize()
{
    if (IsInitialized() && GetRegistry())
    {
       bIsInitialized = false;
    }
}


최종 결론 :

DLC 게임 피처가 Loaded 상태이기 때문에 DT_DLC_Consumable 은 메모리에 로드된 상태이며,

UDataRegistrySubsystem 을 통한 접근은 차단 되었다. (DR_ShopItems 를 통해 접근하지 못하는 상태가 된다.)

물론 DT_DLC_Consumable 은 직접 참조는 가능하다.

(DataRegistrySource 로써 DeInitialize 된 것뿐이다)



3. 비동기 아이템 로딩

void UBSItemShopSystem::LoadShopItems(FGameplayTagContainer FilterTags, 
    const FDataRegistryItemAcquiredCallback& InCallback)
{
    // 모든 아이템 ID 가져오기
    TArray<FDataRegistryId> AllItemIds;
    DRSubsystem->GetPossibleDataRegistryIdList(FName("ShopItems"), AllItemIds);

    // 비동기로 각 아이템 로드
    for (const FDataRegistryId& ItemId : AllItemIds)
    {
        DRSubsystem->AcquireItem(ItemId, 
            FDataRegistryItemAcquiredCallback::CreateLambda([...](...) {
                // 필터링 및 캐싱
                if (FilterTags.IsEmpty() || ItemData->ItemTag.HasAny(FilterTags))
                {
                    CachedItemIds.Add(AssetId);
                }
            })
        );
    }
}

4. UI 갱신 흐름

// 1. UI 생성 시 아이템 로드 요청
ShopSubSystem->LoadShopItems(FilterTags, Callback);

// 2. 모든 아이템 로드 완료 후 UI 구성
void UBSShopWidget::OnShopItemsLoaded(const FDataRegistryAcquireResult& Result)
{
    for (const auto& ItemData : ShopSubSystem->GetAllItems())
    {
        // ShopItemWidget 생성 및 추가
        ItemWidget->SetItemData(ItemData);
        ItemListScrollBox->AddChild(ItemWidget);
    }
}


4. 장단점

모듈형 게임 플레이로써의 장점

  • 완전히 선택적으로 기능을 넣을 수 있다.
  • 독립적으로 기능들을 개발할 수 있다.
  • 게임은 피처에 의존하지 않아, 홀로 실행이 가능하다.
현재
[Core Game] - 최소한의 기능만
[ItemShop Plugin] - 상점 전체 시스템
[DLC Plugin] - 현재는 데이터만 추가

협업에서의 장점

팀에서 개발하면서 팀원이 개발한 기능이 문제가 생겨 고쳐달라고 하는 요청하는 상황이 생기더라..

이런 문제들을 정말 쉽게 해결할 수 있다.

  • ItemShop 팀 완전 독립
  • Core 팀은 ItemShop 모름
  • ItemShop 없이도 게임 실행 됨.

테스트에서의 장점

  • Feature별 테스트 가능
  • 특정 Feature만 활성화하여 디버깅
  • QA 팀에서도 테스트가 용이


단점

  • 알아야 할 초기 학습 비용 크고, 구조 설계에 시간이 많이 든다…
    • 게임 피처부터 시작해서, 피처 액션, 데이터 레지스트리, 소스, 데이터 에셋 등등
    • 초기 학습 비용과 팀원들이 모두 활용할 수 있는 경우에는 정말 좋은 기능이라고 생각된다
    • 버전 관리에서도 문제를 일으킬 일이 크게 없을 가능성이 크다.
  • 생각보다 이쪽 관련 학습 문서가 없다
    • 처음부터 Lyra 를 분석하는 것이 제일 도움이 된다.
    • Lyra 가 단순히 기능 시연용을 목적으로 만든 게 아니고, 이렇게 해야 되는구나라는게 분석하면서 느껴진다.
  • 데이터 레지스트리 비활성화 문제
    • 위에서 설명했던 데이터 레지스트리 비활성화 순서 문제 같은 경우
    • 언리얼에서 이 쪽 문제를 이미 테스트하고, 정리를 잘하는 같긴 하다.


5. 후기

처음에는 "DLC DataTable 1개 추가하는 건데 이렇게까지 해야 하나?" 싶었다. GameFeature Plugin, Data Registry, Meta Source 는 뭐고 초기 설정도 복잡했다.

하지만 막상 구축하고 나니, 재미있었고 되게 효과적이었다. 코드 한 줄 없이 데이터를 추가하고 다시 빼는 것도 가능했다.

중~대규모 팀에서 장기적인 프로젝트에 DLC 까지 계획중이라면 안 쓸 이유가 없는 구조인 것 같다 초기 비용이 좀 들더라도 이전 프로젝트에서 활용했으면 좋았을텐데 하는 아쉬움이 남는다.

그리고 데이터 레지스트리 관련 문제를 좀 디깅하면서 이 쪽 코드가 참 복잡했다. 타고 타고 타고 들어가는 언리얼 디버깅...