Unreal中ini配置文件的hierarchy

发布时间 2023-06-21 20:32:32作者: tsecer

Config

UE的很多配置是通过ini文件实现的,相对于二进制文件来说,ini文件的优点是读取、阅读、修改都非常方便,因为所有的文本编辑器都可以修改。但是UE中的ini文件可谓是眼花缭乱,在Engine、project文件夹下,同样的Engine.ini可能存在baseengine.ini、defaultengine.ini、engine.ini,platform下的engine.ini等,这些结构可能对相同的一个key都会有配置,看起来就有些困惑。
最后在UE的官网可以看到这个关于File Hierarchy的说明:大致的意思就是同一个大类的配置项(例如,前面提到的engine相同的base name都是engine,但是存在base、default等不同前、后缀或者不同文件夹下的ini)使用的是一个简单的层级(hierarchy)来实现。这相当于使用了大家熟知的(well-known)的ini结构的基础上,使用文件系统的结构来实现各种定制化。
因为这个概念比较基础,所以把文字摘录一下作为备份:

The configuration file hierarchy is read in starting with Base.ini, with values in later files in the hierarchy overriding earlier values. All files in the Engine folder will be applied to all projects, while project-specific settings should be in files in the project directory. Finally, all project-specific and platform-specific differences are saved out to [Project Directory]/Saved/Config/[Platform]/[Category].ini.

The below file hierarchy example is for the Engine category of configuration files.

Engine/Config/Base.ini

Base.ini is usually empty.

Engine/Config/BaseEngine.ini

Engine/Config/[Platform]/base[Platform]Engine.ini

[Project Directory]/Config/DefaultEngine.ini

Engine/Config/[Platform]/[Platform]Engine.ini

[Project Directory]/Config/[Platform]/[Platform]Engine.ini

The configuration file in the Saved directory only stores the project-specific and platform-specific differences in the stack of configuration files.

创建project

从工程的Templates文件夹生成具体工程(project)。在工程的根目录下存在一个Templates文件夹,该文件夹下存在了很多可以使用的模板,这些模板中包含了基础的Config、Source、uproject文件,这些文件夹的内容将会被拷贝到新创建的project文件中(可能在拷贝的过程中有一些修改?)。

///@file: Engine\Source\Editor\GameProjectGeneration\Private\GameProjectUtils.cpp
TOptional<FGuid> GameProjectUtils::CreateProjectFromTemplate(const FProjectInformation& InProjectInfo, FText& OutFailReason, FText& OutFailLog, TArray<FString>* OutCreatedFiles)
{
///...
	FGuid ProjectID = FGuid::NewGuid();
	ConfigValuesToSet.Emplace(TEXT("DefaultGame.ini"), TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectID"), ProjectID.ToString(), /*InShouldReplaceExistingValue=*/true);

	// Add all classname fixups
	for (const TPair<FString, FString>& Rename : ClassRenames)
	{
		const FString ClassRedirectString = FString::Printf(TEXT("(OldClassName=\"%s\",NewClassName=\"%s\")"), *Rename.Key, *Rename.Value);
		ConfigValuesToSet.Emplace(TEXT("DefaultEngine.ini"), TEXT("/Script/Engine.Engine"), TEXT("+ActiveClassRedirects"), *ClassRedirectString, /*InShouldReplaceExistingValue=*/false);
	}

	SlowTask.EnterProgressFrame();

	if (!SaveConfigValues(InProjectInfo, ConfigValuesToSet, OutFailReason))
	{
		return TOptional<FGuid>();
	}
///...
	// Generate the project file
	{
}

hierarchy

从实现上看,这个层级又分为了静态和动态两个大类,其中的静态就是约定的配置位置及优先级,它们以数组的形式写死在代码中;另一类动态的则是主要给插件使用的配置,它们的动态主要是因为插件是动态的,所以需要在运行时确定。

static

/**
 * Structure to define all the layers of the config system. Layers can be expanded by expansion files (NoRedist, etc), or by ini platform parents
 */
struct FConfigLayer
{
	// Used by the editor to display in the ini-editor
	const TCHAR* EditorName;
	// Path to the ini file (with variables)
	const TCHAR* Path;
	// Special flag
	EConfigLayerFlags Flag;

};

///@file: Engine\Source\Runtime\Core\Public\Misc\ConfigHierarchy.h

// See FConfigContext.cpp for the types here

static FConfigLayer GConfigLayers[] =
{
	/**************************************************
	**** CRITICAL NOTES
	**** If you change this array, you need to also change EnumerateConfigFileLocations() in ConfigHierarchy.cs!!!
	**** And maybe UObject::GetDefaultConfigFilename(), UObject::GetGlobalUserConfigFilename()
	**************************************************/

	// Engine/Base.ini
	{ TEXT("AbsoluteBase"),				TEXT("{ENGINE}/Config/Base.ini"), EConfigLayerFlags::NoExpand},

	// Engine/Base*.ini
	{ TEXT("Base"),						TEXT("{ENGINE}/Config/Base{TYPE}.ini") },
	// Engine/Platform/BasePlatform*.ini
	{ TEXT("BasePlatform"),				TEXT("{ENGINE}/Config/{PLATFORM}/Base{PLATFORM}{TYPE}.ini")  },
	// Project/Default*.ini
	{ TEXT("ProjectDefault"),			TEXT("{PROJECT}/Config/Default{TYPE}.ini"), EConfigLayerFlags::AllowCommandLineOverride },
	// Project/Generated*.ini Reserved for files generated by build process and should never be checked in 
	{ TEXT("ProjectGenerated"),			TEXT("{PROJECT}/Config/Generated{TYPE}.ini") },
	// Project/Custom/CustomConfig/Default*.ini only if CustomConfig is defined
	{ TEXT("CustomConfig"),				TEXT("{PROJECT}/Config/Custom/{CUSTOMCONFIG}/Default{TYPE}.ini"), EConfigLayerFlags::RequiresCustomConfig },
	// Engine/Platform/Platform*.ini
	{ TEXT("EnginePlatform"),			TEXT("{ENGINE}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini") },
	// Project/Platform/Platform*.ini
	{ TEXT("ProjectPlatform"),			TEXT("{PROJECT}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini") },
	// Project/Platform/GeneratedPlatform*.ini Reserved for files generated by build process and should never be checked in 
	{ TEXT("ProjectPlatformGenerated"),	TEXT("{PROJECT}/Config/{PLATFORM}/Generated{PLATFORM}{TYPE}.ini") },
	// Project/Platform/Custom/CustomConfig/Platform*.ini only if CustomConfig is defined
	{ TEXT("CustomConfigPlatform"),		TEXT("{PROJECT}/Config/{PLATFORM}/Custom/{CUSTOMCONFIG}/{PLATFORM}{TYPE}.ini"), EConfigLayerFlags::RequiresCustomConfig },
	// UserSettings/.../User*.ini
	{ TEXT("UserSettingsDir"),			TEXT("{USERSETTINGS}Unreal Engine/Engine/Config/User{TYPE}.ini"), EConfigLayerFlags::NoExpand },
	// UserDir/.../User*.ini
	{ TEXT("UserDir"),					TEXT("{USER}Unreal Engine/Engine/Config/User{TYPE}.ini"), EConfigLayerFlags::NoExpand },
	// Project/User*.ini
	{ TEXT("GameDirUser"),				TEXT("{PROJECT}/Config/User{TYPE}.ini"), EConfigLayerFlags::NoExpand },
};


/// <summary>
/// Plugins don't need to look at the same number of insane layers. Here PROJECT is the Plugin dir
/// </summary>
static FConfigLayer GPluginLayers[] =
{
	// Engine/Base.ini
	{ TEXT("AbsoluteBase"),				TEXT("{ENGINE}/Config/Base.ini"), EConfigLayerFlags::NoExpand},

	// Plugin/Base*.ini
	{ TEXT("PluginBase"),				TEXT("{PLUGIN}/Config/Base{TYPE}.ini") },
	// Plugin/Default*.ini (we use Base and Default as we can have both depending on Engine or Project plugin, but going forward we should stick with Default)
	{ TEXT("PluginDefault"),			TEXT("{PLUGIN}/Config/Default{TYPE}.ini") },
	// Plugin/Platform/Platform*.ini
	{ TEXT("PluginPlatform"),			TEXT("{PLUGIN}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini") },
	// Project/Default.ini
	{ TEXT("ProjectDefault"),			TEXT("{PROJECT}/Config/Default{TYPE}.ini") },
	// Project/Platform/.ini
	{ TEXT("ProjectDefault"),			TEXT("{PROJECT}/Config/{PLATFORM}/{PLATFORM}{TYPE}.ini") },
};



/**************************************************
**** CRITICAL NOTES
**** If you change these arrays, you need to also change EnumerateConfigFileLocations() in ConfigHierarchy.cs!!!
**************************************************/
static FConfigLayerExpansion GConfigExpansions[] =
{
	// No replacements
	{ nullptr, nullptr, nullptr, nullptr, EConfigExpansionFlags::All },

	// Restricted Locations
	{ 
		TEXT("{ENGINE}/"),						TEXT("{ENGINE}/Restricted/NotForLicensees/"),	
		TEXT("{PROJECT}/Config/"),				TEXT("{RESTRICTEDPROJECT_NFL}/Config/"), 
		EConfigExpansionFlags::ForUncooked | EConfigExpansionFlags::ForCooked
	},
	{ 
		TEXT("{ENGINE}/"),						TEXT("{ENGINE}/Restricted/NoRedist/"),			
		TEXT("{PROJECT}/Config/"),				TEXT("{RESTRICTEDPROJECT_NR}/Config/"), 
		EConfigExpansionFlags::ForUncooked 
	},

	// Platform Extensions
	{
		TEXT("{ENGINE}/Config/{PLATFORM}/"),	TEXT("{EXTENGINE}/Config/"),	
		TEXT("{PROJECT}/Config/{PLATFORM}/"),	TEXT("{EXTPROJECT}/Config/"), 
		EConfigExpansionFlags::ForUncooked | EConfigExpansionFlags::ForCooked | EConfigExpansionFlags::ForPlugin
	},

	// Platform Extensions in Restricted Locations
	// 
	// Regarding the commented EConfigExpansionFlags::ForPlugin expansions: in the interest of keeping plugin ini scanning fast,
	// we disable these expansions for plugins because they are not used by Epic, and are unlikely to be used by licensees. If
	// we can make scanning fast (caching what directories exist, etc), then we could turn this back on to be future-proof.
	{
		TEXT("{ENGINE}/Config/{PLATFORM}/"),	TEXT("{ENGINE}/Restricted/NotForLicensees/Platforms/{PLATFORM}/Config/"),	
		TEXT("{PROJECT}/Config/{PLATFORM}/"),	TEXT("{RESTRICTEDPROJECT_NFL}/Platforms/{PLATFORM}/Config/"), 
		EConfigExpansionFlags::ForUncooked | EConfigExpansionFlags::ForCooked // | EConfigExpansionFlags::ForPlugin 
	},
	{
		TEXT("{ENGINE}/Config/{PLATFORM}/"),	TEXT("{ENGINE}/Restricted/NoRedist/Platforms/{PLATFORM}/Config/"),			
		TEXT("{PROJECT}/Config/{PLATFORM}/"),	TEXT("{RESTRICTEDPROJECT_NR}/Platforms/{PLATFORM}/Config/"), 
		EConfigExpansionFlags::ForUncooked // | EConfigExpansionFlags::ForPlugin
	},
};

///@file: Engine\Source\Runtime\Core\Private\Misc\ConfigContext.cpp
void FConfigContext::AddStaticLayersToHierarchy()
{
	// remember where this file was loaded from
	ConfigFile->SourceEngineConfigDir = EngineConfigDir;
	ConfigFile->SourceProjectConfigDir = ProjectConfigDir;

	// string that can have a reference to it, lower down
	const FString DedicatedServerString = IsRunningDedicatedServer() ? TEXT("DedicatedServer") : TEXT("");

	// cache some platform extension information that can be used inside the loops
	const bool bHasCustomConfig = !FConfigCacheIni::GetCustomConfigString().IsEmpty();


	// figure out what layers and expansions we will want
	EConfigExpansionFlags ExpansionMode = EConfigExpansionFlags::ForUncooked;
	FConfigLayer* Layers = GConfigLayers;
	int32 NumLayers = UE_ARRAY_COUNT(GConfigLayers);
	if (FPlatformProperties::RequiresCookedData())
	{
		ExpansionMode = EConfigExpansionFlags::ForCooked;
	}
	if (bIsForPlugin)
	{
		// this has priority over cooked/uncooked
		ExpansionMode = EConfigExpansionFlags::ForPlugin;
		Layers = GPluginLayers;
		NumLayers = UE_ARRAY_COUNT(GPluginLayers);
	}

	// go over all the config layers
	for (int32 LayerIndex = 0; LayerIndex < NumLayers; LayerIndex++)
	{
		const FConfigLayer& Layer = Layers[LayerIndex];

		// skip optional layers
		if (EnumHasAnyFlags(Layer.Flag, EConfigLayerFlags::RequiresCustomConfig) && !bHasCustomConfig)
		{
			continue;
		}

		// start replacing basic variables
		FString LayerPath = PerformBasicReplacements(Layer.Path, *BaseIniName);
		bool bHasPlatformTag = LayerPath.Contains(TEXT("{PLATFORM}"));

		// expand if it it has {ED} or {EF} expansion tags
		if (!EnumHasAnyFlags(Layer.Flag, EConfigLayerFlags::NoExpand))
		{
			// we assume none of the more special tags in expanded ones
			checkfSlow(FCString::Strstr(Layer.Path, TEXT("{USERSETTINGS}")) == nullptr && FCString::Strstr(Layer.Path, TEXT("{USER}")) == nullptr, TEXT("Expanded config %s shouldn't have a {USER*} tags in it"), *Layer.Path);

			// loop over all the possible expansions
			for (int32 ExpansionIndex = 0; ExpansionIndex < UE_ARRAY_COUNT(GConfigExpansions); ExpansionIndex++)
			{
				// does this expansion match our current mode?
				if ((GConfigExpansions[ExpansionIndex].Flags & ExpansionMode) == EConfigExpansionFlags::None)
				{
					continue;
				}

				FString ExpandedPath = PerformExpansionReplacements(GConfigExpansions[ExpansionIndex], LayerPath);

				// if we didn't replace anything, skip it
				if (ExpandedPath.Len() == 0)
				{
					continue;
				}

				// allow for override, only on BASE EXPANSION!
				if (EnumHasAnyFlags(Layer.Flag, EConfigLayerFlags::AllowCommandLineOverride) && ExpansionIndex == 0)
				{
					checkfSlow(!bHasPlatformTag, TEXT("EConfigLayerFlags::AllowCommandLineOverride config %s shouldn't have a PLATFORM in it"), Layer.Path);

					ConditionalOverrideIniFilename(ExpandedPath, *BaseIniName);
				}

				const FDataDrivenPlatformInfo& Info = FDataDrivenPlatformInfoRegistry::GetPlatformInfo(Platform);

				// go over parents, and then this platform, unless there's no platform tag, then we simply want to run through the loop one time to add it to the
				int32 NumPlatforms = bHasPlatformTag ? Info.IniParentChain.Num() + 1 : 1;
				int32 CurrentPlatformIndex = NumPlatforms - 1;
				int32 DedicatedServerIndex = -1;

				// make DedicatedServer another platform
				if (bHasPlatformTag && IsRunningDedicatedServer())
				{
					NumPlatforms++;
					DedicatedServerIndex = CurrentPlatformIndex + 1;
				}

				for (int PlatformIndex = 0; PlatformIndex < NumPlatforms; PlatformIndex++)
				{
					const FString CurrentPlatform =
						(PlatformIndex == DedicatedServerIndex) ? DedicatedServerString :
						(PlatformIndex == CurrentPlatformIndex) ? Platform :
						Info.IniParentChain[PlatformIndex];

					FString PlatformPath = PerformFinalExpansions(ExpandedPath, CurrentPlatform);

					// @todo restricted - ideally, we would move DedicatedServer files into a directory, like platforms are, but for short term compat,
					// convert the path back to the original (DedicatedServer/DedicatedServerEngine.ini -> DedicatedServerEngine.ini)
					if (PlatformIndex == DedicatedServerIndex)
					{
						PlatformPath.ReplaceInline(TEXT("Config/DedicatedServer/"), TEXT("Config/"));
					}

					// if we match the StartSkippingAtFilename, we are done adding to the hierarchy, so just return
					if (PlatformPath == StartSkippingAtFilename)
					{
						return;
					}

					// add this to the list!
					ConfigFile->SourceIniHierarchy.AddStaticLayer(PlatformPath, LayerIndex, ExpansionIndex, PlatformIndex);
				}
			}
		}
		// if no expansion, just process the special tags (assume no PLATFORM tags)
		else
		{
			checkfSlow(!bHasPlatformTag, TEXT("Non-expanded config %s shouldn't have a PLATFORM in it"), *Layer.Path);
			checkfSlow(!EnumHasAnyFlags(Layer.Flag, EConfigLayerFlags::AllowCommandLineOverride), TEXT("Non-expanded config can't have a EConfigLayerFlags::AllowCommandLineOverride"));

			FString FinalPath = PerformFinalExpansions(LayerPath, TEXT(""));

			// if we match the StartSkippingAtFilename, we are done adding to the hierarchy, so just return
			if (FinalPath == StartSkippingAtFilename)
			{
				return;
			}

			// add with no expansion
			ConfigFile->SourceIniHierarchy.AddStaticLayer(FinalPath, LayerIndex);
		}
	}
}

dynamic

这些主要用在插件(plugin)的配置文件中

bool FPluginManager::IntegratePluginsIntoConfig(FConfigCacheIni& ConfigSystem, const TCHAR* EngineIniName, const TCHAR* PlatformName, const TCHAR* StagedPluginsFile)
{
///...
		for (const FString& ConfigFile : PluginConfigs)
		{
			FString BaseConfigFile = *FPaths::GetBaseFilename(ConfigFile);

			// Use GetConfigFilename to find the proper config file to combine into, since it manages command line overrides and path sanitization
			FString PluginConfigFilename = ConfigSystem.GetConfigFilename(*BaseConfigFile);
			FConfigFile* FoundConfig = ConfigSystem.FindConfigFile(PluginConfigFilename);
			if (FoundConfig != nullptr)
			{
				UE_LOG(LogPluginManager, Log, TEXT("Found config from plugin[%s] %s"), *Plugin.GetName(), *PluginConfigFilename);

				FoundConfig->AddDynamicLayerToHierarchy(FPaths::Combine(PluginConfigDir, ConfigFile));
			}
		}
///...
}

runtime

在加载的过程中,会逐层执行ProcessIniContents>>FConfigFile::Combine>>FConfigFile::CombineFromBuffer中进行读取和覆盖。

///@file: Engine\Source\Runtime\Core\Private\Misc\ConfigContext.cpp
/**
 * This will completely load .ini file hierarchy into the passed in FConfigFile. The passed in FConfigFile will then
 * have the data after combining all of those .ini
 *
 * @param FilenameToLoad - this is the path to the file to
 * @param ConfigFile - This is the FConfigFile which will have the contents of the .ini loaded into and Combined()
 *
 **/
static bool LoadIniFileHierarchy(const FConfigFileHierarchy& HierarchyToLoad, FConfigFile& ConfigFile, bool bUseCache, const TSet<FString>* IniCacheSet)
{
	// Traverse ini list back to front, merging along the way.
	for (const TPair<int32, FString>& HierarchyIt : HierarchyToLoad)
	{
		bool bDoCombine = (HierarchyIt.Key != 0);
		const FString& IniFileName = HierarchyIt.Value;

		// skip non-existant files
		if (IsUsingLocalIniFile(*IniFileName, nullptr) && !DoesConfigFileExistWrapper(*IniFileName, IniCacheSet))
		{
			continue;
		}

		bool bDoEmptyConfig = false;
		//UE_LOG(LogConfig, Log,  TEXT( "Combining configFile: %s" ), *IniList(IniIndex) );
		ProcessIniContents(*IniFileName, *IniFileName, &ConfigFile, bDoEmptyConfig, bDoCombine);
	}

	// Set this configs files source ini hierarchy to show where it was loaded from.
	ConfigFile.SourceIniHierarchy = HierarchyToLoad;

	return true;
}

well-knonw配置类型

这里列出了一些熟知配置文件,其中包括了最为常见的Engine、Game两种类型。

///@file: Engine\Source\Runtime\Core\Public\Misc\ConfigCacheIni.h
#define ENUMERATE_KNOWN_INI_FILES(op) \
	op(Engine) \
	op(Game) \
	op(Input) \
	op(DeviceProfiles) \
	op(GameUserSettings) \
	op(Scalability) \
	op(RuntimeOptions) \
	op(InstallBundle) \
	op(Hardware) \
	op(GameplayTags)


#define KNOWN_INI_ENUM(IniName) IniName,

///@file:Engine\Source\Runtime\Core\Private\Misc\ConfigCacheIni.cpp
FConfigCacheIni::FKnownConfigFiles::FKnownConfigFiles()
{
	// set the FNames associated with each file

	// 	Files[(uint8)EKnownIniFile::Engine].IniName = FName("Engine");
	#define SET_KNOWN_NAME(Ini) Files[(uint8)EKnownIniFile::Ini].IniName = FName(#Ini);
		ENUMERATE_KNOWN_INI_FILES(SET_KNOWN_NAME);
	#undef SET_KNOWN_NAME
}

Saved

///@file: Engine\Source\Runtime\Core\Private\Misc\ConfigContext.cpp
bool FConfigContext::PrepareForLoad(bool& bPerformLoad)
{
	checkf(ConfigSystem != nullptr || ConfigFile != nullptr, TEXT("Loading config expects to either have a ConfigFile already passed in, or have a ConfigSystem passed in"));

	if (bForceReload)
	{
		// re-use an existing ConfigFile's Engine/Project directories if we have a config system to look in,
		// or no config system and the platform matches current platform (which will look in GConfig)
		if (ConfigSystem != nullptr || (Platform == FPlatformProperties::IniPlatformName() && GConfig != nullptr))
		{
			bool bNeedRecache = false;
			FConfigCacheIni* SearchSystem = ConfigSystem == nullptr ? GConfig : ConfigSystem;
			FConfigFile* BaseConfigFile = SearchSystem->FindConfigFileWithBaseName(*BaseIniName);
			if (BaseConfigFile != nullptr)
			{
				if (!BaseConfigFile->SourceEngineConfigDir.IsEmpty() && BaseConfigFile->SourceEngineConfigDir != EngineConfigDir)
				{
					EngineConfigDir = BaseConfigFile->SourceEngineConfigDir;
					bNeedRecache = true;
				}
				if (!BaseConfigFile->SourceProjectConfigDir.IsEmpty() && BaseConfigFile->SourceProjectConfigDir != ProjectConfigDir)
				{
					ProjectConfigDir = BaseConfigFile->SourceProjectConfigDir;
					bNeedRecache = true;
				}
				if (bNeedRecache)
				{
					CachePaths();
				}
			}
		}

	}

	// setup for writing out later on
	if (bWriteDestIni || bAllowGeneratedIniWhenCooked || FPlatformProperties::RequiresCookedData())
	{
		// delay filling out GeneratedConfigDir because some early configs can be read in that set -savedir, and 
		// FPaths::GeneratedConfigDir() will permanently cache the value
		if (GeneratedConfigDir.IsEmpty())
		{
			GeneratedConfigDir = FPaths::GeneratedConfigDir();
		}

		// calculate where this file will be saved/generated to (or at least the key to look up in the ConfigSystem)
		DestIniFilename = FConfigCacheIni::GetDestIniFilename(*BaseIniName, *SavePlatform, *GeneratedConfigDir);

		if (bAllowRemoteConfig)
		{
			// Start the loading process for the remote config file when appropriate
			if (FRemoteConfig::Get()->ShouldReadRemoteFile(*DestIniFilename))
			{
				FRemoteConfig::Get()->Read(*DestIniFilename, *BaseIniName);
			}

			FRemoteConfigAsyncIOInfo* RemoteInfo = FRemoteConfig::Get()->FindConfig(*DestIniFilename);
			if (RemoteInfo && (!RemoteInfo->bWasProcessed || !FRemoteConfig::Get()->IsFinished(*DestIniFilename)))
			{
				// Defer processing this remote config file to until it has finish its IO operation
				bPerformLoad = false;
				return false;
			}
		}
	}

	// we can re-use an existing file if:
	//   we are not loading into an existing ConfigFile
	//   we don't want to reload
	//   we found an existing file in the ConfigSystem
	//   the existing file has entries (because Known config files are always going to be found, but they will be empty)
	bool bLookForExistingFile = ConfigFile == nullptr && !bForceReload && ConfigSystem != nullptr;
	if (bLookForExistingFile)
	{
		// look up a file that already exists and matches the name
		FConfigFile* FoundConfigFile = ConfigSystem->KnownFiles.GetMutableFile(*BaseIniName);
		if (FoundConfigFile == nullptr)
		{
			FoundConfigFile = ConfigSystem->FindConfigFile(*DestIniFilename);
			//// @todo: this is test to see if we can simplify this to FindConfigFileWithBaseName always (if it never fires, we can)
			//check(FoundConfigFile == nullptr || FoundConfigFile == ConfigSystem->FindConfigFileWithBaseName(*BaseIniName))
		}

		if (FoundConfigFile != nullptr && FoundConfigFile->Num() > 0)
		{
			ConfigFile = FoundConfigFile;
			bPerformLoad = false;
			return true;
		}
	}

	// setup ConfigFile to read into if one isn't already set
	if (ConfigFile == nullptr)
	{
		// first look for a KnownFile
		ConfigFile = ConfigSystem->KnownFiles.GetMutableFile(*BaseIniName);
		if (ConfigFile == nullptr)
		{
			check(!DestIniFilename.IsEmpty());

			ConfigFile = &ConfigSystem->Add(DestIniFilename, FConfigFile());
		}
	}

	bPerformLoad = true;
	return true;
}

bool FConfigContext::Load(const TCHAR* InBaseIniName, FString& OutFinalFilename)
{
	// for single file loads, just return early of the file doesn't exist
	const bool bBaseIniNameIsFullInIFilePath = FString(InBaseIniName).EndsWith(TEXT(".ini"));
	if (!bIsHierarchicalConfig && bBaseIniNameIsFullInIFilePath && !DoesConfigFileExistWrapper(InBaseIniName, IniCacheSet))
	{
		return false;
	}

	if (bCacheOnNextLoad || BaseIniName != InBaseIniName)
	{
		ResetBaseIni(InBaseIniName);
		CachePaths();
		bCacheOnNextLoad = false;
	}


	bool bPerformLoad;
	if (!PrepareForLoad(bPerformLoad))
	{
		return false;
	}

	// if we are reloading a known ini file (where OutFinalIniFilename already has a value), then we need to leave the OutFinalFilename alone until we can remove LoadGlobalIniFile completely
	if (OutFinalFilename.Len() > 0 && OutFinalFilename == BaseIniName)
	{
		// do nothing
	}
	else
	{
		check(!bWriteDestIni || !DestIniFilename.IsEmpty());

		OutFinalFilename = DestIniFilename;
	}

	// now load if we need (PrepareForLoad may find an existing file and just use it)
	return bPerformLoad ? PerformLoad() : true;
}

由于Generated文件夹默认是存储在Saved文件夹,所以通常可写的内容都是写入到Saved\Config文件夹下,这也是为什么这个文件夹下的Config内容会自动生成的原因。

const TCHAR* FGenericPlatformMisc::GeneratedConfigDir()
{
	static FString Dir = FPaths::ProjectSavedDir() / TEXT("Config/");
	return *Dir;
}
const FString& FPaths::ProjectSavedDir()
{
	FStaticData& StaticData = TLazySingleton<FStaticData>::Get();
	if (!StaticData.bGameSavedDirInitialized)
	{
		StaticData.GameSavedDir = UE4Paths_Private::GameSavedDir();
		StaticData.bGameSavedDirInitialized = true;
	}
	return StaticData.GameSavedDir;
}

栗子

确定ini配置

在启动过程中,使用"EditorSettings"作为ini的base名字,经过配置获得的GEditorSettingsIni变量值为"../../../Engine/Saved/Config/WindowsEditor/EditorSettings.ini"。

///@file: Engine\Source\Runtime\Core\Private\Misc\ConfigCacheIni.cpp
static void LoadRemainingConfigFiles(FConfigContext& Context)
{
	SCOPED_BOOT_TIMING("LoadRemainingConfigFiles");

#if PLATFORM_DESKTOP
	// load some desktop only .ini files
	Context.Load(TEXT("Compat"), GCompatIni);
	Context.Load(TEXT("Lightmass"), GLightmassIni);
#endif

#if WITH_EDITOR
	// load some editor specific .ini files

	Context.Load(TEXT("Editor"), GEditorIni);

	// Upgrade editor user settings before loading the editor per project user settings
	FConfigManifest::MigrateEditorUserSettings();
	Context.Load(TEXT("EditorPerProjectUserSettings"), GEditorPerProjectIni);

	// Project agnostic editor ini files, so save them to a shared location (Engine, not Project)
	Context.GeneratedConfigDir = FPaths::EngineEditorSettingsDir();
	Context.Load(TEXT("EditorSettings"), GEditorSettingsIni);
	Context.Load(TEXT("EditorKeyBindings"), GEditorKeyBindingsIni);
	Context.Load(TEXT("EditorLayout"), GEditorLayoutIni);

#endif

	if (FParse::Param(FCommandLine::Get(), TEXT("dumpconfig")))
	{
		GConfig->Dump(*GLog);
	}
}

读取键值

从全局配置cache中读取

		FString GameEngineClassName;
		GConfig->GetString(TEXT("/Script/Engine.Engine"), TEXT("GameEngine"), GameEngineClassName, GEngineIni);
					
		// Find the editor target
		FString EditorTargetFileName;
		FString DefaultEditorTarget;
		GConfig->GetString(TEXT("/Script/BuildSettings.BuildSettings"), TEXT("DefaultEditorTarget"), DefaultEditorTarget, GEngineIni);