Component 기반 모듈러 시스템

김혁·2025년 8월 9일

챌린지

목록 보기
4/14

Lyra에 대하여..

Epic Games가 Lyra 샘플 게임을 만든 이유

  1. UE5의 새로운 템플릿 제시
  • 언리얼 엔진 5의 기술력과 구조적 방향성을 보여주는 레퍼런스 프로젝트
  • 실제 게임에 가까운 완성도 높은 구조로 제공
  1. 모듈화와 확장성을 강조한 설계 철학 홍보
  • 엔진 레벨에서 지원하는 모듈화, 게임 기능 플러그인 등의 확장성 중심 구조를 실제로 구현
  • AAA급 게임 개발에 적합한 구조를 직접적으로 참고할 수 있도록 한 것
  1. 멀티플레이어 기반 설계 예시 제공
  • 네트워킹, 팀 기반 게임, 세션 관리, 리스폰 등 멀티플레이 관련 기능을 포함
  • 협업 및 멀티플랫폼 게임 개발을 목표로 하는 개발자에게 큰 도움.

구조 원칙

  1. Modular - 기능은 분리하고, 조립은 자유롭게
  2. Scalable - 소형 인디부터 대형 AAA까지 대응
  3. Collaborative - 수십 명이 동시에 작업 가능

Component 시스템(모듈화)

Component란..

  • Actor = 게임 월드에 존재하는 '물건' 또는 '존재'
  • Component = Actor가 할 수 있는 '기능'

Component의 종류

  1. UActorComponent
  • 공간상 존재하지 않고 기능만 있음, 위치 정보 없음
  • 로직만 처리
  • 체력, 인벤토리, AI 등 (로직 전용)
  • 성능 부담이 가벼움
  1. USceneComponent
  • 위치, 회전, 크기를 가지고, 게임 월드에서 실제로 공간에 존재
  • 다른 컴포넌트에 부착 가능
  • 카메라, 메시, 충돌 등 (위치 필요 기능)
  • Transform 계산이 필요하기 때문에 상대적으로 성능 비용이 있음

Component 생성 방법

1. 블루프린트 에디터

: 블루프린트에서 Components 탭에서 직접 추가하는 방법

  • 장점

    • 빠르고 시각적으로 잘 보인다. (디자이너도 쉽게 가능)
    • 구조가 바로 에디터에서 눈에 보여서 초보자에게 친숙
    • 반복적으로 붙는 컴포넌트에 적합
  • 단점

    • 조건부 생성 불가능
    • 동적으로 붙거나 제거되는 구조는 구현하기 어려움
    • 클래스에 따라 자동 세팅하고 싶은 경우, 제약이 많음

2. C++ 생성자

: 클래스 생성자에서 CreateDefaultSubobject() 함수를 통하여 추가하는 방법

  • 장점

    • 컴포넌트가 항상 Actor에 붙어 있음 (신뢰성 ↑)
    • 퍼포먼스 최적화 : 게임 시작 전에 세팅이 완료됨
    • 블루프린트에서 값만 덮어쓰기 가능
  • 단점

    • 조건적 생성 어려움
    • 생성자 안에서는 런타임 정보 접근 불가
    • 유연성이 떨어짐

3. 런타임 동적 생성

: 동적으로 특정 조건 시에 NewObject() 함수를 통해 컴포넌트를 생성한 후에, RegisterComponentWithWorld() 함수를 통해 등록하는 방법

  • 장점
    • 매우 유연함 : 조건에 따라 언제든지 붙였다 뗐다 가능
    • 실시간 변화에 적합
    • 에디터 설정에 의존하지 않음
  • 단점
    • 구조 추적이 어려움 (코드 상에만 존재)
    • 생성 순서, 부착 순서, BeginPlay 호출 시점 등 조심해야 할 것이 많음
    • 디버깅이 상대적으로 어렵고, 다른 방법에 비해 비용이 많이 듬

Lyra의 동적 조립 시스템

Character

: Character에는 PawnExtComponent인 하나의 컴포넌트만 가지고 있음

ALyraCharacter::ALyraCharacter()
{
	PawnExtComponent = CreateDefaultSubobject<ULyraPawnExtensionComponent>("PawnExt");
}

PawnData

: Component 조립 설계도로, Component랑 Ability를 가지고 있는 데이터 에셋임

UCLASS()
class ULyraPawnData : public UPrimaryDataAsset
{
	UPROPERTY(EditDefaultsOnly)
    TArray<TSubclassOf<UActorComponent>> Components;
    
    UPROPERTY(EditDefaultsOnly)
    TArray<TSubclassOf<ULyraAbilitySet>> AbilitySets;
}

런타임 조립 과정

: 직업 변경 시에 Component 및 Ability를 동적으로 붙임

void ULyraPawnExtensionComponent::SetupPawn(const ULyraPawnData* PawnData)
{
	for (TSubclassOf<UActorComponent> CompClass : PawnData->Components)
    {
    	UActorComponent* NewComp = NewObject<UActorComponent>(GetOwner(), CompClass);
        NewComp->RegisterComponentWithWorld(GetWorld());
    }
    
    for (TSubclassOf<ULyraAbilitySet> AbilitySet : PawnData->AbilitySets)
    {
    	GrantAbilitySet(AbilitySet);
    }
}

ULyraPawnData* PaladinData = LoadObject<ULyraPawnData>("Paladin");

void TransformToPaladin(ALyraCharacter* Character)
{
	auto* PawnExt = Character->GetPawnExtension();
    
    PawnExt->RemoveAllComponents();
    PawnExt->SetupPawn(PaladinData);
}

해당 구조가 해결할 수 있는 문제

문제해결 방식
캐릭터마다 다른 기능기능을 런타임에 붙임
구조가 복잡하고 고정됨데이터 기반 유연 구성
코드 확장 어려움에셋만 바꿔도 구조 확장
디자이너 작업 어려움에디터에서 캐릭터 구성 가능
협업 시 충돌컴포넌트 단위 분리 -> 충돌 최소화

델리게이트와 이벤트 기반 통신

델리게이트

: 이벤트가 발생했을 때, 미리 등록된 함수들을 호출하는 시스템

  • 직접 참조할 시에는 결합도가 너무 높아지기 때문에 직접 코드 수정을 해야 하고, 코드가 점점 복잡해짐
  • 델리게이트 방식은 느슨한 결합으로, 특정 리스너들이 이벤트를 구독하고, 이벤트가 발생하면 방송을 해주고, 리스너들이 이것을 알고 특정 행동을 수행하는 구조다.

델리게이트 종류

1. 단일 - DECLARE_DELEGATE

  • 단일 대상

  • 가장 빠름

  • 호출 비용이 거의 없음

  • 블루프린트랑 연동 안 됨

  • 사용 시기

    • Tick 안에서 매프레임 호출해도 부담 없는 경우
    • 단 하나의 시스템만 반응해야 할 때
// 델리게이트 선언
DECLARE_DELEGATE(FOnActionDone);
FOnActionDone OnDone;

// 델리게이트 바인딩
OnDone.BindUObject(this, &UMyClass::Handler);

// 델리게이트 호출
if (OnDone.IsBound()){
	OnDone.Execute();
}

2. 멀티 - DECLARE_MULTICAST_DELEGATE

  • 여러 구독자

  • 빠르고 유연함

  • 블루프린트랑 연동 안 됨

  • 사용 시기

    • C++에서 여러 시스템이 동시에 반응해야 할 때
// 델리게이트 선언
DECLARE_MULTICAST_DELEGATE_OneParam(FOnDamageTaken, float);
FOnDamageTaken OnDamage;

// 델리게이트 바인딩
OnDamage.AddUObject(this, &UMyClass::ReactToDamage);
OnDamage.AddLambda([](float Damage) {
	UE_LOG(LogTemp, Warning, TEXT("Damage: %.1f"), Damage);
});

// 델리게이트 호출
OnDamage.BroadCast(Amount);

3. 블루프린트 멀티 - DECLARE_DYNAMIC_MULTICAST_DELEGATE

  • 블루프린트에서 바인딩 가능

  • 호출은 느리지만, 확장성 최강

  • 런타임 성능은 다른 델리게이트보다 떨어짐

  • 사용 시기

    • 블루프린트에서도 이벤트를 연결하고 싶을 때
// 델리게이트 선언
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDeath, AActor*, Killer);

UPROPERTY(BlueprintAssignable)
FOnDeath OnDeath;

// 델리게이트 바인딩
OnDeath.AddDynamic(this, &UMyClass::ReactToDeath);

// 델리게이트 호출
OnDeath.BroadCast(Other);

4. Event 보호형 - DECLARE_EVENT

  • 특정 클래스에서만 Broadcast 가능하게 만들고 싶을 때

  • 블루프린트랑 연동 안 됨

  • 문법이 다소 특이하고 덜 쓰이지만, 고급 구조에서 유용

  • 사용 시기

    • 보안성이 중요할 때
    • 내부 로직 외엔 누구도 호출하지 못하게 하고 싶을 때
// 델리게이트 선언
DECLARE_EVENT(UGameStateSubsystem, FOnMatchStarted)

class UMyGameState : public UGameStateSubsystem
{
	FOnMatchStarted OnMatchStarted;
    
public:
	FOnMatchStarted& GetMatchStartedEvent() { return OnMatchStarted; }
}

// 델리게이트 바인딩
UMyGameState->GetMatchStartedEvent.Add(this, &UMyClass::OnMatchStart);

// 델리게이트 호출
FOnMatchStarted.BroadCast();

Lyra의 설계 철학

4대 원칙

원칙의미구현 방법
단일 책임Component = 하나의 역할하나의 Component에 하나의 기능만
이벤트 기반직접 호출 금지델리게이트 Broadcast
데이터 중심설정은 코드가 아닌 데이터 에셋DataAsset 활용
확장 가능수정 없이 기능 추가델리게이트에 구독만 추가하면 가능


출처: 팀스파르타 내일배움캠프
profile
게임 개발자를 향해..

0개의 댓글