Lyra GameSettings Plugin
플러그인 없이 설정 구현
UGameUserSettings
언리얼에서 기본적으로 해상도, 그래픽 품질과 같은 설정은 UGameUserSettings로 관리한다.
// 설정 가져오기
UGameUserSettings* Settings = UGameUserSettings::GetGameUserSettings();
// 값 설정
Settings->SetScreenResolution(FIntPoint(1920, 1080));
Settings->SetFullscreenMode(EWindowMode::WindowedFullscreen);
// 저장 또는 적용
Settings->SaveSettings();
Settings->ApplySettings(true); UGameUserSettings 확장
UGameUserSettings를 상속하고 아래처럼 UPROPERTY(Config)가 붙은 값을 선언하면 설정 파일에 직렬화된다.
UPROPERTY(Config)
float MusicVolume = 1.0f; 하지만 다음 기능까지 구현하려면 설정값을 저장하는 클래스 외에도 데이터 모델과 UI를 연결하는 코드가 필요하다.
- PC, 콘솔, 모바일별 설정 노출
- 키보드와 게임패드용 UI 구성
- 설정 검색과 카테고리, 하위 페이지
- 조건에 따른 숨김과 비활성화
- 설정 항목을 추가해도 UI 코드를 수정하지 않는 구조
Lyra Game Settings Plugin
위와 같은 경우를 쉽게 처리하고자 언리얼 Lyra 예제에선 Game Settings라는 플러그인을 만들어 대응하고 있다.
UI 관련 쪽은 Dependency로 Common UI 가 요구된다.
주요 클래스
설정 데이터는 JSON/YAML 처럼 트리 형식의 구조로 표현하는데 UGameSetting이라는 노드를 중심으로 구성된다.
Class 계층 구조
UGameSetting
├─ UGameSettingCollection
│ └─ UGameSettingCollectionPage
├─ UGameSettingValue
│ ├─ UGameSettingValueDiscrete
│ └─ UGameSettingValueScalar
└─ UGameSettingAction UGameSetting- 모든 설정 노드의 공통 기반
DevName, 표시 이름, 설명, 태그, 부모, 소유LocalPlayer, 편집 상태와 이벤트를 가짐DevName은 Registry 안에서 유일해야 하며 탐색과 특정 위젯 매핑에 사용됨
UGameSettingCollection- 여러
UGameSetting을 담는 그룹 - 자체 선택은 불가능하고 화면에서 Header 역할을 함
- 여러
UGameSettingCollectionPage- 별도 페이지로 이동할 수 있는 Collection
- 선택 가능하며
NavigationText를 가짐
UGameSettingRegistry- 최상위 설정과 모든 하위 설정을 등록
FindSettingByDevName(), 필터링, 변경 및 Navigation 이벤트 중계를 담당
UGameSettingValue- 유저가 실제로 변경하는 값을 나타내는 노드
- 기본적으로 제공되는 값 타입
UGameSettingValueScalarDynamic: Slider에 적합한 수치 값UGameSettingValueDiscreteDynamic: 목록에서 하나를 고르는 값UGameSettingValueDiscreteDynamic_BoolUGameSettingValueDiscreteDynamic_NumberUGameSettingValueDiscreteDynamic_EnumUGameSettingValueDiscreteDynamic_ColorUGameSettingValueDiscreteDynamic_Vector2D
UGameSettingValue 생성 예시
UGameSettingValueScalarDynamic* Setting = NewObject<UGameSettingValueScalarDynamic>();
Setting->SetDevName(TEXT("OverallVolume"));
Setting->SetDisplayName(LOCTEXT("OverallVolume_Name", "Overall"));
Setting->SetDescriptionRichText(LOCTEXT("OverallVolume_Description", "Adjusts the volume of everything."));
Setting->SetDynamicGetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(GetOverallVolume));
Setting->SetDynamicSetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(SetOverallVolume)); FGameSettingDataSource
UGameSettingValue에서 Getter Setter 인자 값으로 TSharedRef<FGameSettingDataSource> 를 받는데
이는 설정 항목이 실제 설정 값에 접근하는 방법을 추상화해, 값의 읽기/쓰기와 초기화 준비를 담당하는 인터페이스이다.
실제로 구현은 이를 상속한 FGameSettingDataSourceDynamic 을 사용하는데
FGameSettingDataSourceDynamic은 TArray<FString>을 받아 언리얼 리플렉션을 사용하여 해당 경로의 Function이나 Property를 가져오는 역할을 한다.
Lyra에선 매번 귀찮으니 아래의 매크로를 만들어서 사용하고 있다.
#define GET_LOCAL_SETTINGS_FUNCTION_PATH(FunctionOrPropertyName)
MakeShared<FGameSettingDataSourceDynamic>(TArray<FString>({
GET_FUNCTION_NAME_STRING_CHECKED(ULyraLocalPlayer, GetLocalSettings),
GET_FUNCTION_NAME_STRING_CHECKED(ULyraSettingsLocal, FunctionOrPropertyName)
})) GET_LOCAL_SETTINGS_FUNCTION_PATH(GetOverallVolume) 는
ULyraLocalPlayer.GetLocalSettings().GetOverallVolume() 를 가져오는 FGameSettingDataSourceDynamic라고 볼 수 있다.
언리얼 리플렉션을 사용하기 때문에 경로에 사용하는 함수는 UFUNCTION, 프로퍼티는 UPROPERTY여야 한다. Getter와 Setter는 초기화 시 Resolve()되므로 이름이나 시그니처가 잘못되면 에디터에서 ensure가 발생한다.
편집 조건과 필터
작성 예정
Lyra 구현
설정 저장 클래스
Lyra는 설정을 기기 단위와 플레이어 단위로 나눈다.
| 클래스 | 저장 단위 | 저장 방식 | 대표 설정 |
|---|---|---|---|
ULyraSettingsLocal | 현재 기기/OS 사용자 | UGameUserSettings의 Config | 해상도, 그래픽 품질, FPS, Device Profile, 볼륨, 출력 장치, 감마, Safe Zone |
ULyraSettingsShared | 로그인한 플레이어 | ULocalPlayerSaveGame | 감도, 축 반전, 데드존, 진동, 자막, 색각 보정, 언어, 키 설정 |
ULyraSettingsLocal은 프로세스 시작 시 읽히며 기기 성능과 관련된 값을 가진다. ULyraSettingsShared는 SaveGame 시스템으로 로드하므로 플랫폼에 따라 로그인 이후에 준비될 수 있고 클라우드 저장에도 적합하다.
ULyraLocalPlayer가 두 객체의 접근 지점이다.
ULyraSettingsLocal* ULyraLocalPlayer::GetLocalSettings() const
{
return ULyraSettingsLocal::Get();
}
ULyraSettingsShared* ULyraLocalPlayer::GetSharedSettings() const
{
// LoadOrCreateSettings 또는 임시 설정 반환
return SharedSettings;
} Registry의 동적 경로가 항상 LocalPlayer에서 시작하는 이유도 여기에 있다.
ULyraGameSettingRegistry
ULyraGameSettingRegistry::OnInitialize()는 다음 최상위 Collection을 만들고 등록한다.
- Video
- Audio
- Gameplay
- Mouse and Keyboard
- Gamepad
class ULyraGameSettingRegistry : public UGameSettingRegistry
{
UPROPERTY()
TObjectPtr<UGameSettingCollection> VideoSettings;
UPROPERTY()
TObjectPtr<UGameSettingCollection> AudioSettings;
// Gameplay, MouseAndKeyboard, Gamepad...
}; Audio 설정 중 전체 볼륨은 다음처럼 구성된다.
UGameSettingCollection* ULyraGameSettingRegistry::InitializeAudioSettings(ULyraLocalPlayer* InLocalPlayer)
{
UGameSettingCollection* Screen = NewObject<UGameSettingCollection>();
Screen->SetDevName(TEXT("AudioCollection"));
Screen->SetDisplayName(LOCTEXT("AudioCollection_Name", "Audio"));
Screen->Initialize(InLocalPlayer);
UGameSettingCollection* Volume = NewObject<UGameSettingCollection>();
Volume->SetDevName(TEXT("VolumeCollection"));
Volume->SetDisplayName(LOCTEXT("VolumeCollection_Name", "Volume"));
Screen->AddSetting(Volume);
UGameSettingValueScalarDynamic* Setting = NewObject<UGameSettingValueScalarDynamic>();
Setting->SetDevName(TEXT("OverallVolume"));
Setting->SetDisplayName(LOCTEXT("OverallVolume_Name", "Overall"));
Setting->SetDescriptionRichText(LOCTEXT("OverallVolume_Description", "Adjusts the volume of everything."));
Setting->SetDynamicGetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(GetOverallVolume));
Setting->SetDynamicSetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(SetOverallVolume));
Setting->SetDefaultValue(GetDefault<ULyraSettingsLocal>()->GetOverallVolume());
Setting->SetDisplayFormat(UGameSettingValueScalarDynamic::ZeroToOnePercent);
Setting->AddEditCondition(FWhenPlayingAsPrimaryPlayer::Get());
Volume->AddSetting(Setting);
return Screen;
} Registry를 만들 때 이 Collection들을 RegisterSetting()한다.
Build 설정
게임 모듈에서 최소한 GameSettings를 의존성에 추가한다. 동적 경로 타입을 Public Header에서 직접 사용한다면 PropertyPath도 모듈 경계에 맞춰 추가해야 한다.
PublicDependencyModuleNames.AddRange(new string[]
{
"PropertyPath"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"CommonInput",
"CommonUI",
"GameSettings"
}); Project 설정
Lyra의 DefaultEngine.ini에는 커스텀 LocalPlayer와 GameUserSettings가 등록되어 있다.
[/Script/Engine.Engine]
LocalPlayerClassName=/Script/LyraGame.LyraLocalPlayer
GameUserSettingsClassName=/Script/LyraGame.LyraSettingsLocal 자체 프로젝트에 적용할 때는 모듈명과 클래스명을 프로젝트의 구현으로 바꾼다.
UI 구현
UGameSettingScreen
UGameSettingScreen은 UCommonActivatableWidget을 상속한 설정 화면이다. 자식 클래스는 CreateRegistry()를 구현해야 하며 위젯에는 Settings_Panel이라는 UGameSettingPanel이 필수로 바인딩된다.
UGameSettingScreen은 FGameSettingRegistryChangeTracker로 Dirty 상태를 관리하고
Apply와 Cancel
설정을 저장/취소하는 Apply, Cancel 기능을 제공한다. ULyraSettingScreen은 여기에 Common UI의 Back, Apply, Cancel Input Action을 등록한다.
HaveSettingsBeenChanged() GameSettingVisualData
UGameSettingVisualData는 설정 데이터 클래스와 Registry에 등록된 GameSettings를 실제 UI 로 Mapping하는 Data Asset이다. Lyra에서는 /Game/UI/Settings/GameSettingRegistryVisuals 에셋을 사용한다.
위젯은 GameSettingListEntry 를 상속하여 구현되며 Value의 경우 Scalar/Discrete여부에 따라
UGameSettingListEntrySetting_(Scalar|Discrete) 를 상속한 위젯을 구현한다.
W_SettingsPanel
- 부모 클래스:
UGameSettingPanel - 필수 바인딩:
ListView_Settings(UGameSettingListView) - 선택 바인딩:
Details_Settings(UGameSettingDetailView)
W_GameSettingsDetailView
- 부모 클래스:
UGameSettingDetailView - 선택 바인딩
Text_SettingNameRichText_DescriptionRichText_DynamicDetailsRichText_WarningDetailsRichText_DisabledDetailsBox_DetailsExtension
SetDynamicDetails()는 현재 상태에 따라 달라지는 설명, SetWarningRichText()는 고정 경고를 지정한다. 비활성화 문구는 AddEditCondition()이 만든 DisabledReasons에서 자동으로 구성된다.
예를 들면 다음과 같다.
- Dynamic Details: 현재 모니터는 HDR을 지원합니다.
- Warning Details: 변경하면 게임을 재시작해야 합니다.
- Disabled Details: 주 플레이어만 변경할 수 있습니다.
에디터에서는 Dynamic Details 아래에 DevName과 클래스가 디버그 정보로 표시된다. 다음 콘솔 변수로 끌 수 있다.
GameSettings.ShowDebugInfo 0 -1은 에디터에서만 표시하는 기본값, 0은 숨김, 1은 항상 표시다. Shipping 빌드에서는 항상 표시되지 않는다.
List Entry
플러그인은 다음 Entry 부모 클래스를 제공한다.
UGameSettingListEntry_Setting:Text_SettingNameUGameSettingListEntrySetting_Discrete: Rotator와 증감 버튼UGameSettingListEntrySetting_Scalar: Slider와 현재값 TextUGameSettingListEntrySetting_Action: Action 버튼UGameSettingListEntrySetting_Navigation: 하위 페이지 이동 버튼
게임패드로 Entry에 진입했을 때 실제 조작 위젯을 GetPrimaryGamepadFocusWidget()에서 반환한다. Scalar Entry라면 Slider, Discrete Entry라면 Rotator를 반환하는 식이다.
GameSettingPanel 포커스
→ ListView가 행 선택
→ ListEntry가 포커스를 받음
→ GetPrimaryGamepadFocusWidget() 호출
→ 반환된 자식 위젯으로 포커스 이동 Navigation
설정에서 하위 페이지를 구성할 수 있다.
UGameSettingCollectionPage를 Registry 트리에 넣으면 Panel이 Common UI의 Stack이 아닌 별도의 Filter Navigation Stack으로 페이지 이동을 처리한다.
UGameSettingCollectionPage* AdvancedPage = NewObject<UGameSettingCollectionPage>();
AdvancedPage->SetDevName(TEXT("AdvancedAudioPage"));
AdvancedPage->SetDisplayName(LOCTEXT("AdvancedAudioPage", "Advanced"));
AdvancedPage->SetNavigationText(LOCTEXT("OpenAdvancedAudioPage", "Open"));
AdvancedPage->SetDescriptionRichText(LOCTEXT("AdvancedAudioDescription", "Advanced audio settings."));
AdvancedPage->AddSetting(/* 하위 설정 */);
AudioCollection->AddSetting(AdvancedPage); 작성 중
참고 소스
Plugins/GameSettings/Source/Public/GameSetting.hPlugins/GameSettings/Source/Public/GameSettingRegistry.hPlugins/GameSettings/Source/Public/DataSource/GameSettingDataSourceDynamic.hPlugins/GameSettings/Source/Public/Widgets/Source/LyraGame/Settings/LyraGameSettingRegistry*.cppSource/LyraGame/Settings/LyraSettingsLocal.*Source/LyraGame/Settings/LyraSettingsShared.*Source/LyraGame/UI/LyraSettingScreen.*