We have discussed about the base class of Gameplay Setting last time, this time we will move further, take a look at how the derived classes finally accomplish the feature. We take the imheritance chain GameplaySetting->GameplaySettingValue->GameplaySettingValueDiscrete->GameplaySettingValueDiscreteDynamic as an example.

 

1. GameplaySettingValue

 

Compared with its parent class GameplaySetting, GameplaySettingValue provides three new methods and overrides the OnInitialize method:

//--------------------------------------
// UGameSettingValue
//--------------------------------------

/**
 * The base class for all settings that are conceptually a value, that can be 
 * changed, and thus reset or restored to their initial value.
 */
UCLASS(Abstract)
class GAMESETTINGS_API UGameSettingValue : public UGameSetting
{
	GENERATED_BODY()

public:
	UGameSettingValue();

	/** Stores an initial value for the setting.  This will be called on initialize, but should also be called if you 'apply' the setting. */
	virtual void StoreInitial() PURE_VIRTUAL(, );

	/** Resets the property to the default. */
	virtual void ResetToDefault() PURE_VIRTUAL(, );

	/** Restores the setting to the initial value, this is the value when you open the settings before making any tweaks. */
	virtual void RestoreToInitial() PURE_VIRTUAL(, );

protected:
	virtual void OnInitialized() override;
};

What is “Initial”, what is “Default”? To be short, Initial will be called when initialzing the settings, the settings panel will show you the initial value, which is used to show the player what the setting is now. And if you changed the setting value now, it will call StoreInitial() to change the initial value, so next time you open the setting panel, it will show the value you changed to last time.

StoreInitial() is now used to init the game setting interval, it is just used for init the GameplaySettingValue object, the latter will be used to accomplish the UI widget to accomplish showing and changing the value it stores in GameplaySetting panel, so it will be called on initialize and called everytime apply the change of the settings.

As for the default value, that is the value used for when the players download the game and start the game, open the setting panel for the first time. Nowerdays every 3A game have a reset to default button which allows player set all settings to default, that is very useful for the players who is not familiar with the game settings and don’t know what they have done that makes the game crash or weird.

Note that Lyra didn’t support reset to default on gameplay setting panel, but with ResetToDefault() APi, it is easy to implement it.

Lyra did not support reset to default for players

The OnInitialized() method is simply implemented as:

void UGameSettingValue::OnInitialized()
{
	Super::OnInitialized();

#if !UE_BUILD_SHIPPING
	ensureAlwaysMsgf(!DescriptionRichText.IsEmpty() || DynamicDetails.IsBound(), TEXT("You must provide a description or it must specify a dynamic details function for settings with values."));
#endif

	StoreInitial();
}

Nothing special after analyzing the StoreInital() method before, when is worth mentioning about here is the macro PURE_VIRTUAL:

#if CHECK_PUREVIRTUALS
#define PURE_VIRTUAL(func,...) =0;
#else
#define PURE_VIRTUAL(func,...) { LowLevelFatalError(TEXT("Pure virtual not implemented (%s)"), TEXT(#func)); __VA_ARGS__ }
#endif

As in known to us, UObject cannot have pure function, so it is wrong to declare a method with virtual void RestoreToInitial()=0. As a result, we use the macro PURE_FUNCTION, it finally generates replace with a method LowLevelFatalError(), the second param of the macro is used for the return value, which can be seen next.

 

2. GameplaySettingValueDiscrete

 

As its name suggets, a GameplaySettingValueDiscrete is used for the discrete value, such as Resolution, Language, Shader Quality and so on, it can be divided into several sub classes, so it is an abstract class too.

It is usually finally be shown as the following in gameplay setting panel:

the implementation is as followed, which is a bit complex the its parent class:

UCLASS(Abstract)
class GAMESETTINGS_API UGameSettingValueDiscrete : public UGameSettingValue
{
	GENERATED_BODY()

public:
	UGameSettingValueDiscrete();

	/** UGameSettingValueDiscrete */
	virtual void SetDiscreteOptionByIndex(int32 Index) PURE_VIRTUAL(,);
	
	UFUNCTION(BlueprintCallable)
	virtual int32 GetDiscreteOptionIndex() const PURE_VIRTUAL(,return INDEX_NONE;);

	/** Optional */
	UFUNCTION(BlueprintCallable)
	virtual int32 GetDiscreteOptionDefaultIndex() const { return INDEX_NONE; }

	UFUNCTION(BlueprintCallable)
	virtual TArray<FText> GetDiscreteOptions() const PURE_VIRTUAL(,return TArray<FText>(););

	virtual FString GetAnalyticsValue() const;
};

Let’s take a look at the design philosophy of it first. It is used to decribe the discrete value, so, you can think that the player will choose “The 1st Option” or “The 2nd Option” or “The 7 th option” no matter what the option value finally be decribed on the panel.

Image that there is a option value with 5 options in total, the first one is English, the second one is Chinese and so on, in data structure layer, you just to fill a option array, the index 0 corresponds “Chinese” and the index 1 corrsponds “English”.

The widget need to know what the option index is now, so we need the method GetDiscreteOptionIndex, and as we have discussed by bofore, it need a default value, we also need a GetDiscreteOptionDefaultIndex() here.

You also need to change the index with the widget, note that change the index here doesn’t mean change the interval value, it would not change the value it manages interval unless the player presses “Apply”.

What is worth mentioning here is the method GetAnalyticsValue, it is the only one which has an implmentation in this class:

FString UGameSettingValueDiscrete::GetAnalyticsValue() const
{
	const TArray<FText> Options = GetDiscreteOptions();
	const int32 CurrentOptionIndex = GetDiscreteOptionIndex();
	if (Options.IsValidIndex(CurrentOptionIndex))
	{
		const FString* SourceString = FTextInspector::GetSourceString(Options[CurrentOptionIndex]);
		if (SourceString)
		{
			return *SourceString;
		}
	}

	return TEXT("<Unknown Index>");
}

It is used to get the string from the text it displayed on the panel. In fact, this method doesn’t not used in project Lyra.

Back to the PURE_VIRTURAL we have mentioned before, in this class, there are several function uses it with return value, such as:

/** Optional */
UFUNCTION(BlueprintCallable)
virtual int32 GetDiscreteOptionDefaultIndex() const { return INDEX_NONE; }

Now we see the usage of the second param of the macro PURE_VIRTURAL.

 

3. GameplaySettingValueDiscreteDynamic

 

From now on, GameplaySettingValueDiscreteDynamic is not an abstract class, and there do exists objects of this type instead of its sub classes like GameplaySettingValueDiscreteDynamic_Bool, GameplaySettingValueDiscreteDynamic_Enum and so on:

UGameSettingValueDiscreteDynamic* Setting = NewObject<UGameSettingValueDiscreteDynamic>();
			Setting->SetDevName(TEXT("ControllerHardware"));
			Setting->SetDisplayName(LOCTEXT("ControllerHardware_Name", "Controller Hardware"));
			Setting->SetDescriptionRichText(LOCTEXT("ControllerHardware_Description", "The type of controller you're using."));
			Setting->SetDynamicGetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(GetControllerPlatform));
			Setting->SetDynamicSetter(GET_LOCAL_SETTINGS_FUNCTION_PATH(SetControllerPlatform));

The design philosophy of GameplaySettingValueDiscreteDynamic is that GameplaySettingValueDiscreteDynamic contains a dynamic getter and setter to get and set the value from the interval data structure, which means that to understand GameplaySettingValueDiscreteDynamic, we need to learn about the dynamic getter and setter:

//--------------------------------------
// FGameSettingDataSource
//--------------------------------------

class GAMESETTINGS_API FGameSettingDataSource : public TSharedFromThis<FGameSettingDataSource>
{
public:
	virtual ~FGameSettingDataSource() { }

	/**
	 * Some settings may take an async amount of time to finish initializing.  The settings system will wait
	 * for all settings to be ready before showing the setting.
	 */
	virtual void Startup(ULocalPlayer* InLocalPlayer, FSimpleDelegate StartupCompleteCallback) { StartupCompleteCallback.ExecuteIfBound(); }

	virtual bool Resolve(ULocalPlayer* InContext) = 0;

	virtual FString GetValueAsString(ULocalPlayer* InContext) const = 0;

	virtual void SetValue(ULocalPlayer* InContext, const FString& Value) = 0;

	virtual FString ToString() const = 0;
};

This class is an abstract class, it has only 1 implementation in Lyra : FGameplaySettingDataSourceDynamic:

//--------------------------------------
// FGameSettingDataSourceDynamic
//--------------------------------------

class GAMESETTINGS_API FGameSettingDataSourceDynamic : public FGameSettingDataSource
{
public:
	FGameSettingDataSourceDynamic(const TArray<FString>& InDynamicPath);

	virtual bool Resolve(ULocalPlayer* InLocalPlayer) override;

	virtual FString GetValueAsString(ULocalPlayer* InLocalPlayer) const override;

	virtual void SetValue(ULocalPlayer* InLocalPlayer, const FString& Value) override;

	virtual FString ToString() const override;

private:
	FCachedPropertyPath DynamicPath;
};

FCachedPropertyPath allows you to get a property by path, it can also finally points to a function which returns a property:

else if(UFunction* Function = Field.Get<UFunction>())
		{
			return FCallGetterFunctionAsStringHelper<ContainerType>::CallGetterFunction(InContainer, Function, OutValue);
		}
		else if(FProperty* Property = Field.Get<FProperty>())
		{
			ArrayIndex = ArrayIndex == INDEX_NONE ? 0 : ArrayIndex;
			if( ArrayIndex < Property->ArrayDim )
			{
				if ( void* ValuePtr = Property->ContainerPtrToValuePtr<void>(InContainer, ArrayIndex) )
				{
					OutProperty = Property;
					OutProperty->ExportTextItem_Direct(OutValue, ValuePtr, nullptr, nullptr, 0);
					return true;
				}
			}
		}

It’s a expensive method so you should not used it frequently, especially in Tick logic, however, it is acceptable in gameplay settings here.

For example, In ULyraLocalPlayyer, there is a method called GetLocalSettings() which returns the member of ULyraLocalSetting type, the latter has a method can return a property, lyra provides a macro to do that:

#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)		\
	}))

the GET_FUNCTION_NAME_STRING_CHECKED contains a comma expression, used for check if the function exists:

#define GET_FUNCTION_NAME_STRING_CHECKED(ClassName, FunctionName) \
	((void)sizeof(&ClassName::FunctionName), TEXT(#FunctionName))

Back to the GameplaySettingValueDiscreteDynamic, it is declared as:

UCLASS()
class GAMESETTINGS_API UGameSettingValueDiscreteDynamic : public UGameSettingValueDiscrete
{
	GENERATED_BODY()
	
public:
	UGameSettingValueDiscreteDynamic();

	/** UGameSettingValue */
	virtual void Startup() override;
	virtual void StoreInitial() override;
	virtual void ResetToDefault() override;
	virtual void RestoreToInitial() override;

	/** UGameSettingValueDiscrete */
	virtual void SetDiscreteOptionByIndex(int32 Index) override;
	virtual int32 GetDiscreteOptionIndex() const override;
	virtual int32 GetDiscreteOptionDefaultIndex() const override;
	virtual TArray<FText> GetDiscreteOptions() const override;

	/** UGameSettingValueDiscreteDynamic */
	void SetDynamicGetter(const TSharedRef<FGameSettingDataSource>& InGetter);
	void SetDynamicSetter(const TSharedRef<FGameSettingDataSource>& InSetter);
	void SetDefaultValueFromString(FString InOptionValue);
	void AddDynamicOption(FString InOptionValue, FText InOptionText);
	void RemoveDynamicOption(FString InOptionValue);
	const TArray<FString>& GetDynamicOptions();

	bool HasDynamicOption(const FString& InOptionValue);

	FString GetValueAsString() const;
	void SetValueFromString(FString InStringValue);

protected:
	void SetValueFromString(FString InStringValue, EGameSettingChangeReason Reason);

	/** UGameSettingValue */
	virtual void OnInitialized() override;

	void OnDataSourcesReady();

	bool AreOptionsEqual(const FString& InOptionA, const FString& InOptionB) const;

protected:
	TSharedPtr<FGameSettingDataSource> Getter;
	TSharedPtr<FGameSettingDataSource> Setter;

	TOptional<FString> DefaultValue;
	FString InitialValue;

	TArray<FString> OptionValues;
	TArray<FText> OptionDisplayTexts;
};

Not complicated if you have read the content below, what is worth mentioning here is that it use SetValueFromString() method to change the value interval, that is because the FGameSettingDataSourceDynamic finally calls PropertyPathHelper::SetPropertyValueFromString to set the value:

void FGameSettingDataSourceDynamic::SetValue(ULocalPlayer* InLocalPlayer, const FString& InStringValue)
{
	const bool bSuccess = PropertyPathHelpers::SetPropertyValueFromString(InLocalPlayer, DynamicPath, InStringValue);
	ensure(bSuccess);
}

Which means that no matter what the type of the interval property is, you must use string to set value even if it is a float or enum value.

You can inherit the FGameSettingDataSource and change the behaviour of SetValue for properties which cannot use PropertyPathHelper::SetPropertyValueFromString to set their value directly in the derived class.

Understanding this feature is benefit for the analyzation of GameplaySettingValueDiscreteDynamic_Enum in the following content.

Now you can create a gameplay setting easily with this class:

{
	UGameSettingValueDiscreteDynamic_Bool* Setting = NewObject<UGameSettingValueDiscreteDynamic_Bool>();
	Setting->SetDevName(TEXT("Subtitles"));
	Setting->SetDisplayName(LOCTEXT("Subtitles_Name", "Subtitles"));
	Setting->SetDescriptionRichText(LOCTEXT("Subtitles_Description", "Turns subtitles on/off."));

	Setting->SetDynamicGetter(GET_SHARED_SETTINGS_FUNCTION_PATH(GetSubtitlesEnabled));
	Setting->SetDynamicSetter(GET_SHARED_SETTINGS_FUNCTION_PATH(SetSubtitlesEnabled));
	Setting->SetDefaultValue(GetDefault<ULyraSettingsShared>()->GetSubtitlesEnabled());

	SubtitleCollection->AddSetting(Setting);
}

GameplaySettingValueDiscreteDynamic has a several sub classes, we take GameplaySettingValueDiscreteDynamic_Enum as an example.

 

4.GameplaySettingValueDiscreteDynamic_Enum

 

This class is declared as:

UCLASS()
class GAMESETTINGS_API UGameSettingValueDiscreteDynamic_Enum : public UGameSettingValueDiscreteDynamic
{
	GENERATED_BODY()

public:
	UGameSettingValueDiscreteDynamic_Enum();

public:
	template<typename EnumType>
	void SetDefaultValue(EnumType InEnumValue)
	{
		const FString StringValue = StaticEnum<EnumType>()->GetNameStringByValue((int64)InEnumValue);
		SetDefaultValueFromString(StringValue);
	}

	template<typename EnumType>
	void AddEnumOption(EnumType InEnumValue, const FText& InOptionText)
	{
		const FString StringValue = StaticEnum<EnumType>()->GetNameStringByValue((int64)InEnumValue);
		AddDynamicOption(StringValue, InOptionText);
	}

	template<typename EnumType>
	EnumType GetValue() const
	{
		const FString Value = GetValueAsString();
		return (EnumType)StaticEnum<EnumType>()->GetValueByNameString(Value);
	}

	template<typename EnumType>
	void SetValue(EnumType InEnumValue)
	{
		const FString StringValue = StaticEnum<EnumType>()->GetNameStringByValue((int64)InEnumValue);
		SetValueFromString(StringValue);
	}

protected:
	/** UGameSettingValue */
	virtual void OnInitialized() override;
};

SetDefaultValue and AddEnumOption methods are encapsulation of methods in its parent class, which simply transfer the enum value to its corresponding string value.

SetValue is a shortcut method, there is no usage in Lyra, GetValue is a shortcut method too, this method is used when creating condition:

Setting->AddEditCondition(FWhenPlatformHasTrait::KillIfMissing(TAG_Platform_Trait_SupportsWindowedMode, TEXT("Platform does not support window mode")));
Setting->AddEditCondition(MakeShared<FWhenCondition>([WindowModeSetting](const ULocalPlayer*, FGameSettingEditableState& InOutEditState)
{
	if (WindowModeSetting->GetValue<EWindowMode::Type>() == EWindowMode::WindowedFullscreen)
	{
		InOutEditState.Disable(LOCTEXT("ResolutionWindowedFullscreen_Disabled", "When the Window Mode is set to <strong>Windowed Fullscreen</>, the resolution must match the native desktop resolution."));
	}
}));

The following example add a enum type option to the option collection:

UGameSettingValueDiscreteDynamic_Enum* Setting = NewObject<UGameSettingValueDiscreteDynamic_Enum>();
					Setting->SetDevName(TEXT("SubtitleTextColor"));
					Setting->SetDisplayName(LOCTEXT("SubtitleTextColor_Name", "Text Color"));
					Setting->SetDescriptionRichText(LOCTEXT("SubtitleTextColor_Description", "Choose different colors for the subtitle text."));

					Setting->SetDynamicGetter(GET_SHARED_SETTINGS_FUNCTION_PATH(GetSubtitlesTextColor));
					Setting->SetDynamicSetter(GET_SHARED_SETTINGS_FUNCTION_PATH(SetSubtitlesTextColor));
					Setting->SetDefaultValue(GetDefault<ULyraSettingsShared>()->GetSubtitlesTextColor());
					Setting->AddEnumOption(ESubtitleDisplayTextColor::White, LOCTEXT("ESubtitleTextColor_White", "White"));
					Setting->AddEnumOption(ESubtitleDisplayTextColor::Yellow, LOCTEXT("ESubtitleTextColor_Yellow", "Yellow"));

					SubtitleCollection->AddSetting(Setting);

Now, you understant the designing philosophy of GameplaySetting, you can easily extend it after reading its source code, in the following posts, we are going to dive into the conditions and setting entries.

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

One thought on “Gameplay Settings in Lyra : A Down-Top Approach(2)”

Leave a Reply

Your email address will not be published. Required fields are marked *