모듈형 게임 플레이 프레임워크 구축

GameFeature Plugin, ISS, Data Registry 활용

Role
프레임워크 설계 및 개발
Engine
Unreal Engine 5
Duration
2개월
Focus
확장성, 협업 용이성

프로젝트 개요 및 목표

언리얼의 Lyra 샘플 프로젝트를 분석하여, GameFeature Plugin, Initialize State System(ISS), Data Registry를 활용한 모듈형 게임 플레이 프레임워크를 구축했습니다.

이 프로젝트의 목표는 플러그인 단위로 개발해 게임의 코어 로직과 콘텐츠를 완전히 분리하고, 협업에서 발생할 수 있는 문제들을 해결하여 생산성을 높이는 것입니다.

추가적으로, GameFeature와 DataRegistry를 활용해 DLC 콘텐츠를 쉽게 추가할 수 있는 확장성을 확보했습니다.

결과

협업 문제와 해결책

협업에서 발생할 수 있는 문제

  • 팀원 간 코드 충돌 문제:
    새로운 콘텐츠 개발·수정 시 코어 모듈(Character, GameMode 등)을 직접 수정
    Merge Conflict가 지속적으로 발생.
  • 초기화 시 의존 객체 준비 시점 불명확 (타이밍 이슈):
    컴포넌트 간 의존성 해결을 위해 Tick이나 Timer를 사용하는 비효율적인 방법 사용.
  • 초기화 로직 분산:
    하나의 시스템 초기화에 필요한 코드가 여러 클래스에 흩어져 있음
    전체 초기화 흐름 파악이 어렵고, 수정 시 여러 파일을 동시에 변경해야 함
  • 객체 생성 후 유효 상태 불확실:
    언리얼 엔진 환경에서는 생성자 제약으로 인해 별도의 Init() 함수를 사용하는 경우가 많음. (다시 초기화 분산 발생 가능)
    비동기 코드 등으로 인해 객체 생성 시점과 유효 상태 확보 시점을 알기 힘듦.

GameFeature Plugin & ISS 로 문제 해결

문제 유형 상세 내용 해결책
팀내 코드 충돌 코어 모듈 직접 수정으로 인한 Merge Conflict 발생 GameFeature Plugin: 각 기능을 독립된 플러그인으로 분리하여 코어 모듈 수정 불필요
의존 객체 준비 시점 불명확 Tick/Timer 반복 체크 등 비효율적인 타이밍 관리 Initialize State System (ISS):
CanChangeInitState()
로 의존성 명시, 필요한 객체가 준비되면 자동으로 초기화
초기화 로직 분산 초기화 코드가 여러 클래스에 흩어져 있어 흐름 파악 곤란 Initialize State System (ISS): 각 컴포넌트의 초기화 로직이
해당 클래스 내부에만 존재, 다른 클래스에서 Init 함수 호출 X
초기화 흐름 파악 곤란 시스템 전체 초기화 과정 추적 어려움 중앙 관리자 (PawnStateManager): 전체 초기화 흐름을 한 곳에서 파악, 디버깅 용이

1. GameFeature Plugin 상세

게임 피처 플러그인은
하나의 기능을 독립적인 부품으로 분리하여 관리하고,
런타임에 추가/제거 가능한 모듈 시스템 입니다.

팀원 간 코드 충돌을 줄이고, 모듈형 게임 플레이 개발의 핵심입니다.

기존 게임에서는 코어 모듈에 기능들을 모두 포함하고, 콘텐츠만 따로 분리시켰다면 기능, 콘텐츠 모두 독립적으로 개발할 수 있습니다.

게임은 피처에 의존 되지 않아, 플러그 앤 플레이 방식이 가능합니다.

1-1. 결과


각 기능들을 독립적인 GameFeature Plugin 으로 개발했습니다.

게임 피처들을 필요할 때, 로드하며 레고 블록처럼 기능을 조립하는 방식입니다.


GameFeature 들을 묶고, 폰을 변경할 수 있는 Character Definition 이라는 PrimaryAsset 을 구현

새로운 캐릭터 추가 시 기존 코드 수정 없이 해당 PrimaryAsset 만 생성하면 되어 확장성을 높였습니다.


예시: 스파이더맨 (Pawn - BP_Spidy / GameFeature - WebSwing, MovementStandard)


GameFeature 적용 효과

  • 모듈형 개발은 생산성을 높이고, 협업의 효율이 증가
  • 커스텀 GameFeatureAction 으로 빠른 프로토타이핑 가능 (예시: AddAbilities 특정 캐릭터에 능력 부여)
  • 동적 로딩으로 메모리 최적화 (예시: 튜토리얼에서는 튜토리얼 관련 GameFeature만 로드)
  • 런타임 전환으로 동적 게임플레이 구현
  • DLC 콘텐츠 쉽게 배포 가능

1-2. Lyra에서의 개선점

Lyra는 GameFeature를 하나의 장르(예: ShooterCore)로서 접근하여 여러 시스템(카메라, 무기 등)을 포함했지만,
범위가 넓으므로 여러 명의 팀원이 같이 개발할 가능성이 높아 보였습니다.

그러므로 팀원 간 코드 충돌 문제를 해결하기 어려울 수 있다고 판단했고, 다음과 같이 개선했습니다.


  • GameFeature 1개하나의 기능 또는 콘텐츠로 정의
    (예: GameFeature_ItemShop, GameFeature_WebSwing)
  • 하나의 기능 = 한 명의 담당자 → 완전한 독립 개발을 목표로 합니다.
  • Lyra 는 게임 장르로서, 현재 프로젝트는 단일 기능으로 사용했습니다.

1-3. 구체적인 과정과 연구

2. Init State System(ISS) 상세

ISS는 초기화에 필요한 의존성을 명확화하고, 자동으로 초기화하여,
기존의 수동적인 초기화 방식을 피하고 타이밍 이슈를 제거합니다.

2-1. 결과

// ===== 기존 방식 (수동 초기화) =====
    void AMyCharacter::BeginPlay()
    {
        Super::BeginPlay();
        
        AbilitySystemComponent->Initialize();  // 먼저 초기화되어야 함
        HealthComponent->Initialize();          // ASC에 의존하므로 ASC 이후에 초기화
        InputComponent->Initialize();
        InventoryComponent->Initialize();
    }
    
    // ===== ISS 적용 후 (각 컴포넌트 내부에서 자동 초기화) =====
    void ABSCharacter::BeginPlay()
    {
        Super::BeginPlay();
    }

ISS 적용 효과 1

  • ISS가 모든 컴포넌트를 자동으로 초기화
  • Git 충돌 위험 제거: Character.cpp 수정 최소화
  • 캐릭터의 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());
		}
	}
}

ISS 적용 효과 2

  • 의존성 명확화: CanChangeInitState() 함수로 의존 관계 표현
  • 의존성이 확보 되면 컴포넌트에서 자동으로 초기화 (현재 상태에 따라 처리 가능)
  • 수동으로 초기화 함수를 호출할 필요 없음.

2-2. Lyra 에서의 개선점과 적용 효과

Lyra 는 컴포넌트들 중 일부만 ISS 를 적용했고, LyraHealthComponent의 Initialize 는 수동으로 호출하고 있습니다.

이는 일관성을 유지하기 힘들고, 후에 코드를 유지보수에도 영향을 끼칠 수 있다고 판단했습니다.

이어서 컴포넌트들의 초기화 상태가 어떻게 진행되고 있는지 파악하기 힘들었습니다.


따라서, 현재 프로젝트에는 다음과 같이 개선했습니다.


1. ISS 를 사용한다면, 모든 컴포넌트에 적용하기 (수동 초기화 로직 피하기)
2. 모든 컴포넌트들의 상태를 한 곳에서 조율하는 중앙 관리자 클래스 추가하기 (PawnStateManager)

적용 효과

  • Character의 BeginPlay 에 초기화 관련 코드가 없음.
  • 각 컴포넌트에서 자동으로 초기화하여 일관성이 유지됨.
  • 중앙 관리자 컴포넌트의 상태로 컴포넌트들의 최소 상태가 보장되고, 디버깅이 용이함.

2-3 구체적인 과정 및 연구

3. Data Registry 로 DLC 추가

3-1. 결과

Data Registry를 활용하여 게임의 정적 데이터를 관리하고, 추가적인 코드 작성 없이
GameFeatureAction 으로 Data Registry Source를 추가하여 코어 시스템 수정 없이 DLC 콘텐츠를 확장했습니다.

3-2. 동작 흐름

1. 기본 상점 활성화

  • ItemShop 게임 피처 플러그인 활성화:
    DR_ShopItems 레지스트리 추가 및 기본 데이터 테이블(DT_Default_Consumable) 등록.
  • 상점 UI 표시:
    UDataRegistrySubsystem을 이용해 ShopItems 레지스트리의 모든 아이템 조회 및 UI 초기화.
    기본 소비 아이템들이 상점에 표시됨.

2. DLC 추가 및 콘텐츠 자동 확장

  • DLC_SpecialEdition 게임 피처 활성화:
    • 기존 ShopItems Data Registry에 Data Registry Source로 DT_DLC_Consumable 추가
    • 코어 시스템 수정 없이 순수하게 데이터만 추가
  • 자동으로 상점의 콘텐츠 확장:
    • ShopItems 레지스트리가 자동으로 DLC 아이템 포함
    • 상점 UI 오픈 시 기본 아이템 + DLC 아이템 모두 표시
    • 런타임 소스 확인 시 데이터 테이블이 추가된 것을 확인 가능

3-3. 구체적인 과정 및 연구