UE 5.5 에서 config ini 변화: 세이브, 로드, 플러그인

결론

  • 플러그인의 경우
    • 플러그인 폴더에 Config 폴더를 만들고, 거기에 Default{PluginName}.ini 파일을 만드세요.
      • 플러그인은 플러그인의 이름만 ini 파일명으로 사용할 수 있습니다.
      • Default{PluginName}.ini 는 비어있어도 괜찮습니다. 특별한 내용이 필요하지 않습니다.
      • UCLASSConfig 지정자에는 PluginName 을 사용하면 됩니다.
  • 모듈의 경우,
    • 로드가 되게 하려면
      • 프로젝트에 Config 폴더가 있을겁니다. 거기에 Default ini 파일이 있어야 합니다. 없다면 만드세요.
    • 저장이 되게 하려면
      • Default ini 파일에, [SectionsToSave] 라는 섹션이 있어야 합니다. 없으면 추가하세요.
      • [SectionsToSave] 섹션에, bCanSaveAllSections=true 를 추가하세요. 이 방법을 쓰지 않고, 세이브를 허용할 클래스를 명시적으로 지정하는 방법도 있습니다.
  • 플러그인에서 ini 파일의 이름으로 “알려진” 카테고리를 사용할 수 있긴 합니다. 다만, 이 경우 모듈의 Default ini 파일에서 플러그인에 속한 클래스를 저장할 수 있게 설정해야 합니다.

앞서서

관련 자료

공식문서

위키

Unreal community wiki: Config Files, Read & Write to Config Files

버전

제목에도 나와있듯, 이 문서는 Unreal Engine 5.5 버전을 기준으로 합니다.

목표

이 문서는 별도의 c++ 코드 없이, UCLASS 의 지정자와 텍스트 ini 파일에 대한 작업만으로 진행하는 것을 전제로 합니다.

즉, 우리가 c++ 수준에서 호출할 함수는 단 하나, UObject::LoadConfig GitHub 링크 뿐입니다.

이것은 제가 엔진의 이전 버전들에서도 잘 동작할 수 있는 코드와 리소스를 원하기 때문입니다.

하지만 자세한 설명에서는 엔진 소스 코드에 대한 여러가지 방면에서의 언급이 있을 것 같습니다.

용어

엔진 코드에서는 ini 파일이라고도 하고 Config 파일이라고도 하는데, 일단 아래부터는 ini 파일이라고 부르겠습니다.

샘플 프로젝트

GitHub 에 예제 프로젝트를 올려놓았습니다: 링크

엔진의 자세한 동작을 디버깅을 통해 알고 싶을 때 사용하기 위해 만들었습니다.

옛날에는

5.3 까지

1
2
3
4
5
6
UCLASS(Config = SomeConfigFileName)
class SOMEPROJECTNAME_API USomeClassName : public UObject
{
  UPROPERTY(Config)
  int32 ValueInObject;
}

이러면 SomeConfigFileName.ini 라는 파일이 생성되고, 여기에 Config 내용이 저장되었었습니다. 위의 예시에서는 ValueInObject 의 값이 저장됩니다.

5.4 에서

5.4 에서 UCLASS 의 config 지정자를 통한 ini 파일명 사용자 지정에 약간의 제한이 생겼다는 내용의 포스트를 쓴 적이 있습니다.

하지만, 5.5부터는 ini 파일명에 UserEditor 를 추가하는 방법을 사용할 수가 없습니다. 이렇게 하면, 5.5 부터는 저장은 되는데 로드가 안됩니다.

언리얼 엔진 5.5 부터

모듈용 ini 파일이냐, 플러그인용 ini 파일이냐에 따라 다릅니다.

만약 모듈이나 플러그인 같은 단어에 익숙하지 않으시다면, 모듈은 플러그인이 아닌 것이라고 생각하시고 모듈 부분만 보셔도 됩니다.

모듈의 경우

엔진이 제공하는, “알려진” 카테고리(=파일명)를 사용할 경우

“알려진” 카테고리(=파일명)?
  • 예시: Engine, Game, Input
  • 전체 내용은 여기를 확인하세요.
방법
  1. 프로젝트 폴더 바로 아래의 Config 폴더에 이미 Default ini 파일이 있는지 확인합니다. 만약 없다면 메모장 등을 통하여 만들어야 합니다.
    • 파일명은 Default{카테고리 이름}.ini입니다.
      • 예를 들어, Game 카테고리를 사용한다면, DefaultGame.ini입니다.
  2. 해당 ini 파일에 [SectionsToSave] 섹션이 있는지 확인합니다. 없으면 추가합니다. 아마 없을 겁니다.
  3. [SectionsToSave]섹션에 bCanSaveAllSections=true를 추가합니다.
    • 이것을 추가하는 이유는, 모듈의 경우, bCanSaveAllSections 의 기본값이 false이기 때문입니다. 모든 클래스의 저장을 허용하기 위해서는 이 항목의 값을 true로 바꿔야 합니다. 자세한 사연은 아래 별도 챕터에서 말씀드리겠습니다.

결과는 아래와 같은 형태가 됩니다.

1
2
[SectionsToSave]
bCanSaveAllSections=true

cpp 코드는 이런 모양이 될 것입니다. Game카테고리를 사용한다고 가정하겠습니다. 이전 버전에서 Config 를 사용하셨다면, 익숙한 내용일 것입니다.

1
2
3
UCLASS(Config = Game)
class SOMEPROJECT_API USomeClassAlpha : public UObject
{...}

엔진이 제공하는 카테고리 외의 파일명을 사용하고 싶은 경우

앞 챕터의 경우와 비교할 때, 언제나 Default ini 파일을 추가해야 한다는 점만 다릅니다.

  1. 프로젝트 폴더 바로 아래의 Config 폴더에 Default ini 파일을 만듭니다.
    • 만약 사용하고 싶은 파일명이 MyConfigCategory.ini라고 한다면, DefaultMyConfigCategory.ini 로 만듭니다.
  2. 해당 파일을 열어, [SectionsToSave] 섹션과 bCanSaveAllSections=true 를 추가합니다.

결과는 역시나 이런 모양이 됩니다.

1
2
[SectionsToSave]
bCanSaveAllSections=true

cpp 코드는 이렇게 되겠지요.

1
2
3
UCLASS(Config = MyConfigCategory)
class SOMEPROJECT_API USomeClassAlpha : public UObject
{...}

플러그인의 경우

  1. 플러그인 폴더에 Config 폴더를 만듭니다.
  2. 해당 폴더에 Default{플러그인 이름}.ini 를 만듭니다.
    • 예를 들어, 플러그인 이름이 MyPlugin 이라고 한다면, DefaultMyPlugin.ini 가 됩니다.

플러그인의 경우 이걸로 끝입니다. ini 파일의 내용은 비어있어도 됩니다.

플러그인의 경우, 모듈과 달리 [SectionsToSave] 섹션과 bCanSaveAllSections=true 가 없어도, 모든 클래스에 대해서 저장과 불러오기가 됩니다.

이것은 플러그인의 경우 bCanSaveAllSections 의 기본값이 모듈과는 반대로 true이기 때문입니다. 역시 자세한 사연은 아래 별도 챕터에서 말씀드리겠습니다.

그래서, 그냥 Default{플러그인 이름}.ini 파일만 Config 폴더에 있으면 됩니다.

대신 플러그인에는 다른 유형의 제한이 있습니다. 현재로서는, 별도의 c++ 코드 추가가 없다면, 플러그인에서는 “알려진” 카테고리가 아니고 플러그인의 이름도 아닌 ini 파일을 사용하는 것이 불가능합니다.

UCLASS 지정자의 사용은, 위에서 말한 대로 플러그인의 이름만 사용할 수 있다는 것을 빼고는, 다른 점이 없습니다. 즉 아래와 같은 모양이 됩니다.

1
2
3
UCLASS(Config = MyPlugin)
class SOMEPROJECT_API USomeClassAlpha : public UObject
{...}

고급: ini 파일에 기록될 클래스를 명시적으로 추가

ini 파일의 [SectionsToSave] 섹션에 기록을 허용할 클래스를 명시적으로 추가해 줄 수 있습니다. 이 방식을 사용하면, bCanSaveAllSections 의 현재 값이 false 여도 해당 클래스는 Config 로서 저장됩니다.

단, 클래스 이름을 쓸 때, 접두어 U는 빼고 추가해야 합니다!

예를 들어, 모듈 MyModuleName 에 속한 USomeClass 만 ini 에 기록하고 싶다면, 아래와 같이 하면 됩니다.

1
2
[SectionsToSave]
+Section=/Script/MyModuleName.SomeClass

플러그인의 경우에도 마찬가지로 클래스를 추가해줄 수 있습니다. 다만, 플러그인의 경우 bCanSaveAllSections 의 기본값이 모듈과는 반대로 true이기 때문에, 만약 지정한 클래스 외의 클래스의 저장을 막고 싶다면, [SectionsToSave]bCanSaveAllSections=false 를 함께 추가해줘야 합니다.

예를 들면 아래와 같습니다. 역시 클래스 이름에서 접두어 U를 빼는 것에 유의해주세요.

1
2
3
[SectionsToSave]
bCanSaveAllSections=false
+Section=/Script/MyPluginName.SomeClass

익숙하지 않은 분들을 위한 추가 설명

  • 모듈이 뭔지 모르시면, 이 챕터에 한해서, 프로젝트 이름이라고 생각하셔도 괜찮습니다.
  • 추가한 +Section=/Script/MyPluginName.SomeClass 부분에 대한 추가 설명
    • 공식문서에 적혀있지만, Section 앞의 + 표시는 이것을 배열에 추가한다는 의미입니다.
    • /Script/ 부분은, 아주 대강 설명하자면, 해당 내용이 c++ 코드에 관한 것이라는 의미입니다.
    • /Script/ 뒤에 따라오는 이름은 모듈 이름 혹은 플러그인 이름입니다.
    • 그리고 그 뒤에 클래스에서 접두어 U 를 뺀 클래스의 이름이 따라오게 됩니다.

자세한 사연

과격한 요약

이 챕터에서 다룰 내용들을 아주 거칠게 요약하겠습니다. 작고 다양한 세부적인 요소들과 예외들이 존재하므로, 이 요약이 언제나 옳은 것은 아니라는 것을 기억해주세요.

  • 로드
  • 세이브
    • 모듈의 ini 파일은 /Engine/Config/Base.ini 를 상속합니다. 이 파일의 [SectionsToSave] 섹션의 bCanSaveAllSections 의 값은 false 입니다.
    • 플러그인의 ini 파일은 /Engine/Config/PluginBase.ini 를 상속합니다. 이 파일의 [SectionsToSave] 섹션의 bCanSaveAllSections 의 값은 true 입니다.
    • [SectionsToSave] 섹션의 bCanSaveAllSections 의 값이 false 이면, 저장될 클래스를 명시적으로 지정해주지 않을 경우 저장이 안됩니다.
    • [SectionsToSave] 섹션의 bCanSaveAllSections 의 값이 true 이면, 모든 클래스가 저장됩니다.

알아둬야 하는 개념과 구조

BaseIniName 와 여러 종류의 ini 파일들

Default ini 파일

언리얼 엔진에서 새 프로젝트를 만들면, 프로젝트 폴더의 Config 폴더 아래에 다음과 같은 파일들이 생성됩니다.

  • DefaultEditor.ini
  • DefaultEngine.ini
  • DefaultGame.ini
  • DefaultInput.ini

이것들을 앞으로 Default ini 파일 이라고 부르겠습니다.

BaseIniName?

Default ini 파일 에서 Default부분을 뺀 부분, 즉 Engine, Game, Input… 부분을 공식 문서에서는 “카테고리"라고 부릅니다.
코드에서는 변수나 파라메터 이름으로 ‘‘‘BaseIniName’’’ 를 사용하고 있습니다. 이 문서에서 우리의 관심사는 파일명이므로, 이 문서에서는 BaseIniName 이란 표현을 사용하도록 하겠습니다.

최종 ini 파일

만약 프로젝트를 생성하고 한 번이라도 에디터가 켜고 껐다면,

프로젝트 폴더의 /Saved/Config/WindowsEditor/ 폴더에도 아래처럼 ini 파일들이 몇 개 있을 겁니다.

  • Editor.ini
  • EditorPerProjectUserSettings.ini
  • GameUserSettings.ini

여기는 파일명에서 Default 부분이 빠져있고, BaseIniName 으로만 되어있죠.

바로 여기가 여러분이 저장하고 로드하는 데이터가 존재하는 곳입니다. 이것을 최종 ini 파일 이라고 부르겠습니다.

패키징시 최종 ini 파일의 위치

참고로 패키징을 했다면, 최종 ini 파일 의 경로는 다음과 같은 위치가 됩니다.

Shipping 이 아닐 경우, C:/MyPackageFolder/ 에서 실행했다면

1
C:/MyPackageFolder/Saved/Config/Windows/MyIniNameInModule.ini

Shipping 버전일 경우

1
C:/Users/MyUserName/AppData/Local/MyProjectName/Saved/Config/Windows/MyIniNameInModule.ini

또한, 엔진 소스 코드를 보다 보면, FinalCombinedLayers 같은 변수명이 나오는데, 이것은 최종 ini 파일 과는 전혀 다른 것이므로, 혼동하지 말아주세요.

Base ini 파일

사실 언리얼 엔진의 폴더에 보면 더 많은 ini 파일들이 있습니다.

  • 만약 당신이 런쳐 버전을 사용하고 있고,
  • 엔진의 설치 폴더를 바꾸지 않았다면,

C:/Program Files/Epic Games/UE_5.5/Engine/Config 폴더에서 많은 수의 ini 파일들을 보실 수 있습니다. (제 경우 34개네요.)

그 중에는 BaseEditor.ini 도 있습니다. Editor.ini 앞에 Base 가 붙어있죠.

이런 종류의 것을 앞으로 Base ini 파일 이라고 부르겠습니다.

BaseIniName 과 다릅니다. Base ini 파일 는, BaseIniName 앞에 문자열 Base가 붙은 ini 파일입니다.

(네. 이 동네가 좀 헷갈립니다.)

ini 파일의 Hierarchy

ini 파일이 합쳐지는 방식, 간단히

아주 간략하게 말하자면, BaseIniNameEditor 이고 최종 ini 파일Editor.ini 인 파일의 내용을 결정하기 위해선, 아래의 파일들이 관여를 하게 됩니다.

위의 순서대로 파일을 읽어서 BaseIniName Editor 에 대한 내용을 완성합니다.

단, 같은 항목에 대해 서로 다른 값이 존재한다면, 뒤의 값이 앞에 있던 값을 덮어 씁니다.

(참고로, 모든 값이 저장되지 않고, ClassDefaultObject 의 디폴트값에서 내용이 바뀐 내용만 기록됩니다.)

그런데 이게 다냐면, 그렇지 않습니다.

ini 파일의 Hierarchy - 모듈의 경우

디버깅을 통해 ini 파일을 읽는 곳들의 목록을 나열해보자면, 아래와 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[0] = L"../../../Engine/Config/Base.ini"
[1] = L"../../../Engine/Config/BaseMyIniNameInModule.ini"
[2] = L"../../../Engine/Restricted/NotForLicensees/Config/BaseMyIniNameInModule.ini"
[3] = L"../../../Engine/Restricted/NoRedist/Config/BaseMyIniNameInModule.ini"
[4] = L"../../../Engine/Restricted/LimitedAccess/Config/BaseMyIniNameInModule.ini"
[5] = L"../../../Engine/Config/VulkanPC/BaseVulkanPCMyIniNameInModule.ini"
[6] = L"../../../Engine/Config/Windows/BaseWindowsMyIniNameInModule.ini"
...
[19] = L"../../../Engine/Restricted/LimitedAccess/Platforms/VulkanPC/Config/BaseVulkanPCMyIniNameInModule.ini"
[20] = L"../../../Engine/Restricted/LimitedAccess/Platforms/Windows/Config/BaseWindowsMyIniNameInModule.ini"
[21] = L"E:/GitHubDesktop/MyProjectName/Config/DefaultMyIniNameInModule.ini"
[22] = L"E:/GitHubDesktop/MyProjectName/Restricted/NotForLicensees/Config/DefaultMyIniNameInModule.ini"
[23] = L"E:/GitHubDesktop/MyProjectName/Restricted/NoRedist/Config/DefaultMyIniNameInModule.ini"
...
[75] = L"E:/GitHubDesktop/MyProjectName/Restricted/LimitedAccess/Platforms/VulkanPC/Config/GeneratedVulkanPCMyIniNameInModule.ini"
[76] = L"E:/GitHubDesktop/MyProjectName/Restricted/LimitedAccess/Platforms/Windows/Config/GeneratedWindowsMyIniNameInModule.ini"
[77] = L"C:/Users/MyUserName/AppData/Local/Unreal Engine/Engine/Config/UserMyIniNameInModule.ini"
[78] = L"C:/Users/MyUserName/OneDrive/문서/Unreal Engine/Engine/Config/UserMyIniNameInModule.ini"
[79] = L"E:/GitHubDesktop/MyProjectName/Config/UserMyIniNameInModule.ini"

80군데에서 읽어요.

엔진의 기본값이냐, 프로젝트의 기본값이냐, 프로젝트의 저장내용이냐 말고도, 플랫폼에 따라서도 ini 파일의 내용을 바꿔서 지정할 수 있습니다. 엔진을 어떤 라이센스를 통해 사용하고 있는지에 따라 달라지는 내용으로 추측되는 부분도 있죠. 그 외에, 엔진에서 사용하는 중간 단계의 파일들로 추측되는 항목들도 보입니다.

이 부분이 만들어지는 부분의 소스 코드를 보고 싶다면, 이 각주를 참고하세요 1

Hierarchy?

위에 예시로 든 목록의 엔진에서의 변수명은 Hierarchy 입니다. 그래서 이 문서에서는 이 목록을 Hierarchy 라고 부르겠습니다. (참고로 FConfigBranch 클래스에 있습니다. 그리고 엄밀히 말하면, Hierarchy의 타입은 TMap<int32, FString> 를 상속해 만들어지는 클래스 FConfigFileHierarchy 입니다.)

ini 파일의 Hierarchy - 플러그인의 경우

플러그인의 경우는 규칙은 같은데, 내용에 차이가 있습니다.

플러그인에 속한 ini 파일을 읽을 때, 로드를 시도하는 Hierarchy 의 목록은 아래와 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[0] = L"../../../Engine/Config/PluginBase.ini"
[1] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/BaseMyIniNameInPlugin.ini"
[2] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/DefaultMyIniNameInPlugin.ini"
[3] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/VulkanPC/VulkanPCMyIniNameInPlugin.ini"
[4] = L"E:/GitHubDesktop/MyProjectName/Plugins/ConfigTestEnginePlugin/Config/Windows/WindowsMyIniNameInPlugin.ini"
[5] = L"E:/GitHubDesktop/MyProjectName/Config/DefaultMyIniNameInPlugin.ini"
[6] = L"E:/GitHubDesktop/MyProjectName/Config/VulkanPC/VulkanPCMyIniNameInPlugin.ini"
[7] = L"E:/GitHubDesktop/MyProjectName/Config/Windows/WindowsMyIniNameInPlugin.ini"
[8] = L"E:/GitHubDesktop/MyProjectName/Platforms/VulkanPC/Config/VulkanPCMyIniNameInPlugin.ini"
[9] = L"E:/GitHubDesktop/MyProjectName/Platforms/Windows/Config/WindowsMyIniNameInPlugin.ini"

모듈에 비해 훨씬 짧습니다!!!

이 부분이 만들어지는 부분의 소스 코드를 보고 싶다면, 이 각주를 참고하세요 2

Hierarchy 와 최종 ini 파일은 별개

그런데… 뭔가 빠진 것 같죠. 네. 위의 Hierarchy 에는, 제일 중요한 최종 ini 파일이 없습니다.

위의 예시대로라면, BaseIniName MyIniNameInModule 에 대한 최종 ini 파일 파일은, 프로젝트 폴더인 E:/GitHubDesktop/MyProjectName/ 아래의 /Saved/Config/WindowsEditor/ 폴더에, 모듈일 경우 MyIniNameInModule.ini 로서, 플러그인일 경우 MyIniNameInPlugin.ini 로서 있어야 합니다.

그리고 우리의 에디터 혹은 PIE 게임은 이 ini 파일에 환경설정을 저장합니다.

위의 예시대로라면, 모듈용 ini 와 플러그인용 ini 의 경로는 아래 항목들 중 각각 하나가 될 것입니다. (위치에 대해서는 앞에서 설명했었습니다.)

1
2
3
4
5
6
7
8
9
//Module
E:/GitHubDesktop/MyProjectName/Saved/Config/WindowsEditor/MyIniNameInModule.ini
C:/MyPackageFolder/Saved/Config/Windows/MyIniNameInModule.ini
C:/Users/MyUserName/AppData/Local/MyProjectName/Saved/Config/Windows/MyIniNameInModule.ini

//Plugin
E:/GitHubDesktop/MyProjectName/Saved/Config/WindowsEditor/MyIniNameInPlugin.ini
C:/MyPackageFolder/Saved/Config/Windows/MyIniNameInPlugin.ini
C:/Users/MyUserName/AppData/Local/MyProjectName/Saved/Config/Windows/MyIniNameInPlugin.ini

일단 Hierarchy최종 ini 파일 이 별개라는 것을 기억해둡시다.

이 부분은 뒤에 언급할 로드가 되지 않는 이유와 관계가 있습니다.

ini 가 저장되지 않는 이유

Hierarchy 의 ini 파일들과 최종 ini 파일를 전부 합쳐본 결과, [SectionsToSave] 섹션의 bCanSaveAllSections 항목의 값이 false 라서 그렇습니다.

ini 저장 동작

일단 코드의 흐름상의 규칙은 이렇습니다. 엔진에는 ini 파일의 내용을 담게되는 FConfigFile 라는 클래스가 있습니다. 그리고,

  • FConfigFilebCanSaveAllSections 값이 true 이면
    • 해당 ini 파일에는 어떤 클래스든 기록될 수 있습니다.
  • FConfigFilebCanSaveAllSections 값이 false 이면
    • 해당 ini 파일에는 [SectionsToSave]에 명시적으로 지정한 클래스만 저장할 수 있습니다.
      • 만약 저장할 때 [SectionsToSave]에 명시적으로 지정된 클래스가 아니면, 해당 클래스에 대한 내용을 삭제합니다.

FConfigFile 의 생성시 bCanSaveAllSections 의 디폴트 값은 true 입니다. 그리고 생성 직후 ini 파일에서 읽어온 bCanSaveAllSections 의 값을 가져와 여기에 씁니다. (사실은 약간의 추가 처리가 있는데, 이것은 이 글의 뒷 부분에서 다루겠습니다.)

이제 모듈과 플러그인 별로, 좀 더 자세한 내용을 봅시다.

모듈에서 ini 가 저장되지 않는 이유

이것은 엔진의 /Engine/Config/Base.ini 를 열어보면 알 수 있습니다. 그냥 거기에 아래와 같이 쓰여있습니다. (실제로 열어보면 코멘트들과 다른 내용들도 같이 있는데, 여기서는 생략했습니다.)

1
2
[SectionsToSave]
bCanSaveAllSections=false

모든 모듈의 ini 파일은 이 내용을 상속받습니다. 왜냐하면, 모듈에서 Hierarchy 의 첫 항목은 언제나 /Engine/Config/Base.ini 이기 때문입니다.

그러므로, 모듈의 ini 파일에서 뭔가를 저장하려면,

  • Hierarchy 상에서 후 순위에서,
    • [SectionsToSave] 섹션의 bCanSaveAllSections 의 값을 true 로 덮어 써주거나,
    • 명시적으로 클래스의 이름을 추가해줘야 합니다.
      • 클래스 이름을 명시적으로 추가하는 방법은 앞에서 설명한 바 있습니다.

혹시나 하는 마음에 최종 ini 파일 에다가 bCanSaveAllSections=true를 추가해봐서 테스트를 해봤습니다. 제대로 동작하는 것을 보았습니다. 이 경우, Hierarchy 상에서 앞의 파일들로부터 물려받은 bCanSaveAllSections 의 값이 false 여도, 저장이 되었습니다.3

플러그인에 bCanSaveAllSections=true 를 추가하지 않아도 저장이 되는 이유

모듈과는 달리, 플러그인의 ini 파일은 모두 엔진의 /Engine/Config/PluginBase.ini 를 상속받습니다. 예상하시는 대로, 내용은 이렇습니다. (역시나 이 파일도 열어보면 코멘트들과 다른 내용들도 같이 있는데, 여기서는 생략했습니다.)

1
2
[SectionsToSave]
bCanSaveAllSections=true

그래서, 모듈과는 반대로 플러그인의 ini 에서 bCanSaveAllSections의 기본값은 true가 됩니다. 만약 플러그인에서 ini 파일에 저장될 클래스를 제한하려면, 이 값을 false로 덮어써줄 필요가 있습니다.

ini 가 로드되지 않는 이유

ini 로드 규칙

  1. ini 를 읽기 전에,
  2. Hierarchy에 있는 경로들 중에서,
  3. Hierarchy의 첫 항목을 제외하고, 실제로 존재하는 파일이 하나도 없다면
  4. 최종 ini 파일 을 로드하지 않습니다.

참고로, 위의 3번 항목에서 언급한 Hierarchy의 첫 항목은, 모듈의 경우 Engine/Config/Base.ini 가 되며, 플러그인의 경우 Engine/Config/PluginBase.ini 가 됩니다. 이 파일이 어떤 파일이냐는 세이브에서는 중요하지만 로드에선 중요하지 않습니다. 다만, 파일의 존재가 항상 보장되며, 동시에 로드된 파일의 수를 세는 데에선 제외된다는 점을 기억할 필요가 있습니다. 즉, 이 첫 항목을 제외하고, 어딘가 항목이 하나 이상 있어야 합니다. 그렇지 않으면 최종 ini 파일는 로드하지 않습니다.

ini 로드 실패에 대한 호출스택은 다음 주석을 참고하세요: 4

모듈의 ini 로드 실패

결과적으로, 모듈의 Hierarchy 상에서 프로젝트에 대하여 고정적으로 파일을 하나 추가해야 한다면, 그건 프로젝트 폴더의 Config 폴더 아래에 배치될 Default ini 파일 입니다. 그래서 이 파일을 추가하지 않았다면, 대부분의 경우 최종 ini 파일는 로드될 수 없습니다.

이론적으로 엔진 폴더에 BaseIniName 에 대한 Base ini 파일을 추가해도 최종 ini 파일이 로드되게 만들 수는 있습니다. 하지만 버전 관리 조건에 아주 특별한 요구사항이 있지 않는 한, 이런 파일은 엔진 폴더보다는 프로젝트 폴더에 존재하는 편이 더 나을 것입니다.

플러그인의 ini 로드 실패

플러그인이 ini 를 읽는 부분은 모듈과 조금 다릅니다.

플러그인에서 ini 파일을 사용할 때, ini 파일의 이름이 “알려진” 카테고리일 경우엔 모듈의 경우와 동일하게 동작합니다.

반면, 플러그인에서, 별도의 c++ 코드를 사용하지 않고, “알려진” 카테고리가 아닌 이름을 쓰는데엔, 다음의 제약이 존재합니다.

  • 플러그인에서는, BaseIniName 가 플러그인의 이름과 동일한 ini 파일만 사용 가능합니다.
  • 또한, 이 ini 파일을 로드하기 위해선, 이 파일에 대한 Default ini 파일을 플러그인 폴더의 Config 폴더에 추가해야 합니다.

예를 들어, 플러그인의 이름이 MyPlugin 이라면,

이것은, 엔진이 시작될 때, 플러그인의 이름을 기반으로 하는 Default ini 파일만 로드하고 있기 때문입니다. 관련한 소스 코드 및 호출스택은 다음 주석을 보시기 바랍니다: 5

더 자세히: GConfig, FConfigCacheIni, FConfigBranch

ini 파일이 엔진에 저장되는 구체적인 장소는 전역 변수인 GConfig 입니다: GitHub 링크 이것의 타입은 FConfigCacheIni 입니다.

FConfigCacheIni 는 내부에 많은 FConfigBranch 항목들을 가지는데, 이 FConfigBranchBaseIniName 에 해당하는 데이터들을 가지고 있습니다. 엔진은 ini 를 로드해서 이 FConfigBranch 에 담아 사용합니다. 그리고 ini 를 저장해야 할 때 이 FConfigBranch의 내용을 파일로 저장합니다. 참고로, 기본값과 달라진 내용만 저장이 됩니다. ClassDefaultObject 와 동일한 값을 가진 항목은 저장되지 않습니다.

기록해두고 싶은 다른 것들

엔진 소스 중 플러그인의 ini 파일을 읽는 예시

엔진 소스에서 플러그인의 ini 파일을 읽는 예시는 ChaosVD 플러그인에서 볼 수 있습니다. GitHub 링크 이 플러그인의 DefaultChaosVD.ini 의 맨 위에는 bCanSaveAllSections=true가 있기 때문에 플러그인에도 bCanSaveAllSections=true 를 추가해줘야 한다는 착각을 할 수 있습니다. 하지만 앞서 말씀드린 이유로, 이 부분은 추가하지 않아도 되는 부분입니다.

bCanSaveAllSections 에 대한 작은 예외

예전 포스트 에서도 다룬 바 있는 부분입니다. 엔진은 ini 파일에 대한 bCanSaveAllSections 의 최종값을 다음과 같은 코드로 최종결정합니다. (코드를 조금 요약했습니다. 전체 코드는 GitHub 링크 에서 보실 수 있습니다.)

1
2
3
4
5
bool bIsUserFile = BaseIniName.Contains(TEXT("User"));
bool bIsEditorSettingsFile = BaseIniName.Contains(TEXT("Editor")) && BaseIniName != TEXT("Editor");

//bLocalSaveAllSections 는 ini 파일에서 읽어온 값입니다.
FinalFile.bCanSaveAllSections = bLocalSaveAllSections || bIsUserFile || bIsEditorSettingsFile;

그러니까, 다음의 경우엔 저장되는 내용을 제한하지 않고 모두 저장합니다.

  • ini file 의 bCanSaveAllSectionstrue 이거나
  • BaseIniNameUser 라는 문자열이 포함되어 있거나
  • BaseIniNameEditor가 아니면서 Editor 라는 문자열이 포함되어 있거나
    • 즉, 예를 들어, Editor 은 안되지만, EditorSettings 는 됩니다.

EditorLayout 의 다른 동작

BaseIniName 중, EditorLayout 는 동작이 좀 특별합니다.

일단, 파일이 저장되는 위치가 다릅니다. 다음 폴더에 저장됩니다.

C:/Users/MyUserName/AppData/Local/UnrealEngine/5.5/Saved/Config/WindowsEditor/EditorLayout.ini

엔진에는, BaseIniName 의 저장과 로드를 담당하는 FLayoutSaveRestore 라는 구조체가 존재합니다. FLayoutSaveRestore 는 에디터의 레이아웃을 저장하고 로드를 하는데 Json 형식을 주로 사용합니다. ini 형식도 사용하긴 하는데, 하위 호환성을 위한 부차적 수단으로만 사용합니다.

그리고, 별도의 c++ 코드가 없는 한, EditorLayout 에 무언가를 저장했을 경우, 저장된 내용은 다음 번 에디터가 꺼지는 시점에서 지워집니다.

사용자가 EditorLayout 에 무언가를 저장했다고 가정하고, 아래의 과정을 따라가봅시다.

로드 6

  • 만약, Json 으로 저장된 에디터 레이아웃이 있으면, Json 으로 저장된 내용을 사용합니다.
    • 이 경우 ini 파일은 읽지 않습니다. 즉, 이 경우 GConfig 내부의 EditorLayout에 대한 ini 의 내용은 비어있는 상태가 됩니다. 우리는 이 경우를 지나간다고 가정합니다.
  • Json 으로 저장된 에디터 레이아웃이 없다면, ini 파일에서 에디터 레이아웃을 읽어 사용합니다.

저장 7

  • Json 과 ini 양 쪽 형태로 모두 저장합니다.
    • 만약, 로드할 떄 Json 형식의 데이터를 사용했다면, GConfig 내부의 EditorLayout에 대한 ini 의 내용은 비어있습니다. 이 빈 내용에 새 에디터 레이아웃의 내용을 추가한 다음, 이것을 EditorLayout 의 ini 파일에 덮어씁니다.

전형적인, 로드를 하지 않은 상태에서 저장을 할 때 일어나는 데이터의 손실입니다.

그러므로, EditorLayout 은 의도된 목적 외의 용도로는 사용하지 마세요. EditorLayout 에 대한 내용은 FLayoutSaveRestore 를 통해서만 수정하는 것이 좋습니다.

ini 로드 실패후, ini 내용의 수정이 없으면, 패키지 빌드에서만 ini 파일이 삭제됨

이 현상은 샘플 프로젝트UCustomButBaseNameHasUser의 Config 가 동작하는 것을 통해 관찰할 수 있습니다.

원래 목적은 ini 파일의 이름에 User 가 들어있으며 동시에 Default ini 파일 이 존재하지 않을 경우를 테스트하는 것이었습니다. 그런데 에디터일 때와 패키지 빌드일 때 동작이 다른 것을 발견했습니다. 의도된대로의 사용 시나리오에서는 발생하지 않겠지만, 유사한 문제를 만났을 때 참고하기 위하여 기록해두고 싶었습니다.

달라진 동작을 유발하는 조건과 달라진 동작 자체의 내용은 다음과 같습니다.

  • BaseIniNameUser 가 들어있는 문자열을 사용하고, Default ini 파일 을 설정하지 않습니다.
  • 에디터 혹은 실행파일을 실행시킨 뒤, 오브젝트의 값을 변화시키고, ini 파일에 내용을 저장합니다.
  • 에디터 혹은 실행파일을 닫습니다.
  • 에디터 혹은 실행파일을 다시 엽니다. 이 때, Default ini 파일 이 없기 때문에 ini 파일은 로드되지 않습니다.
  • 아무것도 하지 않고 에디터 혹은 실행파일을 닫습니다. 그러면 이 때,
    • 에디터의 ini 파일은 이전에 저장된 내용 그대로 존재합니다.
    • 패키지 빌드의 ini 파일은 삭제됩니다.

원인: FConfigFile InMemoryFileDirty 변수의 값

이 현상은, GConfig 의 내부에 존재하는 FConfigBranch 내부의 FConfigFile InMemoryFileDirty 값에 의해 달라집니다.

  • FConfigFileDirty 값은 생성 시점에서 false 입니다.
  • 에디터에서 Dirty 의 값은 ini 파일에 변경이 없었을 경우 false 로 유지됩니다.
  • 하지만, 패키지 빌드에서 Dirty의 값은, 로드하는 타이밍에 true 로 설정됩니다.

ini 파일이 삭제되는 부분

이 차이는, ini 파일의 저장과정에 다음과 같이 영향을 미칩니다.

  • InMemoryFileDirty 값이 false 일 경우, 저장하는 함수 초반에 처리를 종료.
  • InMemoryFileDirty 값이 true 일 경우, 저장할 내용을 계산해본 다음, 그 내용의 길이가 0 이면 해당 ini 파일을 삭제.

이 저장 과정의 차이는 Source/Runtime/Core/Private/Misc/ConfigCacheIni.cppstatic bool SaveBranch(FConfigBranch& Branch) 코드를 보면 알 수 있습니다. GitHub 링크

코드 초반 866 라인 에서 Branch.InMemoryFile.Dirtytrue 이면 처리를 종료하고 함수를 나가고,

코드 후반부 에서, 저장 후 출력내용의 길이가 0 이고 bBuiltStringfalse 일 경우, 파일을 지우는 부분이 존재합니다.

참고로 이 때의 호출 스택은 다음과 같았습니다.

1
2
3
4
5
6
SaveBranch(FConfigBranch &) ConfigCacheIni.cpp:896
FConfigCacheIni::Flush(bool, const FString &) ConfigCacheIni.cpp:3879
FConfigCacheIni::Exit() ConfigCacheIni.cpp:4482
[Inlined] FEngineLoop::AppExit() LaunchEngineLoop.cpp:6933
[Inlined] LaunchWindowsShutdown() LaunchWindows.cpp:306
WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:318

로드 직후 FConfigFile InMemoryFileDirty 값이 달라지는 부분

ini를 로드한 다음, 패키지 빌드에서만 InMemoryFileDirty 값이 true 가 되는 이유는 아래와 같습니다.

  1. 먼저, 에디터와 패키지 빌드는 전역변수 GDefaultReplayMethod 의 값이 다릅니다: GitHub 링크
  2. GDefaultReplayMethod 의 값에 따라, FConfigBranch 생성자에서 FConfigBranch 의 멤버 변수 ReplayMethod 의 값이 달라집니다. GitHub 링크
  3. FConfigBranch 의 멤버 변수 ReplayMethod 의 값에 따라, FConfigContext::LoadIniFileHierarchy 함수에서 ini 파일을 읽어온 뒤, InMemoryFile 에 기록하는 방법이 달라집니다. 여기서 차이가 발생합니다. GitHub 링크

챕터를 한 번 나눈 뒤, 설명을 이어가겠습니다.

FConfigContext::LoadIniFileHierarchy 에서 일어나는 일

결론부터 말하자면, FConfigFile::ApplyFileDirty 값을 복사해오지 않기 때문입니다.

FConfigContext::LoadIniFileHierarchy 에서 일어나는 일은, Branch->ReplayMethodEBranchReplayMethod::NoReplay가 아닌 한, 대략 다음과 같습니다.

  • ini 파일의 내용을 읽어 Branch->CombinedStaticLayers에 기록.
  • Branch->CombinedStaticLayers의 내용을 Branch->InMemoryFile에 복사.

여기서 ini 파일을 읽는데 사용하는 함수는 Branch->ReplayMethod의 값에 따라, 즉 에디터냐 패키지 빌드냐에 따라 서로 다릅니다만,

결과적으로 어느 쪽이든 ConfigCacheIni.cpp 파일의 FillFileFromBuffer를 호출하게 됩니다.

그리고 이 FillFileFromBuffer 함수 내에서, Dirty 값이 true가 되는데요.8

패키지 빌드일 경우, 즉 즉 Branch->ReplayMethodEBranchReplayMethod::DynamicLayerReplay 일 경우엔 CombinedStaticLayers 의 멤버 함수인 FConfigFile::FillFileFromDisk 를 통하여 ini 파일로부터 읽어온 내용을 직접 자기 자신에 쓰는데 비하여,

에디터일 경우, 즉 Branch->ReplayMethodEBranchReplayMethod::FullReplay 일 경우엔,

  • Branch->StaticLayers 에 새 FConfigCommandStream 항목을 추가한 다음
  • FConfigCommandStream::FillFileFromDisk 함수를 통하여, 추가한 FConfigCommandStream 항목에 읽은 ini 파일의 내용을 쓰고,
  • 추가한 FConfigCommandStream 항목에서 Branch->CombinedStaticLayers 에 값을 복사하기 위해 FConfigFile::ApplyFile 함수를 사용합니다.
    • 그런데, FConfigFile::ApplyFile 함수는 FConfigCommandStreamDirty 변수의 값을 FConfigFileDirty 값으로 복사하지 않습니다. 이 부분 때문에 이 경로를 통과한 경우, Branch->CombinedStaticLayersDirty 값은 false 로 남아있게 됩니다.

그리고 어느 쪽이든, Branch->CombinedStaticLayers의 내용이 Branch->InMemoryFile에 복사되면서 FConfigContext::LoadIniFileHierarchy 함수는 끝납니다.

그리고 초반에 말씀드린 것 처럼, FConfigBranchInMemoryFileDirty 값이 무엇이냐에 따라 삭제하는 부분에 도달할 수 있는 여부가 달라집니다.

노트

미래의 나는 분명 이 부분에서 또 혼란스러워하거나 실수를 할 게 분명하다고 생각해서 이 조사 작업과 문서 작업을 시작했습니다. 솔직히 고백하자면 Config ini 파일과 관련해서 디버깅을 오랫 동안 한 것은 언리얼 엔진을 사용한 이래 처음이 아닙니다. 아주 여러 번 있었습니다. 이후에도 종종 하겠죠. 그래서 이 글을 쓰고 있습니다.

틀린 내용이 있으면 알려주세요!


  1. Hierarchy 가 만들어지는 과정을 보고 싶으시면, 아래 항목들을 보세요.

    • 함수 void FConfigContext::AddStaticLayersToHierarchy GitHub 링크
    • 전역 변수 배열 FConfigLayer GConfigLayers GitHub 링크

    Hierarchy가 만들어질 때의 호출 스택을 예시로 하나 들어보자면, 아래와 같습니다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    FConfigContext::AddStaticLayersToHierarchy(TArray<> *, bool) ConfigContext.cpp:747
    FConfigContext::PerformLoad() ConfigContext.cpp:535
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879
    UClass::CreateDefaultObject() Class.cpp:4882
    UClass::InternalCreateDefaultObjectWrapper() Class.cpp:5492
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    [Inlined] Invoke(void (*const &)(FName, bool), FName &&, bool &&) Invoke.h:47
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (*const &)(FName, bool), FName &&, bool &&) Tuple.h:317
    TBaseStaticDelegateInstance::ExecuteIfSafe(FName, bool) DelegateInstancesImpl.h:777
    [Inlined] TMulticastDelegateBase::Broadcast(FName, bool) MulticastDelegateBase.h:257
    TMulticastDelegate::Broadcast(FName, bool) DelegateSignatureImpl.inl:1079
    FModuleManager::LoadModuleWithFailureReason(FName, EModuleLoadResult &, ELoadModuleFlags) ModuleManager.cpp:821
    FModuleDescriptor::LoadModulesForPhase(Type, const TArray<> &, TMap<> &) ModuleDescriptor.cpp:753
    FProjectManager::LoadModulesForProject(Type) ProjectManager.cpp:60
    FEngineLoop::LoadStartupModules() LaunchEngineLoop.cpp:4702
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3959
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  2. 플러그인일 경우에도 Hierarchy 의 생성은, 앞의 모듈의 경우와 동일한 함수 void FConfigContext::AddStaticLayersToHierarchy에 의해 이루어집니다. GitHub 링크 다만, 호출 시기와 조건이 다르기 때문에, FConfigLayer GConfigLayers 대신 FConfigLayer GPluginLayers GitHub 링크 에 기반하여 이루어집니다.

    이 부분은 앞의 모듈 플러그인의 로드와 다르게 훨씬 더 빠른 시기에 별도의 과정을 통하여 이루어집니다. 호출스택은 아래와 같습니다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    FConfigContext::AddStaticLayersToHierarchy(TArray<> *, bool) ConfigContext.cpp:764
    FConfigContext::PerformLoad() ConfigContext.cpp:535
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    FConfigContext::Load(const wchar_t *) ConfigContext.cpp:296
    FPluginManager::ConfigureEnabledPlugins() PluginManager.cpp:1983
    FPluginManager::LoadModulesForEnabledPlugins(Type) PluginManager.cpp:2836
    FEngineLoop::AppInit() LaunchEngineLoop.cpp:6489
    FEngineLoop::PreInitPreStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:2924
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4437
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  3. 너무나 큰 죄를 지은 것 같아 테스트 직후 지웠습니다. 이것이 왜 큰 죄냐면, 이 파일은 대개 버전 관리에 추가되는 대상도 아니고, 에디터나 패키징된 프로그램에 의해 수시로 내용이 바뀌는 파일이기 때문입니다. 여기에 bCanSaveAllSections=true 를 넣어놓고 제대로 ini 파일이 저장되는 상태라고 생각하고 있다가, 나중에 패키징된 프로그램에서나 다른 사람의 자리에서, 혹은 기존의 ini 파일을 전부 지우고 새로 버전 관리 프로그램에서 새로 작업물을 받거나 하면, 그 이전까진 잘 저장되던 ini 파일이 갑자기 저장되지 않을 것입니다. 이 원인을 찾는 과정은 괴로울 것이며, 찾게 된 다음엔 과거의 자신을 원망할 가능성이 높습니다. ↩︎

  4. ini 로드 실패시 호출스택은 아래와 같습니다. LoadIniFileHierarchy 에서 false를 반환하기 때문에, GenerateDestIniFile 에서 최종 ini 파일을 로드하는 부분 GitHub 링크 에 도달하지 못합니다. 아래는 모듈의 경우에 대한 예시이지만, 플러그인의 경우에도 크게 다르지 않을 것입니다: 아래 호출스택에서 하이라이트된 부분은 동일할 것입니다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    FConfigContext::LoadIniFileHierarchy() ConfigContext.cpp:935
    FConfigContext::GenerateDestIniFile() ConfigContext.cpp:1033
    FConfigContext::PerformLoad() ConfigContext.cpp:540
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879 //FObjectInitializer 의 소멸자라 발견하기가 까다롭습니다.
    UClass::CreateDefaultObject() Class.cpp:4882
    UClass::InternalCreateDefaultObjectWrapper() Class.cpp:5492
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    [Inlined] Invoke(void (*const &)(FName, bool), FName &&, bool &&) Invoke.h:47
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (*const &)(FName, bool), FName &&, bool &&) Tuple.h:317
    TBaseStaticDelegateInstance::ExecuteIfSafe(FName, bool) DelegateInstancesImpl.h:777
    [Inlined] TMulticastDelegateBase::Broadcast(FName, bool) MulticastDelegateBase.h:257
    TMulticastDelegate::Broadcast(FName, bool) DelegateSignatureImpl.inl:1079
    FModuleManager::LoadModuleWithFailureReason(FName, EModuleLoadResult &, ELoadModuleFlags) ModuleManager.cpp:821
    FModuleDescriptor::LoadModulesForPhase(Type, const TArray<> &, TMap<> &) ModuleDescriptor.cpp:753
    FProjectManager::LoadModulesForProject(Type) ProjectManager.cpp:60
    FEngineLoop::LoadStartupModules() LaunchEngineLoop.cpp:4702
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3959
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  5. 플러그인이 ini 를 로드하는 코드는 다음과 같습니다: GitHub 링크 플러그인의 이름에 대한 ini 파일만을 로드하는 것을 볼 수 있습니다.

    이 때의 호출스택은 아래와 같습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    FPluginManager::ConfigureEnabledPlugins() PluginManager.cpp:1969
    FPluginManager::LoadModulesForEnabledPlugins(Type) PluginManager.cpp:2836
    FEngineLoop::AppInit() LaunchEngineLoop.cpp:6489
    FEngineLoop::PreInitPreStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:2924
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4437
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  6. EditorLayout 을 저장하는 부분의 코드는 여기서 보실 수 있습니다. GitHub 링크

    GConfig 에 내용을 저장하고, Json으로 별도의 파일을 또 저장합니다.

    저장하는 시기의 호출스택은 아래와 같습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    FLayoutSaveRestore::SaveToConfig(const FString &, const TSharedRef<> &) LayoutService.cpp:220
    [Inlined] Invoke(void (FMainFrameHandler::*)(const TSharedRef<> &), FMainFrameHandler *&, const TSharedRef<> &) Invoke.h:66
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (FMainFrameHandler::*&)(const TSharedRef<> &), FMainFrameHandler *&, const TSharedRef<> &) Tuple.h:317
    V::TBaseRawMethodDelegateInstance::ExecuteIfSafe(const TSharedRef<> &) DelegateInstancesImpl.h:533
    [Inlined] TDelegate::ExecuteIfBound(const TSharedRef<> &) DelegateSignatureImpl.inl:634
    FTabManager::SavePersistentLayout() TabManager.cpp:967
    FGlobalTabmanager::SaveAllVisualState() TabManager.cpp:2767
    FMainFrameHandler::ShutDownEditor() MainFrameHandler.cpp:142
    ...(생략)
     ↩︎
  7. EditorLayout을 로드하는 부분의 코드는 이 링크에서 보실 수 있습니다: GitHub 링크 Json 으로 로드시 ini 파일을 읽지 않습니다.

    아래는 로드시 호출스택입니다.

    1
    2
    3
    4
    5
    6
    7
    
    FLayoutSaveRestore::LoadFromConfigPrivate(const FString &, const TSharedRef<> &, EOutputCanBeNullptr, const bool, TArray<> &) LayoutService.cpp:250
    FLayoutSaveRestore::LoadFromConfig(const FString &, const TSharedRef<> &, EOutputCanBeNullptr, TArray<> &) LayoutService.cpp:238
    FMainFrameModule::CreateDefaultMainFrameAuxiliary(const bool, const bool, const bool) MainFrameModule.cpp:297
    EditorInit(IEngineLoop &) UnrealEdGlobals.cpp:178
    GuardedMain(const wchar_t *) Launch.cpp:165
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
  8. ini 파일 로드시, Dirty 값이 true 로 바뀌는 부분은 다음과 같습니다.

    코드: GitHub 링크

    에디터에서 실행되든 패키지 빌드에서 실행되든, 위의 지점에서 Dirty 값이 true 로 바뀐다는 점은 동일합니다.

    호출스택은 에디터일 때와 패키지 빌드일 때 사이에 살짝 차이가 있지만, 전반적인 형태는 비슷합니다.

    패키지 빌드에서의 호출스택은 아래와 같습니다. 참고로, 이 호출스택 FConfigContext::LoadIniFileHierarchy()부분에서 라인 번호가 988 이 표시된 것은, 잘못된 숫자입니다. 정확한 위치는 973 라인입니다. 아마 컴파일러의 최적화 때문인 것 같습니다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    FillFileFromBuffer<>(FConfigFile *, TStringView<>, bool, const FString &) ConfigCacheIni.cpp:1673
    FillFileFromDisk<>(FConfigFile *, const FString &, bool) ConfigCacheIni.cpp:1692
    FConfigContext::LoadIniFileHierarchy() ConfigContext.cpp:988
    FConfigContext::GenerateDestIniFile() ConfigContext.cpp:1033
    FConfigContext::PerformLoad() ConfigContext.cpp:540
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    [Inlined] GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879
    UClass::CreateDefaultObject() Class.cpp:4882
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3784
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    GuardedMainWrapper(const wchar_t *) LaunchWindows.cpp:123
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:277
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317

    아래는 에디터에서의 호출스택입니다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    FillFileFromBuffer<>(FConfigCommandStream *, TStringView<>, bool, const FString &) ConfigCacheIni.cpp:1673
    FillFileFromDisk<>(FConfigCommandStream *, const FString &, bool) ConfigCacheIni.cpp:1692
    FConfigContext::LoadIniFileHierarchy() ConfigContext.cpp:965
    FConfigContext::GenerateDestIniFile() ConfigContext.cpp:1033
    FConfigContext::PerformLoad() ConfigContext.cpp:540
    FConfigContext::Load(const wchar_t *, FString &) ConfigContext.cpp:278
    UClass::GetConfigName() Class.cpp:6740
    GetConfigFilename(UObject *) Obj.cpp:2167
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2918
    UObject::LoadConfig(UClass *, const wchar_t *, unsigned int, FProperty *, TArray<> *) Obj.cpp:2783
    FObjectInitializer::PostConstructInit() UObjectGlobals.cpp:4011
    FObjectInitializer::~FObjectInitializer() UObjectGlobals.cpp:3879
    UClass::CreateDefaultObject() Class.cpp:4882
    UClass::InternalCreateDefaultObjectWrapper() Class.cpp:5492
    [Inlined] UClass::GetDefaultObject(bool) Class.h:3409
    UObjectLoadAllCompiledInDefaultProperties(TArray<> &) UObjectBase.cpp:811
    ProcessNewlyLoadedUObjects(FName, bool) UObjectBase.cpp:906
    [Inlined] Invoke(void (*const &)(FName, bool), FName &&, bool &&) Invoke.h:47
    [Inlined] UE::Core::Private::Tuple::TTupleBase::ApplyAfter(void (*const &)(FName, bool), FName &&, bool &&) Tuple.h:317
    TBaseStaticDelegateInstance::ExecuteIfSafe(FName, bool) DelegateInstancesImpl.h:777
    [Inlined] TMulticastDelegateBase::Broadcast(FName, bool) MulticastDelegateBase.h:257
    TMulticastDelegate::Broadcast(FName, bool) DelegateSignatureImpl.inl:1079
    FModuleManager::LoadModuleWithFailureReason(FName, EModuleLoadResult &, ELoadModuleFlags) ModuleManager.cpp:821
    FModuleDescriptor::LoadModulesForPhase(Type, const TArray<> &, TMap<> &) ModuleDescriptor.cpp:753
    FProjectManager::LoadModulesForProject(Type) ProjectManager.cpp:60
    FEngineLoop::LoadStartupModules() LaunchEngineLoop.cpp:4702
    FEngineLoop::PreInitPostStartupScreen(const wchar_t *) LaunchEngineLoop.cpp:3959
    [Inlined] FEngineLoop::PreInit(const wchar_t *) LaunchEngineLoop.cpp:4444
    [Inlined] EnginePreInit(const wchar_t *) Launch.cpp:49
    GuardedMain(const wchar_t *) Launch.cpp:144
    LaunchWindowsStartup(HINSTANCE__ *, HINSTANCE__ *, char *, int, const wchar_t *) LaunchWindows.cpp:266
    WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) LaunchWindows.cpp:317
     ↩︎
태그