Module
언리얼에는 모듈이라는 개념이 있다.
모듈은 언리얼 엔진의 기본 구성요소이다.
모든 언리얼 엔진 프로젝트는 최소 하나의 primary모듈, 하위 모듈, thirdparty 모듈, plugin모듈들의 집합으로 구성된다.
각 모듈은 일정한 함수들을 (경로와 전처리를 포함하여) 캡슐화하고,
다른 모듈에서 사용할 수 있도록 공용 인터페이스와 컴파일 환경을 제공한다.
모듈은 C# 소스파일에 .build.cs 확장자로 선언되고, 프로젝트의 Source 디렉터리 아래에 저장된다.
c++ 소스 파일들은 .build.cs 파일 옆이나 그 하위 디렉터리에 저장된다.
각 .build.cs 파일에서는 ModuleRules라는 c# 객체를 상속받은 객체를 정의하고,
이 객체 내부에서 어떻게 빌드할지를 제어하도록 프로퍼티들을 설정한다.
이러한 .build.cs 파일은 언리얼 빌드 툴이 컴파일하여 전반적인 컴파일 환경을 구축한다.
다음은 .build.cs 파일의 기본적인 구조이다.
using UnrealBuildTool;
using System.Collections.Generic;
public class MyModule : ModuleRules
{
public MyModule(ReadOnlyTargetRules Target) : base(Target)
{
// Settings go here
}
}
플러그인에 모듈 추가하기
플러그인에 실제로 모듈을 추가해보자.
플러그인 만들기
일단 다음 링크를 통해 플러그인을 만들어 보자.
모듈 추가하기
링크를 통해 플러그인을 만들었다면, 플러그인 내부에 모듈을 하나 더 추가해보자.
기본적으로 플러그인은 하나의 모듈을 가지고 있다.
위와 같은 형태로, Source 파일 하위에 <ModuleName> 폴더가 있고,
그 하위에 <ModuleName>.Build.cs 파일과 소스파일들이 위치해 있다.
같은 형태의 폴더 구조로 모듈을 생성하기 위해, 새 모듈 디렉터리를 하나 만들고 그 하위에 모듈.Build.cs 파일과 소스파일을 추가 후 넣는다.
이후 프로젝트를 다시 생성하고 빌드하면, 새 모듈이 추가된 상태로 작업을 진행할 수 있다.
ThirdParty 모듈 추가하기
일반적으로 thirdparty 모듈이라고 하면, 외부의 라이브러리나 코드를 의미한다.
언리얼에서도 외부 라이브러리나 코드를 thirdparty 모듈 형태로 추가할 수 있다.
Source 하위에 ThirdParty라는 폴더를 생성한다. 일반적으로 언리얼 엔진에서 ThirdParty 모듈들은 ThirdParty라는 이름의 디렉터리에 넣는다. (Source 하위에 ThirdParty를 두지 않으면 라이브러리가 임포트되지 않는 문제가 생긴다.)
모듈 추가하기 예제에서의 방법과 똑같이 모듈을 하나 만든다.
Build.cs를 다음과 같이 수정한다.
using System;
using System.IO;
using UnrealBuildTool;
public class MyThirdPartyLibrary : ModuleRules
{
public MyThirdPartyLibrary(ReadOnlyTargetRules Target) : base(Target)
{
Type = ModuleType.External;
//...
}
}
ModuleType.External으로 타입설정을 해주어, 언리얼 빌드 시스템이 이 모듈 소스를 따로 빌드하지 않도록 한다.
다음은 ThirdPary 모듈 세팅 관련한 공식 문서이다.
모듈 구조 및 관리
모듈 안에서 내수용/외수용 모듈 분리하기
길찾기 과정을 UI에 표시해주는 모듈을 만들었다고 가정해보자.
이 모듈은 NavigationSystem 모듈과 UMG 모듈에 대한 의존성을 갖게 될 것이다.
하지만 여기서 만약 당신이 NavigationSystem 모듈과 UMG 모듈을 모두 public으로 세팅하였다면 어떻게 될까?
그러면 이 모듈을 참조하는 다른 모든 모듈들은 그 두 모듈들에 대한 의존성이 생기게 될 것이다.
하지만 만약 모든 기능을 UI로만 조작하고 NavigationSystem의 기능들은 외부로 노출시키지 않도록 한다면,
NavigationSystem 모듈만 private으로 세팅해주면 될 것이다.
PublicDependencyModuleNames.AddRange(
new string[]
{
"UMG"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"NavigationSystem"
}
);
모듈 안에서 내수용/외수용 코드 분리하기
모듈 내에 클래스를 생성하는 경우 일반적으로 private/public 폴더가 생성된다.
이 폴더들은 코드의 구현 레벨과 인터페이스 레벨을 나누기 위해 존재한다.
일반적으로 구현에 대한 내용이 담긴 cpp파일들은 private 폴더에 넣고, 인터페이스에 대한 내용이 담긴 header파일들은 public 폴더에 넣는다. 그리고 header 중에서도 굳이 외부로 노출될 일이 없는 header들은 private에 넣는것이 의존성을 줄이기 위한 좋은 방법일 것이다.
다음은 엔진 내 AssetManagerEditor 라는 모듈의 private/public에 따른 코드 분리 구조이다.
public 폴더 내에 선언된 클래스나 함수 중에서도 외부에서 직접 사용이 될 수 있는 코드들은,
<ModuleName>_API를 클래스 이름에 붙여 외부 모듈에서 사용 가능하도록 설정해주어야 한다.
이것은 제작된 클래스나 함수에 tag를 붙여 모듈 dll 파일에서 외부로 사용될 수 있도록 해준다.
이에 관련한 자세한 내용은 다음 링크를 참조하자.
stackoverflow.com/questions/56788666/what-is-it-somtemplate-vr-api
언리얼 엔진에서의 일반적 플러그인/모듈 구조
언리얼 엔진에서 일반적으로 플러그인을 개발할때 사용하는 방법을 실제 엔진 플러그인을 참고하여 알아보자.
"Program Files/Epic Games/UE_4.25/Engine/Plugins/Compositing/OpenCVLensDistortion/" 플러그인 참고.
먼저 Source 디렉토리를 보자.
Source의 하위로 다음과 같은 디렉토리들이 있다.
Source/OpenCVHelper
Source/OpenCVLensCalibration
Source/OpenCVLensDistortion
Source/ThirdParty
이 디렉토리들은 각각 하나의 모듈들을 담고 있다.
이들이 모듈이라는것을 확실하게 확인하고싶다면 내부에 .Build.cs 파일이 존재하는지 확인해보면 된다.
엔진에서는 일반적으로 외부 라이브러리(OpenCV 영상처리 라이브러리)를 사용하고 싶은 경우, 지금과 같은 구조로 플러그인을 설계한다.
하나의 플러그인 내에, 외부 라이브러리만 포함하는 ThirdParty모듈을 따로 생성하여, 그 내부에 lib, header와 같은 파일들을 넣고, 이 ThirdParty 모듈을 엔진 내에서 편하게 이용하기 위한 모듈들을 기능 단위로 분리하여 설계한다.
ThirdParty 모듈은 .Build.cs 파일에 필요한 라이브러리와 헤더파일을 플랫폼, 경로, 메크로에 맞게 로드하여, 외부 라이브러리를 손쉽게 포함할 수 있도록 한다.
Source/ThirdParty/OpenCV/OpenCV.Build.cs
// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using UnrealBuildTool;
public class OpenCV : ModuleRules
{
public OpenCV(ReadOnlyTargetRules Target) : base(Target)
{
Type = ModuleType.External;
if (Target.Platform == UnrealTargetPlatform.Win64 ||
Target.Platform == UnrealTargetPlatform.Win32)
{
string PlatformDir = Target.Platform.ToString();
string IncPath = Path.Combine(ModuleDirectory, "include");
PublicSystemIncludePaths.Add(IncPath);
string LibPath = Path.Combine(ModuleDirectory, "lib", PlatformDir);
string BinaryPath = Path.GetFullPath(Path.Combine(ModuleDirectory, "../../../Binaries/ThirdParty", PlatformDir));
string LibName = "opencv_world331";
if (Target.Configuration == UnrealTargetConfiguration.Debug &&
Target.bDebugBuildsActuallyUseDebugCRT)
{
LibName += "d";
}
PublicAdditionalLibraries.Add(Path.Combine(LibPath, LibName + ".lib"));
string DLLName = LibName + ".dll";
PublicDelayLoadDLLs.Add(DLLName);
RuntimeDependencies.Add(Path.Combine(BinaryPath, DLLName));
PublicDefinitions.Add("WITH_OPENCV=1");
PublicDefinitions.Add("OPENCV_PLATFORM_PATH=Binaries/ThirdParty/" + PlatformDir);
PublicDefinitions.Add("OPENCV_DLL_NAME=" + DLLName);
}
else // unsupported platform
{
PublicDefinitions.Add("WITH_OPENCV=0");
}
}
}
ThirdParty 모듈을 통해 포함된 라이브러리와 헤더는, 이후 다른 기능 담당 모듈들에서 쉽게 참조하여 사용할 수 있다.
Source/OpenCVLensDistortion/OpenCVLensDistortion.Build.cs 일부 코드
// Copyright Epic Games, Inc. All Rights Reserved.
namespace UnrealBuildTool.Rules
{
public class OpenCVLensDistortion : ModuleRules
{
public OpenCVLensDistortion(ReadOnlyTargetRules Target) : base(Target)
{
// ...
PublicDependencyModuleNames.AddRange(
new string[]
{
"OpenCVHelper",
"OpenCV",
}
);
//...
}
}
}
이렇게 ThirdParty (OpenCV) 모듈을 포함한 OpenCVLensDistortion 모듈은 OpenCV의 라이브러리 코드를 언리얼 환경에 속에서 이용할 수 있다.
Source/OpenCVLensDistortion/Private/OpenCVLensDistortionParameters.cpp 일부 코드
// ...
#if WITH_OPENCV
cv::Mat FOpenCVLensDistortionParameters::CreateOpenCVCameraMatrix(const FVector2D& InImageSize) const
{
cv::Mat CameraMatrix = cv::Mat::eye(3, 3, CV_64F);
CameraMatrix.at<double>(0, 0) = F.X * InImageSize.X;
CameraMatrix.at<double>(1, 1) = F.Y * InImageSize.Y;
CameraMatrix.at<double>(0, 2) = C.X * InImageSize.X;
CameraMatrix.at<double>(1, 2) = C.Y * InImageSize.Y;
return CameraMatrix;
}
#endif
// ...
시행착오
1. Module 내부의 코드를 외부 다른 모듈에서 사용하는 경우, 코드가 Public 폴더 내부에 있어야한다.
그렇지 않은 경우 컴파일 에러가 발생할 수 있다.
2. 한 모듈에서 다른 모듈의 class를 이용하는 경우, <ModuleName>_API 메크로를 붙여주어야 한다.
이 메크로는 UBT에서 자동으로 생성시켜주는 메크로이고, dll을 import할 수 있도록 도와주는 메크로이다.
메크로가 붙지 않은 상태로 class를 이용하는 경우 lnk error가 발생할 수 있다.
ex)
// NewModule
// 외부 모듈에서 이 class를 사용하고 싶은경우
class NEWMODULE_API MyClass
{
// ...
}
3. ThirdParty 모듈에서는 언리얼 코드를 사용할 수 없다.
4. dll이 정상적으로 로드되지 않은 경우 생기는 문제
아래 코드는 dll을 delay로드하는 과정에서 dll이 정상적으로 로드되지 않은 경우 예외가 발생할 수 있는 지점이다.
if (hmod == 0) {
dli.dwLastError = ::GetLastError();
if (__pfnDliFailureHook2) {
// when the hook is called on LoadLibrary failure, it will
// return 0 for failure and an hmod for the lib if it fixed
// the problem.
//
hmod = HMODULE((*__pfnDliFailureHook2)(dliFailLoadLib, &dli));
}
if (hmod == 0) {
PDelayLoadInfo rgpdli[1] = { &dli };
DloadReleaseSectionWriteAccess();
RaiseException(
VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND),
0,
1,
PULONG_PTR(rgpdli)
);
// If we get to here, we blindly assume that the handler of the exception
// has magically fixed everything up and left the function pointer in
// dli.pfnCur.
//
return dli.pfnCur;
}
}
먼저 dll이 로드되지 않았는지 정확하게 확인하기 위해서는 visual studio f5로 디버깅모드 실행 후,
메뉴바에서 디버그 > 창 > 모듈 을 누르고 모듈 윈도우에 의심되는 dll을 검색해보면 된다.
보통 문제가 생겼다는 것은 dll이 로드되지 않았거나, 다른 버전의 dll이 로드된 경우이다.
dll이 정상적으로 로드되지 않았을 경우,
메인 모듈의 Binaries 폴더에 로드되길 원하는 dll이 포함되어 있는지 확인한다.
로드되어야할 dll이 참조해야할 다른 dll들이 있는지 확인한다.
(보통 운영체제 공용 라이브러리들에 대해 참조관계를 갖고있는데, 가끔 공용 라이브러리중에 누락된 라이브러리가 있는 경우 문제가 생길 수 있다. 이런 경우 내 PC에서는 잘 동작하지만, 다른 사람의 PC에서는 잘 돌지 않는다.)
다른 버전의 dll이 로드된 경우,
가끔 새로 추가한 dll이 이미 엔진에서 사용하고 있는 dll인 경우가 있다. 운영체제에서 같은 dll을 로드하지는 못하기 때문에, 이런 경우 엔진의 dll과 새로 추가하려는 dll의 버전이 다르다면 문제가 생길 수 있다.
일단 이런 상황이 맞는지 확인하기 위해 모듈 윈도우에 dll을 검색하여 로드한 경로를 확인해본다.
예상한 dll 위치가 아닌 다른 경로에서 로드되었다면 문제가 있는 것이고, 이 상황을 해결하려면, dll의 버전에 맞게 다른 종속 dll들도 대체해야하는 번거로운 작업이 필요할 것이다.
5. Extras 폴더 내부의 모든 파일을 Binaries 폴더로 복사하는 예제
ModuleDirectory - 현재 스크립트 PlubginExample.Build.cs 파일이 존재하는 디렉토리 위치
$(BinaryOutputDir) - 최종 Binaries 폴더 위치
다음 코드를 통해 inputPath에 있는 파일을 outputPath로 복사할 수 있다.
특히 dll과 같은 런타임에 필요한 파일들을 포함시키기 위해 많이 사용한다.
RuntimeDependencies.Add(outputPath, inputPath);
Build.cs 는 다음과 같이 작성하면 된다.
// Copyright Epic Games, Inc. All Rights Reserved.
using System.IO;
using UnrealBuildTool;
public class PluginExample : ModuleRules
{
public PluginExample(ReadOnlyTargetRules Target) : base(Target)
{
//..
string TargetDir = Path.Combine(ModuleDirectory, "../../Extras");
string[] ExtraFiles = Directory.GetFiles(TargetDir, "*.*", SearchOption.AllDirectories);
string OutDirectory = "$(BinaryOutputDir)";
foreach (var path in ExtraFiles)
{
string relativePath = "Extras" + path.Replace(TargetDir, "");
string finalPath = Path.Combine(OutDirectory, relativePath);
RuntimeDependencies.Add(finalPath, path);
System.Console.WriteLine("TargetDir : " + TargetDir);
System.Console.WriteLine("path : " + path);
System.Console.WriteLine("relativePath : " + relativePath);
System.Console.WriteLine("finalPath : " + finalPath);
}
}
}
6. ThirdParty의 dll을 옮기는 과정에서 생기는 이슈
새로운 모듈을 개발하다보면 모듈에서 필요로 하는 라이브러리의 dll을 이용해야하는 상황이 생긴다. 언리얼에서 이러한 dll을 정상적으로 이용하도록 하려면 해주어야하는 작업이 있다.
다음은 memcached라는 라이브러리를 이용하는 언리얼 모듈을 만드는 예제이다.
먼저 ThirdParty 내부 적당한 위치에 dll을 위치시킨다.
../Plugins/<ModuleName>/Source/ThirdParty/libmemcached/bin
Build.cs에서 dll 파일이 잘 스테이징 될 수 있도록 코드를 작성한다.
RuntimeDependencies에 dll 파일을 Add하여 해당 파일이 잘 스테이징되도록 할 수 있다.
일반적으로 Plugins/Binaries에만 스테이징 시킨 후, c++ 모듈에서 따로 FPlatformProcess::GetDllHandle 함수를 이용하여 dll을 필요한 시점에 로드하는 방법이 있고, 또 다른 방법으로는 최종 바이너리 아웃풋 파일 옆에 dll이 스테이징되도록 하는 방법이 있다. 후자의 경우 $(TargetOutputDir) 위치에 dll을 스테이징 시키면 된다.
언리얼 공식 문서 참고
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
using System.IO;
using System.Diagnostics;
using System;
public class Memcached : ModuleRules
{
public Memcached(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
if (Target.Platform == UnrealTargetPlatform.Win64)
{
string ThirdPartyDir = Path.Combine(PluginDirectory, "Source", "ThirdParty");
string IncPath = Path.Combine(ThirdPartyDir, "libmemcached", "include");
string LibPath = Path.Combine(ThirdPartyDir, "libmemcached", "lib");
string BinaryPath = Path.Combine(ThirdPartyDir, "libmemcached", "bin");
string PluginDir = PluginDirectory;
string[] LibNames = new string[]
{
"hashkit", "memcached", "memcachedprotocol", "memcachedutil", "p9y"
};
string[] DllNames = new string[]
{
"hashkit", "memcached", "memcachedprotocol", "memcachedutil"
};
PrivateIncludePaths.Add(IncPath);
foreach (string Name in LibNames)
{
PublicAdditionalLibraries.Add(Path.Combine(LibPath, Name + ".lib"));
}
foreach (string Name in DllNames)
{
string DLLName = Name + ".dll";
string DLLPath = Path.Combine(BinaryPath, DLLName);
string DestinationDir = Path.Combine(PluginDir, "Binaries", Target.Platform.ToString());
CopyToBinaries(DestinationDir, DLLPath);
string DestinationFilePath = Path.Combine(DestinationDir, DLLName);
// stage dll files to Plugins/Memcached/Binaries folder
RuntimeDependencies.Add(DestinationFilePath, StagedFileType.NonUFS);
// stage dll files to ProjectName/Binaries folder
RuntimeDependencies.Add($"$(TargetOutputDir)/{DLLName}", DLLPath);
}
}
}
private void CopyToBinaries(string DestinationDir, string SourceFilePath)
{
string SourceFileName = Path.GetFileName(SourceFilePath);
if (!Directory.Exists(DestinationDir))
Directory.CreateDirectory(DestinationDir);
string DestinationFilePath = Path.Combine(DestinationDir, SourceFileName);
Console.WriteLine($"CopyToBinaries DestinationDir : {DestinationDir} SourceFilePath : {SourceFilePath} DestinationFilePath : {DestinationFilePath}");
if (!File.Exists(DestinationFilePath))
File.Copy(SourceFilePath, DestinationFilePath, true);
}
}
'게임 엔진 > Unreal' 카테고리의 다른 글
[Unreal] [Networking] TCP Socket을 이용한 Client 개발 (0) | 2022.08.27 |
---|---|
[Unreal] [Example] Editor에 Asset 생성 및 저장 (7) | 2021.08.10 |
[Unreal] Android 디버깅 방법들 (0) | 2021.03.18 |
[Unreal] 언리얼로 Google Play 결제 시스템 이용하기 (3) | 2021.03.04 |
[Unreal] 언리얼 안드로이드 프로젝트 Google Play에 출시하기 (0) | 2021.02.09 |