UnrealEngine3 에는 다양한 설정들을 .ini 파일에 저장하여 사용합니다. 여기서는 설정파일의 구조를 설명하고 엔진에서 이를 다루는 방식에 대해 소개합니다.
설정파일은 다음과 같은 일반적인 구조를 가집니다.
; ConfigFile.ini (임의의 설정파일) [Section00] Property00=Value00 Property01=Value01 [Section01] Property00=Value00 Property01=Value01 [Section02] Property00=Value00 Property01=Value01 Property02=Value02
Window 표준 설정파일의 형식을 가지며 이를 UnrealEngine3 에서도 차용하고 있습니다.
위 구조를 UnrealEngine3 내부적으로 다음과 같은 타입으로 핸들링하고 있습니다.
class FConfigCacheIni // config 모든 파일을 관장하는 manager 급 클래스 TMap<FFilename, FConfigFile> 로 상속받아 몇가지 method 를 추가 class FConfigFile // 설정파일. FConfigCacheIni 와 Has-A 관계에 있으며 상기 구조에서 ConfigFile.ini 에 해당 class FConfigSection // 섹션파일. FConfigFile 과 Has-A 관계에 있으며 상기 구조에서 Section00-02 에 해당 class FString // 속성파일. FConfigSection 과 Has-A 관계에 있으며 상기 구조에서 Property00=Value00 에 해당
세부적인 코드는 FConfigCacheIni.h & .cpp 에서 확인할 수 있습니다.
엔진에서 설정파일을 로드하는데 사용하는 메인함수가 있습니다.
그 함수는 아래와 같습니다.
// UnMisc.cpp void appCheckIniForOutdatedness( const TCHAR* GeneratedIniFile, const TCHAR* DefaultIniFile, const UBOOL bTryToPreserveContents, UINT& YesNoToAll, UBOOL bForceReload/*=FALSE*/ ) { ... GConfig->LoadFile( GeneratedIniFile, &DefaultConfigFile ); // GConfig (FConfigCacheIni) 에 설정파일 내용 저장 }
이 함수가 아래 이어지는 여러 상황에서 호출되어집니다.
// UnMisc.cpp void appInit( ... ) { ... //// Init config. GConfig = ConfigFactory(); ... #if WITH_EDITOR // 에디터 로드 시 appCheckInitForOutdateness( GEditrIni, GDefaultEditorIni, FALSE, YesNoToAll ); appCheckInitForOutdateness( GEditrUserSettingsIni, GDefaultEditorUserSettingsIni, FALSE, YesNoToAll ); #endif appCheckInitForOutdateness( GCompatIni, GDefaultCompatIni, FALSE, YesNoToAll ); appCheckInitForOutdateness( GLightmassIni, GDefaultLightmassIni, FALSE, YesNoToAll ); appCheckInitForOutdateness( GEngineIni, GDefaultEngineIni, FALSE, YesNoToAll ); appCheckInitForOutdateness( GGameIni, GDefaultGameIni, FALSE, YesNoToAll ); appCheckInitForOutdateness( GInputIni, GDefaultInputIni, FALSE, YesNoToAll ); appCheckInitForOutdateness( GUIIni, GDefaultUIIni, FALSE, YesNoToAll ); ... }
// UnLinker.cpp // package 파일로부터 오브젝트 파일의 내용을 로드하는 함수 void ULinkerLoad::Preload( UObject* Object ) { ... if ( Object->HasAnyFlags( RF_ClassDefaultObject ) && !GIsUCCMake ) { // config 파일 로드, 그리고 아래 나와있는 UClass::GetConfigName() 으로 이어진다. Object->LoadConfig(); } ... } // UnClass.h class UClass : public UState { ... // 현 class 가 사용하는 config 파일을 반환하는 함수 (지만 파일명을 조합하고 로드하는 역할도 한다.) const FString GetConfigName() const { if ( ClassConfigName == NAME_Engine ) { return GEngineIni; } else if ( ... ) { ... } ... else { FString ConfigGameName = appGameConfigDir() + FString( GGameName ) + ClassConfigName.ToString() + TEXT( ".ini" ); // user 설정파일과 FString ConfigDefaultName = appGameConfigDir() + TEXT( "Default" ) + ClassConfigName.ToString() + TEXT( ".ini" ); // default 설정파일명을 생성 UINT YesNoAll = ART_No; const UBOOL bTryToPreserveContents = FALSE; appCheckIniForOutdateness( *ConfigGameName, *ConfigDefaultName, bTryToPreserveContents, YesNoAll ); // 파일을 로드 return ConfigGameName; } } ... }
클라이언트를 재부팅하지 않아도 설정파일을 다시 로드할 수 있는 커맨드가 존재한다. reloadcfg 나 reloadconfig 가 그것이다. TAB 이나 '`' 를 누르면 나오는 콘솔창으로 입력하는 이 커맨드의 시그니쳐는 다음과 같다.
reloadcfg <Class/Object> // reloadcfg 혹은 reloadconfig 다음의 파라메터는 class 이름이나 object 이름이 될 수 있다.
이 커맨드의 의미는 실시간 modify 가 아닌 restore 의 개념이다. 무슨 말인고 하니, 설정파일을 고치고 저 커맨드를 실행해도 반영되지 않는다. 엔진이 부트업되는 시점 딱 한번 설정파일들을 Heap에 올려놓은 후 그 값으로 다시 복원restore한다는 개념이다.1) 때문에 잘 알고서 사용해야 한다.
(계속 설명을 이어가자면) 의미론적으로 class 는 변수 type 에 해당하며 Object 는 instance 에 해당된다. 따라서 실제 게임 내 실시간 반영시키려면 Object 이름을 넣어야 한다.
Object 이름은 프로그래머가 아니라면 알기 힘들다. 왜냐면 UnrealEngine3 내부적으로 Object, 즉 instance 의 이름은 [class이름+넘버링] 으로 조합되기 때문이다. 예를 들어 UTPawn class 인스턴스는 UTPawn_0, UTPawn_1, … , UTPawn_999 등의 순서로 생성된다.
레벨이 다시 로드된다거나 죽어서 리스폰된다거나 하면 기존 instance 는 소멸되고 넘버링이 증가된 새 instance 를 생성한다. (UnrealEngine3 내부적으로..) 따라가 debugger 로 실시간 instance 이름 확인이 가능한 프로그래머가 아닌 다른 직군들은 Object 의 이름을 제대로 알기 힘들다.
위에서 엔진이 기본 제공하는 reloadcfg 는 우리가 원하는 기능을 하지 않는다는 것을 알았다. 이제 restore 기능이 아닌 change & adjust 기능을 하는 치트키를 실제 구현해보기로 한다.
크게 다음과 같은 단계로 구현될 것이다.
이를 구현한 코드조각은 아래와 같다.
void UMyCheatManager::RelCfgNative( FName ClassName ) { // 원본 클래스가 존재하는지 확인 UClass* pOrigClass = FindObject<UClass>( ANY_PACKAGE, *ClassName.ToString() ); if ( !pOrigClass ) { debugf( TEXT( "class not found ( name : %s )" ), *ClassName.ToString() ); return; } // config 파일 (.ini) 다시 로드 const FString ConfigFileName = pOrigClass->GetConfigName(); if ( GConfig->FindConfigFile( *ConfigFileName ) ) { FString ConfigGameName = appGameConfigDir() + FString( GGameName ) + pOrigClass->ClassConfigName.ToString() + TEXT( ".ini" ); FString ConfigDefaultName = appGameConfigDir() + TEXT( "Default" ) + pOrigClass->ClassConfigName.ToString() + TEXT( ".ini" ); UINT YesNoAll = ART_No; const UBOOL bTryToPreserveContents = FALSE; appCheckIniForOutdatedness( *ConfigGameName, *ConfigDefaultName, bTryToPreserveContents, YesNoAll, TRUE ); } // 루프를 돌며 Config 를 다시 읽어들여 적용 for ( TObjectIterator<UObject> It; It; ++It ) { if ( It->IsA( pOrigClass ) ) { It->ReloadConfig(); } } }
위 네이티브 코드를 스크립트의 exec 함수로 호출하는 형태면 좋다.
exec function RelCfg( Name ClassName ) { if ( Role == ROLE_Authority && WorldInfo.NetMode != NM_Client && Pawn.IsLocallyControlled() ) { RelCfgNative( ClassName ); } }
만약 MyPawn.uc 파일에서 config 로부터 불러오는 속성을 DefaultPawn.ini 에서 수정한 후 인게임에 바로 적용하려면 틸트( ` ) 나 탭을 눌러 콘솔창을 열고 relcfg MyPawn 을 입력하면 된다.