Player settings
Every plugin and the game register their player settings (subtitles, volume, key bindings, graphics) in one registry per local player. Any settings menu shows them without knowing where they come from; PulseCore’s settings screen is one such menu, a game can build its own on the same view models.
How it fits together
- Descriptor (
FPulseSettingDescriptor): id (gameplay tag), category, name, description, type, options, range, default, apply mode, conditions and source. - Source (
FPulseSettingSource, instanced struct): where the value lives. Plugins can add their own sources. - Registry (
UPulseSettingsRegistry, owned byUPulseSettingsSubsystemper local player): current and pending values, defaults, conditions, saving, events. - View models (
UMVVMPulseSettingsinPulseFoundationMVVM): tabs, sections and settings for UI.
Values are strings in a canonical form: Bool true/false, numbers 42 / 0.5, Options the option value, Key the key name (SpaceBar), Color (R=..,G=..,B=..,A=..).
Registering settings
1. A UPulsePlayerSettings class (plugins and game code)
Derive from UPulsePlayerSettings, add normal properties (not Config) and declare them. Every subclass is registered automatically (bAutoRegisterPlayerSettingsClasses). The class defaults are the factory defaults; each local player gets its own instance.
virtual void DeclareSettings(FPulsePlayerSettingsDeclarer& Declarer) const override
{
using namespace Pulse::Foundation::Tags;
Declarer.Add(GET_MEMBER_NAME_CHECKED(ThisClass, bShowSubtitles), TAG_Pulse_Setting_Dialog_Subtitles)
.Category(SettingCategory_Interface)
.DisplayName(LOCTEXT("Subtitles", "Subtitles"));
Declarer.Add(GET_MEMBER_NAME_CHECKED(ThisClass, SubtitleSize), TAG_Pulse_Setting_Dialog_SubtitleSize)
.Category(SettingCategory_Interface)
.Range(0.5f, 2.f, 0.1f).Percent()
.EnabledIf(TAG_Pulse_Setting_Dialog_Subtitles);
}
Supported property types: bool, integers, float / double, enums (options from the enum), FString, FText, FName, FKey, FLinearColor, FColor. Builder functions: DisplayName, Description, Category, Order, Range, Format, Percent, Option, RequiresRestart, ApplyOnConfirm, Condition, EnabledIf.
Read values with UPulseSettingsSubsystem::GetPlayerSettings<T>(LocalPlayer) (falls back to the class defaults without local player). UPulsePlayerSettings::OnSettingWritten is called on the instance after each change.
2. A UPulseSettingsCollection asset (no C++)
Create a Pulse Settings Collection data asset, add descriptors with a source (console variable, game user settings, sound class volume, key mapping, custom provider) and list the asset in Project Settings > Pulse > Foundation > Settings Collections. The asset is validated on save (ids, sources, names).
Example with a Blueprint provider (custom source) and a game user setting (see Examples):
3. At runtime
UPulseSettingsSubsystem::RegisterSetting / UnregisterSetting, or bind UPulseSettingsSubsystem::OnGatherSettings() at module startup and register on GetGatheringSubsystem() (for settings that depend on the machine, e.g. audio devices).
Sources
| Source | Value | Saved by |
|---|---|---|
| Player Settings Property | Property of a UPulsePlayerSettings instance | Registry |
| Console Variable | Any console variable (bools are written as 1/0) | Registry, applied again on start |
| Game User Settings | Window mode, resolution, VSync, frame rate limit, resolution scale, dynamic resolution, quality levels | UGameUserSettings::SaveSettings |
| Sound Class Volume | Sound class volume through a sound mix override (0..1) | Registry, applied again after each map change |
| Input Key Mapping | Key of a player mappable key mapping, per slot | Enhanced Input user settings (enable User Settings in the Enhanced Input project settings) |
| Custom Provider | A UPulseSettingValueProvider Blueprint or C++ class | The provider (bStoreValue lets the registry store it) |
| Stored Value | Nothing behind it: code reads the value with Get Bool Value and friends (the toggle settings of hold actions) | Registry |
For Player Settings Property, pick the Settings Class first: Property Name then becomes a drop-down with the properties of that class (and its parents below UPulsePlayerSettings) whose type can be a setting: bool, number, enum, string, text, name, key and color. A name that is not on the class or has another type is shown in red. In C++, FPulseSettingSource_Property::GetSupportedProperties returns the same list.
Values the registry stores are in GameUserSettings.ini, section [/Script/PulseFoundation.PulseStoredPlayerSettings]. Local players after the first one use the keys P<Index>:<Id>, so split-screen players keep their own values.
Categories
Tabs are the categories directly below Pulse.SettingCategory: Gameplay, Video, Audio, Controls, Interface, Accessibility exist natively. Deeper categories (Pulse.SettingCategory.Audio.Voice) become sections of their tab. Names come from Category Display Names in the project settings or from the last part of the tag.
Apply modes and conditions
Immediate(default): written at once, saved one second after the last change.OnApply: the value stays pending untilApplyChanges;RevertChangesdrops it. Use it for resolution and window mode.PendingChangesRequireRestarttells a menu to show a restart hint.- Conditions (
FPulseSettingCondition): Setting Value (another setting has one of some values) and Platform. Each condition either hides or disables the setting;bInvertflips it.
What Apply does
ApplyChanges (C++ ApplyWithResult of the registry, Blueprint Apply Changes With Result of the subsystem) checks every pending value before anything is written, through FPulseSettingSource::CanWriteValue:
- Values the source does not take (a resolution it cannot parse, a key that does not exist) stay pending: the value is wrong and the player can pick another one or discard it. Until then
GetValue, the subsystem getters, the view models and the Setting Value condition show the pending value, as for every pending value. Gameplay that needs the value really in effect readsGetAppliedValue. - Values whose source has nothing to write to (the console variable, property, settings object, provider, sound mix or key mapping is missing) are dropped: no value would work.
- All other values are written, in the order of the settings menu. A value the source fails to write (a key mapping that fails, a property that does not import, a provider that returns false) is dropped and reported, never kept: it would block every later Apply.
The result (FPulseResult) is a failure when a value was not applied; its details are an FPulseSettingsApplyReport (written settings and the rejected values with reason, message and whether they are still pending). Every refusal is logged once. ApplyChanges without result does the same and ignores the result.
The change events (OnSettingChanged, Pulse.Message.Settings.Changed) come after all values of the Apply are written, so a listener of the first change already sees the others. Callbacks of the sources themselves (UPulsePlayerSettings::OnSettingWritten, console variable sinks, a provider) still run while the value is written; they see the pending values that are left, and values they set through the registry stay. The registry events their own changes cause wait until the plan’s events are sent; what the engine broadcasts inside a source (console variable sinks, the OnSettingsChanged of Enhanced Input) runs during the write.
Own sources override CanWriteValue with the cheap checks their WriteValue makes first (return RefusesValue or TargetMissing with a message, write nothing). The default accepts every value. Key settings take an empty value or None as an unbind; a slot without a mapping in a row that has mappings is fine (the write creates it).
Several values in one step
ResetToDefaults (C++ ResetToDefaultsWithResult, Blueprint Reset To Defaults With Result) and the registry’s SetValues change several settings as one step: the conditions of each setting see the values of this change that are taken, not the state while the settings are set one by one. So the order of the settings does not matter: a setting that is enabled only while another one is on is reset as well when the reset turns that one on, and a value that a refused value would have disabled comes back. A reset leaves disabled settings as they are (listed as Skipped); SetValues rejects them (Pulse.Result.Failure.Conditions). Values the source does not take are rejected, and settings whose condition depends on such a value see the current value instead. Immediate values are written also when they equal the live value, so a stored value that drifted is reset too. Both return a result like Apply.
SetValues(Values, /*bInAllOrNothing*/ true) changes nothing when one value is not taken (the others are reported with Pulse.Result.Failure.Cancelled), for example to swap two keys. It covers the checks before writing; a source that fails while writing cannot be rolled back.
Own conditions read other settings through FPulseSettingContext::GetValue, which returns the value a plan is about to set before the registry’s value; Registry->GetValue would not see the step as a whole.
Reacting to changes
UPulseSettingsSubsystem::OnSettingChanged(Blueprint) /GetRegistry()->OnSettingChanged(C++).- Message
Pulse.Message.Settings.ChangedwithFPulseSettingChangedMessage(setting, value, local player).
View models
Create UMVVMPulseSettings with the Pulse Player View Model Resolver in a settings widget. It offers Tabs (each UMVVMPulseSettingsCategory with Settings and Sections), bHasPendingChanges, bPendingChangesRequireRestart, ApplyChanges, RevertChanges and ResetToDefaults. After ApplyChanges, LastApplyResult holds the result (details FPulseSettingsApplyReport) and LastApplyMessage its text for the player (empty on success), so a menu can show what was not applied.
UMVVMPulseSetting has everything an entry widget needs: DisplayName, Description, ValueType, ValueText, bBoolValue, NumberValue, NormalizedValue (sliders), Options, SelectedIndex, KeyValue, bIsEnabled, bIsVisible, bIsPending, bIsDefault, and the setters SetBoolValue, SetNumberValue, SetNormalizedValue, SelectOption, SelectNextOption, SelectPreviousOption, SetKeyValue, ResetToDefault.