Lyra is composed of many modules, but there are only two of them belongs to the project, the others are plugins for reusable, in this series of posts, we will introduce to GameSettings and discuss about the usage of it in Lyra.

 

1. Game Settings Plugin Overview

 

You can find the documentation here : Game Settings Documentation, However, this documentation is to simple to understand the structure of the plugin.

There are several key parts of the framework:

  • UGameSettingRegistry – The Game Setting Registry is a set of settings. A single registry can be exposed partially or in its entirety to the user, however, you will be required to register at least one of your Game’s Settings. If there are other systems in your game that require settings that do not have primary game settings, then we recommend you use another registry.
  • UGameSetting – Defines the base class of all settings. Any setting in the list of the UI is considered a Game Setting. This class handles core concepts like name, description, dependencies, and edit conditions.
  • UGameSettingValue – Implements the base class of any setting that has a value that needs to be get and set. A few settings will directly inherit from this class, instead, they’ll use:
  • UGameSettingCollection – Defines organization settings. A Game Setting Collection is used to group settings together. For example, if you wanted a collection to generate headers in a list, then that collection represents that headered group.
  • FGameSettingEditCondition – Each setting has a set of attached edit conditions. These edit conditions provide you with the ability to encode logic that queries if a setting is disabled, hidden, or destroyed.

We will introduce them on by one in the following.

 

2. UGameSetting Overview

 

A gameplay setting is the item you can see from the setting panel, game settings can have different widget combined with it, as an example, the highlight UI widget in the following image represents a game setting, it used to create a combination to the implementation of the setting. If you treat the game setting as a MVC model, it should be a Control.

Each widget here represents a game setting

It inherits from the UObject directly, which is declared as:

UCLASS(Abstract, BlueprintType)
class GAMESETTINGS_API UGameSetting : public UObject
{
	GENERATED_BODY()

public:
	UGameSetting() { }

public:
	DECLARE_EVENT_TwoParams(UGameSetting, FOnSettingChanged, UGameSetting* /*InSetting*/, EGameSettingChangeReason /*InChangeReason*/);
	DECLARE_EVENT_OneParam(UGameSetting, FOnSettingApplied, UGameSetting* /*InSetting*/);
	DECLARE_EVENT_OneParam(UGameSetting, FOnSettingEditConditionChanged, UGameSetting* /*InSetting*/);

	FOnSettingChanged OnSettingChangedEvent;
	FOnSettingApplied OnSettingAppliedEvent;
	FOnSettingEditConditionChanged OnSettingEditConditionChangedEvent;

...
...

UGameSetting is a relatively large class with about 250 lines for its .h file, it has many child class, the inheritance tree in Lyra is as followed:

We take ULyraSettingValueDiscrete_Language as an example to introduce how a game setting is displayed on the UI Panel in the following posts.

But first, let’s take a look at the members in UGameSetting:

 

3. Members of UGameSetting

 

Name, DisplayName & Visibility

First of all, UGameSetting has a dev name, this dev name should be unique and remain constant, this data-member is used for register it in a setting registry, which will be introduced in the future,  now,  you can simply treat it as a set of game setting:

/**
* Gets the non-localized developer name for this setting.  This should remain constant, and represent a 
* unique identifier for this setting inside this settings registry.
*/
UFUNCTION(BlueprintCallable)
FName GetDevName() const { return DevName; }
void SetDevName(const FName& Value) { DevName = Value; }

It also contains methods for display names, in non-shipping version, it can be provided by a FString param.

UFUNCTION(BlueprintCallable)
	FText GetDisplayName() const { return DisplayName; }
	void SetDisplayName(const FText& Value) { DisplayName = Value; }
#if !UE_BUILD_SHIPPING
	void SetDisplayName(const FString& Value) { SetDisplayName(FText::FromString(Value)); }
#endif
	UFUNCTION(BlueprintCallable)
	ESlateVisibility GetDisplayNameVisibility() { return DisplayNameVisibility; }
	void SetNameDisplayVisibility(ESlateVisibility InVisibility) { DisplayNameVisibility = InVisibility; }

An exaple is given as followed:

ULyraSettingValueDiscrete_Language* Setting = NewObject<ULyraSettingValueDiscrete_Language>();
			Setting->SetDevName(TEXT("Language"));
			Setting->SetDisplayName(LOCTEXT("LanguageSetting_Name", "Language"));
			Setting->SetDescriptionRichText(LOCTEXT("LanguageSetting_Description", "The language of the game."));

These codes finally be displayed as:

Descriptions & Details

UFUNCTION(BlueprintCallable)
	FText GetDescriptionRichText() const { return DescriptionRichText; }
	void SetDescriptionRichText(const FText& Value) { DescriptionRichText = Value; InvalidateSearchableText(); }
#if !UE_BUILD_SHIPPING
	/** This version is for cheats and other non-shipping items, that don't need to localize their text.  We don't permit this in shipping to prevent unlocalized text being introduced. */
	void SetDescriptionRichText(const FString& Value) { SetDescriptionRichText(FText::FromString(Value)); }
#endif

	UFUNCTION(BlueprintCallable)
	const FGameplayTagContainer& GetTags() const { return Tags; }
	void AddTag(const FGameplayTag& TagToAdd) { Tags.AddTag(TagToAdd); }

A description is used to inform users about the specific function of a game setting:

ULyraSettingValueDiscrete_Language* Setting = NewObject<ULyraSettingValueDiscrete_Language>();
			Setting->SetDevName(TEXT("Language"));
			Setting->SetDisplayName(LOCTEXT("LanguageSetting_Name", "Language"));
			Setting->SetDescriptionRichText(LOCTEXT("LanguageSetting_Description", "The language of the game."));
			
#if WITH_EDITOR
			if (GIsEditor)
			{
				Setting->SetDescriptionRichText(LOCTEXT("LanguageSetting_WithEditor_Description", "The language of the game.\n\n<text color=\"#ffff00\">WARNING: Language changes will not affect PIE, you'll need to run with -game to test this, or change your PIE language options in the editor preferences.</>"));
			}
#endif

It is shown as:

GameSetting also has a dynamic text and a warning text, the previous one is used for dynamic text, which is changed when the exactly value of the setting is changed, and the other one is for info which used for warning players, for example, inform that Ray Tracing is expensive and DLSS is only works for Nvidia users:

/** Set the dynamic details callback, we query this when building the description panel.  This text is not searchable.*/
	void SetDynamicDetails(const FGetGameSettingsDetails& InDynamicDetails) { DynamicDetails = InDynamicDetails; }

	/**
	 * Gets the dynamic details about this setting.  This may be information like, how many refunds are remaining 
	 * on their account, or the account number.
	 */
	UFUNCTION(BlueprintCallable)
	FText GetDynamicDetails() const;

	UFUNCTION(BlueprintCallable)
	FText GetWarningRichText() const { return WarningRichText; }
	void SetWarningRichText(const FText& Value) { WarningRichText = Value; InvalidateSearchableText(); }
#if !UE_BUILD_SHIPPING
	/** This version is for cheats and other non-shipping items, that don't need to localize their text.  We don't permit this in shipping to prevent unlocalized text being introduced. */
	void SetWarningRichText(const FString& Value) { SetWarningRichText(FText::FromString(Value)); }
#endif

For these complicated property, like color blind requires for complex detail widget, which is implemented with a class called “UGameSettingDetailExtension”:

 

4. Events

 

There are many delegates in UGameSettings:

DECLARE_EVENT_TwoParams(UGameSetting, FOnSettingChanged, UGameSetting* /*InSetting*/, EGameSettingChangeReason /*InChangeReason*/);
DECLARE_EVENT_OneParam(UGameSetting, FOnSettingApplied, UGameSetting* /*InSetting*/);
DECLARE_EVENT_OneParam(UGameSetting, FOnSettingEditConditionChanged, UGameSetting* /*InSetting*/);

FOnSettingChanged OnSettingChangedEvent;
FOnSettingApplied OnSettingAppliedEvent;
FOnSettingEditConditionChanged OnSettingEditConditionChangedEvent;	

 

Setting Chaned

Change a game setting calls “NotifySettingChanged”, and NotifySettingChanged calls OnSettingChanged intervally, and  broadcast OnSettingChanged. Note that changed a setting is not equavial with apply a change, only after you press apply button calls Apply().

/** Notify that the setting changed */
void NotifySettingChanged(EGameSettingChangeReason Reason);
virtual void OnSettingChanged(EGameSettingChangeReason Reason);

Which is implemented as:

void UGameSetting::NotifySettingChanged(EGameSettingChangeReason Reason)
{
	OnSettingChanged(Reason);
	
	// Run through any edit conditions and let them know things changed.
	for (const TSharedRef<FGameSettingEditCondition>& EditCondition : EditConditions)
	{
		EditCondition->SettingChanged(LocalPlayer, this, Reason);
	}

	if (!bOnSettingChangedEventGuard)
	{
		TGuardValue<bool> Guard(bOnSettingChangedEventGuard, true);
		OnSettingChangedEvent.Broadcast(this, Reason);
	}
}

void UGameSetting::OnSettingChanged(EGameSettingChangeReason Reason)
{
	// No-Op
}

 

Apply

Apply is used to implement the interval property changed, and for edit coniditons changed notify and special notify, the following is an example, after you apply the change of the language, it will push a special panel and tell the players to restart the game:

void UGameSetting::Apply()
{
	OnApply();

	// Run through any edit conditions and let them know things changed.
	for (const TSharedRef<FGameSettingEditCondition>& EditCondition : EditConditions)
	{
		EditCondition->SettingApplied(LocalPlayer, this);
	}

	OnSettingAppliedEvent.Broadcast(this);
}

void UGameSetting::OnApply()
{
	// No-Op by default.
}

As for the change of the property? well, it is implemented in children classes, for example, an implementation of UGameSetting, UGameSettingDiscreteDynamic use a data-bind method to accomplish the automatic change of the property:

void SetDynamicGetter(const TSharedRef<FGameSettingDataSource>& InGetter);
void SetDynamicSetter(const TSharedRef<FGameSettingDataSource>& InSetter);

 

Startup & Init

Startup and OnInitialized are virtural methods, the call stacks from down to top is Initialize->Startup->StartupComplete->OnInitialized

void UGameSetting::Initialize(ULocalPlayer* InLocalPlayer)
{
	// If we've already gotten this local player we're already initialized.
	if (LocalPlayer == InLocalPlayer)
	{
		return;
	}

	LocalPlayer = InLocalPlayer;

	//TODO: GameSettings
	//LocalPlayer->OnPlayerLoggedIn().AddUObject(this, &UGameSetting::RefreshEditableState, true);

#if !UE_BUILD_SHIPPING
	ensureAlwaysMsgf(DevName != NAME_None, TEXT("You must provide a DevName for the setting."));
	ensureAlwaysMsgf(!DisplayName.IsEmpty(), TEXT("You must provide a DisplayName for settings."));
#endif

	for (const TSharedRef<FGameSettingEditCondition>& EditCondition : EditConditions)
	{
		EditCondition->Initialize(LocalPlayer);
	}

	// If there are any child settings go ahead and initialize them as well.
	for (UGameSetting* Setting : GetChildSettings())
	{
		Setting->Initialize(LocalPlayer);
	}

	Startup();
}

void UGameSetting::Startup()
{
	StartupComplete();
}

void UGameSetting::StartupComplete()
{
	ensureMsgf(!bReady, TEXT("StartupComplete called twice."));

	if (!bReady)
	{
		bReady = true;
		OnInitialized();
	}
}

There is no override for Startup() in lyra, all logic for initialized are in OnInitialized().

 

EditCondition

/** Any edit conditions for this setting. */
TArray<TSharedRef<FGameSettingEditCondition>> EditConditions;

/** Adds a new edit condition to this setting, allowing you to control the visibility and edit-ability of this setting. */
void AddEditCondition(const TSharedRef<FGameSettingEditCondition>& InEditCondition);

/** Add setting dependency, if these settings change, we'll re-evaluate edit conditions for this setting. */
void AddEditDependency(UGameSetting* DependencySetting);

void HandleEditDependencyChanged(UGameSetting* DependencySetting, EGameSettingChangeReason Reason);
void HandleEditDependencyChanged(UGameSetting* DependencySetting);

/** Notify that the settings edit conditions changed.  This may mean it's now invisible, or disabled, or possibly that the options have changed in some meaningful way. */
void NotifyEditConditionsChanged();
virtual void OnEditConditionsChanged();

So what’s the different between EditCondition and EditDependency? basically, EditCondition used for those situations that if a setting does not match the condition, it should be diabled. While EditDependency used for Adding setting dependency, if these settings change, we’ll re-evaluate edit conditions for this setting.

void UGameSetting::RefreshEditableState(bool bNotifyEditConditionsChanged)
{
	// The LocalPlayer may be destroyed out from under us, if that happens,
	// we need to ignore attempts to refresh the editable state.
	if (!LocalPlayer)
	{
		return;
	}

	//TODO: GameSettings
	//// We should wait until the player is fully logged in before trying to refresh settings.
	//if (!LocalPlayer->IsLoggedIn())
	//{
	//	return;
	//}

	if (!bOnEditConditionsChangedEventGuard)
	{
		TGuardValue<bool> Guard(bOnEditConditionsChangedEventGuard, true);
	
		EditableStateCache = ComputeEditableState();

		if (bNotifyEditConditionsChanged)
		{
			NotifyEditConditionsChanged();
		}		
	}
}

void UGameSetting::NotifyEditConditionsChanged()
{
	OnEditConditionsChanged();

	OnSettingEditConditionChangedEvent.Broadcast(this);
}

void UGameSetting::OnEditConditionsChanged()
{

}

void UGameSetting::HandleEditDependencyChanged(UGameSetting* DependencySetting)
{
	OnDependencyChanged();
	RefreshEditableState();
}

void UGameSetting::HandleEditDependencyChanged(UGameSetting* DependencySetting, EGameSettingChangeReason Reason)
{
	OnDependencyChanged();
	RefreshEditableState();

	if (Reason != EGameSettingChangeReason::DependencyChanged)
	{
		NotifySettingChanged(EGameSettingChangeReason::DependencyChanged);
	}
}

void UGameSetting::OnDependencyChanged()
{

}

By JiahaoLi

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

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

Leave a Reply

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