본문 바로가기

Unreal/작업방식

언리얼: Build.cs에서 디렉터리 존재유무 확인하기

언리얼 엔진 플러그인 개발시 다른 플러그인에 종속성을 갖는 것은 자주 생기는 일입니다. 모듈의 Build.cs에서 다른 모듈을 디펜던시에 추가함으로써 라이브러리를 가져다 사용할 수 있게 되는데요, 만약 종속성이 생겨야 하는 모듈이 존재하지 않을 수도 있다면 어떻게 처리해야할까요?

 

엔진 단에서 직접적인 기능을 제공하지는 않지만, 간접적으로나마 몇 가지 사항을 체크하여 동적으로 종속성의 판단을 수행할 수 있습니다. 다만 동적이라고 표현되었지만 빌드 스크립트에서 지정해 컴파일 타임에 진행된다는 점을 유의해야 합니다. 

 

플러그인이 설치되었는지 몇 가지 단서가 존재할 수 있는데요. 

 

1. 설치된 엔진에 플러그인이 존재하는지 or 프로젝트에 플러그인이 존재하는지

2. 현재 프로젝트에 플러그인이 활성화 되었는지

3. 적합한 Target, Configuration을 위해 허용된 플러그인 인지 확인

 

이 정도 과정을 거치면 어느정도 판단이 가능합니다. 

모든 케이스를 다루지는 않을텐데요, 간단한 예제를 소개하겠습니다. 

 

string ConstraintPluginDir = Path.GetFullPath(PluginDirectory + "/../" + PLUGIN_NAME);
bool IsDirExist = Directory.Exists(ConstraintPluginDir);

if (IsDirExist)
{
	ProjectDescriptor Project = ProjectDescriptor.FromFile(Target.ProjectFile);
	var projectPlugins = Plugins.ReadProjectPlugins(Target.ProjectFile.Directory);

	foreach (PluginInfo pluginInfo in projectPlugins)
	{
		if (pluginInfo.Name == PLUGIN_NAME)
		{
			if (
#if UE_4_24_OR_LATER
				Plugins.IsPluginEnabledForTarget
#else
							Plugins.IsPluginEnabledForProject
#endif
					(pluginInfo, Project, Target.Platform, Target.Configuration, 
                     TargetRules.TargetType.Game))
			{
				PrivateDependencyModuleNames.Add(MODULE_NAME);
			}

			break;
		}
	}
}

 

먼저, 디렉터리를 확인합니다. 

저는 프로젝트폴더/Plugins 경로에 모든 플러그인을 설치해두는 스타일인데요, 그에 기반하여 코드를 작성했습니다. 

 

PluginDirectory는 모듈이 소속된 플러그인의 경로를 리턴하는데요, ChatClient.Build.cs 였기 때문에, ProjectRoot/Plugins/ChatClient 를 리턴할 것입니다. 그래서 상위 폴더에 LiteTCP 폴더가 있는지 확인하는 과정을 코드로 작성했습니다. 

 

만약 게임 프로젝트의 Build.cs 에서 확인해야 한다면, Target.ProjectFile에서 Plugins 폴더를 찾은 다음, 플러그인이 포함되었는지 서치할 수 있습니다. 

 

 

조금 더 심화 해 볼까요?

LiteTCP 모듈 에서 전처리기를 정의하고, 이를 활용하여 ChatClient에서 로직을 구성해 봅시다.

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class LiteTCP : ModuleRules
{
	public LiteTCP(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
		
		PublicDependencyModuleNames.AddRange(
			new string[]
			{
				"Core",
			}
			);
			
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject", "Engine", "Slate", "SlateCore",
			}
			);

		PublicDefinitions.Add("LITE_TCP=1");
	}
}

LITE_TCP=1 이라는 전처리기를 넣어 주었는데요, 이는 C++ 코드에서 

#define LITE_TCP 1

이 됩니다. 

 

이제 ChatClient 모듈의 코드에서 이 전처리기를 활용하여 로직을 작성합니다.

// Copyright Epic Games, Inc. All Rights Reserved.

#include "ChatClient.h"

#define LOCTEXT_NAMESPACE "FChatClientModule"

void FChatClientModule::StartupModule()
{
#if LITE_TCP
	UE_LOG(LogTemp, Log, TEXT("LITE TCP IS LOADED!"));
#endif
}

void FChatClientModule::ShutdownModule()
{
}

#undef LOCTEXT_NAMESPACE
	
IMPLEMENT_MODULE(FChatClientModule, ChatClient)

 

런타임에 모듈이 로드될 때, LITE_TCP 모듈이 존재한다면 (이미 Build.cs를 통해 컴파일타임에 결정되었음) 로그가 출력될 것입니다. 그런데 만약, 컴파일 타임에 LiteTCP 플러그인이 없다면 컴파일 에러가 발생할텐데요, LITE_TCP 전처리기가 정의되지 않았기 때문입니다. 

 

ChatClient 플러그인을 개발하는 입장에서는 어떤 외부 플러그인이 선택적으로 적용될 수 있을지 미리 알고 있으므로, 이런 상황을 대비하는 방어코드를 넣어주어야 합니다. 

 

ChatClient.Build.cs에서 외부 모듈의 활성화를 판독한 다음, PublicDefinitions.Add로 LITE_TCP=0 을 넣어주면 됩니다. 이번에는 조금 다른 방법으로 해결해 보았습니다. 

 

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

#ifndef LITE_TCP
#define LITE_TCP 0
#endif

class FChatClientModule : public IModuleInterface
{
public:

	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
};

원래는 PreCompiledHeader와 같은 헤더에 포함되는 것이 바람직한데요, LITE_TCP 정의를 모듈의 구현부에서 사용했으므로 모듈 헤더에 넣어도 무방하다고 판단되어 모듈헤더에 직접 정의했습니다. 

 

ChatClient.Build.cs에서 처리하지 않은 이유는 다소 설득력이 떨어질 수 있지만 코드를 처음 파악할 때 전처리기가 정의되지 않는 경우를 C++ 코드에서 확인할 수 있는 쪽이 흐름 파악에 도움이 될 것이라 판단했기 때문입니다. 필요에 따라 Build.cs에서 모두 정의해주는 것도 괜찮을 선택일 수 있을 것입니다. 

 

여담

Build.cs에서 넣어주면 IDE 레벨에서는 predefined라고 뜨며 정의부를 찾아주지 못하는데요,

LITE_TCP_INTERNAL과 같이 실사용 전처리기와 다른이름으로 정의하고, 그 정의 여부에 따라 코드에서 재정의 해 사용해주어도 됩니다.