언리얼 엔진 워크플로우를 검색해보면 꼭 따라오는 이미지가 있다.

(워크플로우, 게임 플로우 등으로 검색하면 나온다)

 

바로 위의 Flow Chart인데, 에디터 / 스텐드얼론 (패키징) 에서 게임이 어떻게 실행되는지에 대해 간략히 설명해준다.

 

사실, 해당 과정들을 간략히만 알아도 큰 문제는 없었으나 연차가 얼추 붙으면서 좀 정리를 할 필요가 있을거같아 정리를 해둘까 한다.

 

 


해당 자료는 MS Copilot과 공식 문서 등을 참고하여 제작되었습니다.

틀린점이 있다면 지적바랍니다.


Editor 관점

Editor 관점은, 언리얼 에디터를 실행하고, PIE (Play In Editor)로 게임을 실행하기까지의 과정을 의미한다.

 

[1] UEditorEngine Initialize 단계

쉽게 설명해서, Editor를 구성하는 단계라고 볼 수 있다.

 

1. Editor Modules Initialize :

에디터에서 사용되는 모듈 초기화. 레벨 에디터, 블루프린트 편집기, 머티리얼 편집기 등 에디터 기능들이 포함됨.


2. 프로젝트 파일 열기:

사용자 프로젝트 파일을 로드하고, 필요한 에셋들을 초기화.


3. Config 파일 로드:

엔진 설정과 프로젝트 설정에 필요한 config 파일을 로드. DefaultEngine.ini 파일 등을 불러오는 과정.

 

4. Editor 세계 생성:

에디터에서 사용할 UWorld 인스턴스 생성

 

5. 플러그인 로드:

에디터에서 필요한 플러그인들을 로드.


6. 레벨 브라우저 및 콘텐츠 브라우저 초기화:

사용자가 에디터에서 접근할 수 있는 다양한 브라우저를 초기화.

 

7. 뷰포트 설정

에디터의 뷰포트를 초기화, 사용자에게 보여질 화면을 설정.


8. 리스너 설정:

에디터 이벤트에 대한 리스너들을 설정.
여기서 리스너는 키보드, 마우스 등의 입력장치 뿐만 아니라, "에디터의" UI 이벤트, 파일 IO시 발생하는 이벤트를 의미.


9. 게임 인스턴스 생성:

UGameInstance를 베이스로 하는 게임 인스턴스 클래스를 선택 후 인스턴스 생성

해당 인스턴스는 개발 중의 게임 상태를 관리할때 사용하는 게임 인스턴스임.

 

 

[2] UEngine Start 단계

게임 엔진 초기화가 끝나고, 실제로 시작하는 단계라고 볼 수 있다.

 

1. 엔진 초기화 확인:

모든 필수 모듈과 시스템이 올바르게 초기화되었는지 확인.

 

2. 로그 시스템 설정:

로그 및 디버그 시스템을 설정합니다. 로그 레벨 (Error, Warning 등)을 정의하는 단계.

 

3. 렌더링 초기화:

GPU 와 관련된 렌더링 시스템 / 리소스를 초기화하고, 렌더러를 설정.

렌더러, 쉐이더, 텍스쳐 및 모델을 설정하고 초기화하는 단계이기도 함.


4. 입력 시스템 초기화: 

입력 장치 및 매핑을 초기화. 입력 매핑을 설정하고, 입력 이벤트를 처리할 수 있도록 세팅.


5. 오디오 시스템 초기화: 

하드웨어 오디오 장치와 관련된 리소스 초기화, 사운드 파일 경로 설정 및 파일 로드.


6. 네트워크 시스템 초기화:

서버와 클라이언트 연결을 설정하고, 네트워크 프로토콜을 초기화하며, 온라인 세션을 관리.


7. 콘솔 명령어 등록:

디버깅과 테스트를 위한 콘솔 명령어를 등록. 엔진 내에서 다양한 명령어를 사용할 수 있도록 설정.

 

8. 게임 인스턴스 초기화:

이전 단계에서 생성된 게임 인스턴스 (PIE UGameInstnace) 를 초기화. 게임 인스턴스 내의 서브시스템 초기화, 각종 이벤트 리스너 바인딩 등을 수행

 

9. 월드 생성 및 초기화:

게임 내의 월드를 생성하고 초기화. 맵을 로드하고, 월드 내의 객체들을 스폰하며, 게임플레이를 위한 환경을 조성.

 

[3] PIE 활성화 단계

Create UGameInstance 및 Initialize PIE로 설명되어있으나, 구체적으로는 조금 다른듯 하다.

 

1. PIE 세션 설정: (Create UGameInstacne -> Initialize PIE 단계)

이미 생성된 UGameInstnace가 PIE 모드에서 실행 될 수 있도록 네트워크, 세션 설정등을 적용.

 

2. PIE 게임 인스턴스 시작: ( (UGameInstance) Init 단계 )

게임 인스턴스 (PIE UGameInstnace) 에 대한 추가적인 초기화 진행. 게임 데이터 및 리소스를 로드하거나 설정 적용.

UEngine Start에서 수행한것과 유사하나 PIE에 맞는 세팅이 추가적으로 진행되는것으로 보임.

 

3. (UEditorEngine) CreatePIEGameInstance:

이미 초기화된 UGameInstance를 PIE 모드로 전환하기 위한 설정을 하고, UEditorEngine에서 PIEGameInstance를 실행할 준비를 수행. PIE모드에서 실제 게임 플레이를 시뮬레이션하기 위한 추가 설정 및 조정이 이루어짐.

이 과정에서는 CreatePIEGameInstance (정확히는 UEditorEngine 클래스에서 PreCreatePIEInstances 함수가 호출되며 생성되는것으로 보임) 가 호출되어 PIE용 UGameInstance가 생성됨.

 

4. PIE 모드 월드 초기화: ( (UEditorEngine) StartPIEGameInstance 단계 )

레벨 로드, 객체 생성 등 기본적인 월드 구성을 수행

 


Standalone 관점 

Standalone 모드에서는 PIE가 아닌 독립적으로 실행되는 프로그램으로써 진행된다.

 

[1] 엔진 시작 단계:

해당 단계는 Start Engine -> (UGameEngine) Init 단계를 의미한다.

 

1. 엔진 시작:

Standalone 모드로 엔진을 시작. 엔진 기본 설정이 로드되고 초기 작업 수행.

 

2. 필수 모듈 로드:

RenderCore, InputCore 등 각종 모듈들이 로드 되는 과정

 

3. 기본 설정 초기화:

각종 Config 파일들 (DefaultEngine.ini 파일 등)을 불러오는 과정

 

4. 엔진 초기화 확인: (UGameEngine Init 단계)

필수적인 모듈과 시스템이 올바르게 초기화되었는지 확인.

 

5. 로그 시스템 설정:

로그 및 디버그 시스템을 설정합니다. 로그 레벨 (Error, Warning 등)을 정의하는 단계.

 

6. 렌더링 초기화:

GPU 와 관련된 렌더링 시스템 / 리소스를 초기화하고, 렌더러를 설정.

렌더러, 쉐이더, 텍스쳐 및 모델을 설정하고 초기화하는 단계이기도 함.


7. 입력 시스템 초기화: 

입력 장치 및 매핑을 초기화. 입력 매핑을 설정하고, 입력 이벤트를 처리할 수 있도록 세팅.


8. 오디오 시스템 초기화: 

하드웨어 오디오 장치와 관련된 리소스 초기화, 사운드 파일 경로 설정 및 파일 로드.


9. 네트워크 시스템 초기화:

서버와 클라이언트 연결을 설정하고, 네트워크 프로토콜을 초기화하며, 온라인 세션을 관리.


10. 콘솔 명령어 등록:

디버깅과 테스트를 위한 콘솔 명령어를 등록. 엔진 내에서 다양한 명령어를 사용할 수 있도록 설정.


[2] UGameInstance 준비 단계:

Create UGameInstance -> InitializeStandalone -> UGameInstance Init 단계를 의미

 

1. 게임 인스턴스 생성:

UGameInstance를 베이스로 하는 게임 인스턴스 클래스를 선택 후 인스턴스 생성.

메모리 할당 및 객체 초기화도 이루어짐

 

2. 독립 실행 모드 설정:

Standalone 모드에서 게임 인스턴스를 초기화하는 과정.

네트워크 설정, 게임 데이터 로드 등 독립 실행 모드에 필요한 추가 설정이 적용됨.

 

3. 서브시스템 초기화:

Standalone 모드에서 필요한 다양한 서브시스템을 초기화. 세이브 데이터 시스템, 네트워크 관리 시스템 등이 포함됨.

 

4. 게임 로직 초기화:

UGameInstance 내에서 기본적인 게임 로직을 초기화. 게임 상태 초기화, 플레이어 데이터 로드 등이 포함됨.

 

5. 이벤트 바인딩:

게임 인스턴스와 관련된 다양한 이벤트에 대한 리스너를 설정하고 바인딩.

게임 시작, 종료, 일시정지 등의 이벤트에 대한 리스너 및 처리기를 등록.

 

[3] 네트워크 준비 단계:

Create UOnlineSession and register delegates 과정을 의미. 선택사항.

 

1. 온라인 세션 생성 :

네트워크 플레이를 지원하는 경우, 새로운 온라인 세션을 생성하고 서버와 클라이언트 연결을 설정함.

2. 대리자(Delegate) 등록:
온라인 세션에서 발생하는 다양한 이벤트를 처리하기 위해 대리자(delegate)를 등록.

플레이어가 세션에 참가하거나 나갈 때 호출될 함수들을 설정함.

 

[4] UEngine Start 단계:

기본적으로 초기화 단계는 Init 단계와 유사하나, 일부 다른점이 있음.

 

1. 모듈 초기화:

Init 단계에서 수행한 과정과 유사함.

 

2. 게임 인스턴스 시작:

이전 단계에서 생성된 게임 인스턴스를 시작. 게임의 전반적인 상태를 관리하고, 게임 로직을 실행할 준비를 수행.

 

3. 월드 생성 및 초기화:

게임 내의 월드를 생성하고 초기화. 맵을 로드하고, 월드 내의 객체들을 스폰하며, 게임플레이를 위한 환경을 조성.

 


이후에는 둘다 동일하게 BeginPlay부터 시작하는 과정을 거친다.

해당 과정은 또 따로 정리할 필요가 있어 여기서는 설명하지 않는다.

회사 및 과거 프로젝트가 유추될 수 있는 사항은 제외했습니다.

 


언리얼 리플렉션 시스템 (https://www.unrealengine.com/ko/blog/unreal-property-system-reflection)

  • 프로그램이 실행시간에 자기 자신을 조사할 수 있는 기능. (본인이 어떤 클래스인지 등을 알 수 있음)
  • 언리얼 엔진에서 C++ 클래스, 구조체, 함수, 멤버 변수, 열거형 관련 정보를 수집, 질의, 조작하는 별도의 시스템
  • 리플렉션 시스템에 등록시키고자 하는 변수 등에 UProperty 키워드 등을 부착시 Unreal Header Tool (UHT) 에 의해 리플렉션 시스템에 등록됨
    • UProperty 사용시, 언리얼 GC에 포함이 됨.
    • 각종 메타데이터들을 추가하여 해당 변수가 어떤 속성을 가졌는지를 알 수 있음.

 

 

 

언리얼 최적화 관련

  • 언리얼에서는 자체적인 GC를 사용함.
  • GC는 다음과 같이 작동함 :: 
    Unreachable 플래그를 모든 UObject에 부착 -> RootSet에 걸려있는 UObject에서 플래그 회수 -> 플래그 남은 UObject 기록 -> 리스트업 된 UObject Destroy (Begin/Finish) -> UObject의 소멸자 호출
  • GC의 타겟이 되지 않게 하기 위해서는 관련 오브젝트에 "AddToRoot" 혹은 "SetFlags(RF_MarkAsRootSet)" 플래그를 붙이면 됨.
  • 주로, 인게임에서 이미 소멸된 액터거나 Replicate 범위 밖 (NetCullDistance)에 존재하는 경우 타겟이 될 수 있음.

 

 

 

언리얼 리플리케이션 관련

  • 기본 개념 : 서버-클라간 동기화를 위한 개념
    • 서버->클라로는 호출한 / 소지한 (Autonomous) 클라이언트에게, 혹은 모든 클라이언트에게 전달 (Multicast) 가능
    • 클라->서버로는 클라이언트가 소지한 액터가 Autonomous일때만 서버로 전송가능 (이것도 RPC만 가능)
    • 변수 리플리케이션은 "원칙적으로는" 서버->클라만 가능함.
  • Role
    • Role은 Local Role / Remote Role 2개가 있음.
    • 보통 서버 입장에서 Local Role은 Authority인 경우가 대부분
    • 클라에서는 Remote는 Authority, Local이 다를 수 있음.
    • 비교
      • 클라 입장에서 Autonomous - "현재 클라이언트가 컨트롤 중인 플레이어"
      • 클라 입장에서 Simulated - "현재 클라이언트의 영향 외의 동기화 액터"
  • 기타
    • RPC는 단발성 리플리케이션에 주로 사용, 변수 리플리케이션은 영구적 리플리케이션에서 주로 사용함.

 

 

언리얼 포인터 관련


기본적으로 언리얼 스마트 포인터는 Object 포인터와 일반 포인터 (비 Object 포인터 = C++ 노멀 포인터)가 있음

  • 오브젝트 포인터 (Actor 클래스 등의 언리얼 오브젝트에서 사용. 언리얼 GC 타겟이 된다.)
    • TWeakObjectPtr - 순환참조 이슈 등을 대응하기 위한 오브젝트 포인터. 참조 대상 파괴시 nullptr로 세팅됨.
    • TObjectPtr - 64bit 기반 신규 포인터.
      원시포인터 (Normal Pointer) 와 유사하나, 다이나믹 해상도 (액터에 대한 동적 해상도 전환), 엑세스 트래킹 등의 신규 기능 추가.
      • 5.0 되면서 추가된 신규 포인터임. 원시 포인터를 TObjectPtr로 바꾸는걸 언리얼에서는 권장함.

  • 노멀 포인터
    • TUniquePtr - 일반적인 Unique Pointer와 동일. (하나의 포인터로 단 하나의 메모리만을 참조)
    • TSharedPtr - 일반적인 Shared Pointer와 동일
    • TWeakPtr - 일반적인 Weak Pointer와 동일 (Shared Pointer의 보조)
      • TWeakPtr은 TSharedPtr를 통해서만 복사 생성, 대입 연산이 가능하다.
      • TWeakPtr은 TSharedPtr를 참조하되, 레퍼런스 카운트를 증가시키지 않는다.
      • TWeakPtr을 통해 객체를 참조하려면 반드시 TSharedPtr로 변환하여 사용해야 한다.

 

 

 

* 주의 *

1. 해당 작업은 StandAlone에서만 테스트되었습니다.

2. 해당 작업은 임시 연동을 위해 작업되었으며, 타 환경 (네트워크 환경 등)에서는 정상 작동을 보장하지 않습니다.

3. 필요시, 코드를 적당히 수정해서 사용해주세요!

 

 

(아마 기억이 맞다면) 언리얼 5.3에 들어오면서 큰 변화가 몇가지 생겼다.

그중에 프로그래머 입장에서 유의미하게 볼것이 바로 EnhancedInput의 정식 사용 시작 및 AbilitySystem의 공식 플러그인 화 일것이다.

 

근데 웃긴게, 이 두가지가 하나는 정식기능, 다른 하나는 플러그인이라서 그런가 공식적으로 지원하는 연동 코드가 없다.....

 

물론, 진짜 하나도 없진 않다. 대신, LyraStarterGame에 포함되어있을뿐...

 

LyraStarterGame에서는 해당 기능을 HeroComponent라는 연동 컴포넌트와, EnhancedInput을 위한 LyraInputComponent를 만들어서 사용하는데, 해당 컴포넌트 및 Lyra 식 사용에 대한 분석은 다음에 하기로 하고, 우선은 조금 빠른 사용법을 익혀보도록 하자.

 

 

* 해당 코드의 일부는 EpicGames에서 제공하는 Lyra Starter Game에서 발췌했습니다.


우선, 이전 글을 참고하여 AbilitySystemComponent를 정상적으로 사용할 수 있는 상태를 기준으로 한다.

 

https://locketgoma.tistory.com/80

 

짧막 팁 : AbilitySystem 관련 헤더를 불러오지 못할때

대충 이런 상황이다. AbilitySystemComponent 및 기타 관련 헤더를 사용해야하는데 못불러오는경우... 해당 문제는 모듈이 빠져있는 상황으로, 프로젝트 명칭으로 된 폴더에 있는 "(ProjectName).Build.cs"

locketgoma.tistory.com

 

 

두가지를 연동하려면 우선, InputAction과 GameplayTag를 연동시켜둔 데이터파일이 필요하다.

해당 파일 타입을 Lyra에서는 "FLyraInputAction" 라는 struct를 사용하는 ULyraInputConfig라는 DataAsset를 만들어 사용한다.

 

일단, 해당 방법을 그대로 사용하자.

 

UDataAsset를 상속받는 신규 클래스를 만들고, 해당 파일에

 

USTRUCT(BlueprintType)
struct FInputAction		//이름은 적당히 바꿔서 쓰면 됩니다.
{
	GENERATED_BODY()

public:

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
	TObjectPtr<const UInputAction> InputAction = nullptr;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Meta = (Categories = "InputTag"))
	FGameplayTag InputTag;
};

해당 구조체를 추가, uproperty로 추가해주면 된다.

Lyra에서는 (EditDefaultsOnly, BlueprintReadOnly) 조건으로 사용중이나, 적당히 필요한대로 쓰시면 될것같다.

 

 

 

그 다음부터는 선택지가 갈리는데, EnhancedInputComponent 및 PanwComponent를 상속받아서 Lyra와 같이 InputComponent + HeroComponent를 만들어서 사용하거나, 간략하게 AbilitySystemComponent를 상속하여 구현해주는 방법 등이 있다.

 

여기서는 AbilitySystemComponent 을 상속하여 추가 함수를 만들어주는 방식으로 구현하고자 한다.

 

Lyra 방식은... 공부도 하실겸 Lyra 코드를 열어보시는걸 추천드립니다.

 

 

 

첫번째. UAbilitySystemComponent를 상속받은 컴포넌트를 추가한다.

두번째. 위에서 만든 DataAsset Class를 헤더에 추가한다.
(단순히 전방선언으로 사용하면 구조체의 사이즈를 알 수 없어 오류가 발생한다)

세번째. 다음과 같이 (혹은 적당히 수정하여) 헤더를 작성한다.

UCLASS()
class PROJECTCLOUD_API UCLAbilitySystemComponent : public UAbilitySystemComponent
{
	GENERATED_BODY()

public:
	//인풋 액션을 세팅하는 함수 (return bool인 이유는 오류 검출 용도)
	bool BindInputActions(const UCLAbilityInputConfig* InputConfig, UEnhancedInputComponent* EnhancedInputComponent);
	
	//인풋 액션을 호출하는 바인딩 함수
	void TryActiveAbilityFromInputAction(const FInputActionInstance& Value);

private:
	//인풋 액션 리스트
	TMap<TObjectPtr<const UInputAction>, FGameplayTag> InputactionList;
};

네번째. cpp 파일에 필요한 헤더들을 추가한다.
EnhancedInput관련, GameplayTag 관련 헤더를 추가해주면 된다.

다섯번째. 바인딩 코드 작성.

bool UCLAbilitySystemComponent::BindInputActions(const UCLAbilityInputConfig* InputConfig, UEnhancedInputComponent* EnhancedInputComponent)
{
	if (!ensure(EnhancedInputComponent))
	{
		UE_LOG(LogTemp, Error, TEXT("UCLAbilitySystemComponent :: EnhancedInputComponent is Null."));
		return false;
	}

	for (const FCLInputAction Action : InputConfig->AbilityInputActions)
	{
		InputactionList.Add(Action.InputAction, Action.InputTag);

		EnhancedInputComponent->BindAction(Action.InputAction, ETriggerEvent::Triggered, this, &UCLAbilitySystemComponent::TryActiveAbilityFromInputAction);
	}
	return true;
}

 

Input으로 받은 DataAsset에서 리스트를 가져와서 EnhancedInput에 바인드하고, 로직 검사를 위해 ASC에 추가한 Map에 등록해주는 로직이다.

 

여섯번째. 바인드 될 호출함수 작성

void UCLAbilitySystemComponent::TryActiveAbilityFromInputAction(const FInputActionInstance& Value)
{
	//1. 이벤트로 들어온 InputAction과 매치되는 Tag가 있는지 검사
	FGameplayTag* InputTag = InputactionList.Find(Value.GetSourceAction());
	
    //2. 매치되는 Tag가 있다면
	if (InputTag != nullptr)
	{
    	//3. Pay로드 작성 후
		FGameplayEventData TempPayload;
		TempPayload.EventTag = *InputTag;
		TempPayload.Instigator = GetOwner();
        
        //4. ASC에 포함된 HandleGameplaytEvent를 호출한다.
        HandleGameplayEvent(TempPayload.EventTag, &TempPayload);
	}
}

Payload값은 임의로 수정해도 된다. (단, EventTag 제외)

 

 

다음과 같이 작성하면 함수 준비는 끝났다.

 

이를 어떻게 사용하느냐...

 

사실 생각보다 별거 없다. EnhancedInput과 ASC를 사용하고자 한다면, 아마 여러가지 검색을 하면서 Player Pawn에다가 연동하는 작업도 수행했을것이다.

아니었다구요? 그렇다면..

 

	UEnhancedInputComponent* EnhancedInputComponent = GetComponentByClass<UEnhancedInputComponent>();

	if (ACLPlayerState * PS = Cast<ACLPlayerState>(GetPlayerState()))
	{
		PS->GetAbilitySystemComponent()->InitAbilityActorInfo(PS, this);
		PS->SetAbilitiesFromActionSet(AbilitySet);
		PS->GetAbilitySystemComponent()->BindInputActions(InputConfig, EnhancedInputComponent);
	}

다음과 같은 로직을 Player Pawn의 Beginplay 등의 Init 단계에서 호출하도록 추가해주면 된다.

 

당연한 얘기겠지만, 이미 EnhancedInput을 Player Pawn에 등록했어야 하고,
해당 로직의 경우에는 PlayerState에 ASC를 등록해서 사용했기에, 해당 과정도 거친 상태여야 한다.

(스텐드 얼론이어서 Pawn에 직접 ASC를 붙여서 사용한다면, 코드를 적당히 수정하면 된다.)

 

다음과 같이 등록하고, Player Pawn에 "InputConfig"  변수를 추가해주면 끝.

 

정상적으로 잘 따라왔다면,

 다음과 같은 InputConfig를 만들 수 있고,

해당 InputConfig를 추가할 수 있는 공간이 Player Pawn에 추가된다.

 

이제 자유롭게 InputAction과 InputTag를 부착하면, Player의 ASC에 부착된 GameplayAbility 중 Trigger Event로 호출되는 Ability를 사용할 수 있게 된다.

 

 


ASC 사용법은 저 말고도 다른 분들이 잘 설명해준 문서들이 많으니 참고 바랍니다...
EnhancedInput과 ASC 연동하는 방법은 어디에도 없길래 Lyra 코드 분석해가면서 간략화해서 만들어봤어요

 

틀린 내용에 대한 지적은 언제나 받습니다.

+ Recent posts