Utilities
Handles
FPulseHandle is a process-wide unique number for things that are added and removed again (listeners, markers, requests). FPulseHandle::Generate() creates one; 0 is invalid. Never save handles. Blueprint: Is Valid, Invalidate, ==, !=, To String.
Instanced struct sets
FPulseInstancedStructSet holds at most one instanced struct per type, e.g. options or traits of an asset. Lookups match derived types.
UPROPERTY(EditAnywhere, meta = (BaseStruct = "/Script/MyModule.MyOptionBase", ExcludeBaseStruct))
FPulseInstancedStructSet Options;
if (const FMyRangeOption* range = Options.Find<FMyRangeOption>()) { ... }
For plain TArray<FInstancedStruct> use Pulse::Structs::SetUniqueByType, FindByType, RemoveByType.
Async loading
Pulse::Loading::FLoadBundle loads a set of soft paths with progress and keeps them loaded until it is reset or destroyed. Blueprint: Load Assets Async With Progress (On Progress 0..1, On Completed with the loaded assets).
Loading holds
Anything that must finish before the game shows (a save that loads, shaders that compile, a world that generates) adds a loading hold to UPulseLoadingHoldSubsystem (Loading/PulseLoadingHoldSubsystem.h, game instance). A loading screen stays up while holds exist: the one of PulseFrontend does, a screen of your own reads the subsystem.
UPulseLoadingHoldSubsystem* holds = UPulseLoadingHoldSubsystem::Get(this);
const FPulseHandle hold = holds->AddHold(TEXT("Shaders"), LOCTEXT("Shaders", "Preparing shaders"), 10);
holds->SetProgress(hold, 0.4f); // 0 to 1, below 0 while unknown (the start value)
holds->RemoveHold(hold);
Blueprint: Add Loading Hold (reason, text, priority), Set Loading Hold Progress, Set Loading Hold Text, Remove Loading Hold, Has Loading Holds, Get Loading Holds Progress and Get Loading Holds Text.
- Progress of all holds is the mean of the holds that know their progress; a hold without progress does not hide the bar of the others. It is unknown (below 0) while no hold knows its progress, so a screen shows a throbber.
- Text: the screen shows the text of the most important hold, the one with the highest priority among the holds with a text (the oldest of them on a tie).
- Events: every change (added, progress, text, removed) goes to
OnHoldChangedNative,OnHoldChangedand the channelPulse.Message.Loading.HoldChanged(message channels). Progress is reported in steps of at least 1 %, and when it becomes known or unknown or reaches 1. - Holds belong to the machine that adds them; they are not replicated. The Pulse Debugger shows them under the category Loading.
The state is the plain struct FPulseLoadingHolds; the rules are pure functions in Pulse::Loading (AddHold, SetProgress, ComputeProgress, FindMostImportant, MakeMessage), so you can test a screen of your own without a game.
Server time
FPulseServerTime (Net/PulseServerTime.h) is the world time of the server on every machine, for clocks that must agree in multiplayer (time of day, water waves). The server and standalone games read their own world time. A client reads the replicated server time of the game state, which jumps a little whenever an update arrives, and smooths the difference to its own world time. Keep one per world, for example in a world subsystem, call Update regularly (a timer is enough) and read Get:
ServerTime.Update(GetWorld(), DeltaSeconds);
const double serverSeconds = ServerTime.Get(GetWorld());
SnapThreshold (0.5 s) takes larger changes at once, SmoothingSpeed (2 per second) sets how fast the rest follows.
The clock subsystem keeps one for every game world: read it as the clock Server there instead of holding your own.
Replication mode
EPulseReplicationMode (Net/PulseReplicationMode.h) says which clients receive the state of a Pulse component: Owner Only, Everyone, or Default (what the plugin chose, for example the owning client for the achievements of a player and everyone for the world’s). Components register their properties with COND_Dynamic and set the condition once per instance:
void UMyComponent::GetReplicatedCustomConditionState(FCustomPropertyConditionState& OutActiveState) const
{
Super::GetReplicatedCustomConditionState(OutActiveState);
const ELifetimeCondition condition = Pulse::Replication::GetCondition(ReplicationMode, EPulseReplicationMode::OwnerOnly);
DOREPDYNAMICCONDITION_INITCONDITION_FAST(ThisClass, Items, condition);
}
The engine reads the condition when the component starts to replicate, so set the mode in the defaults or in a constructor; Pulse::Replication::CanChangeMode tells whether the component or its actor is still being constructed.
Grid
Pulse::Grid (Grid/PulseGrid.h) splits the X/Y plane into square cells with 64-bit coordinates (FInt64Point), for every plugin that groups the world into cells: replication cells, chunks, regions, environment windows. Cells use floor, so negative positions get negative cells without a doubled cell at zero, and 64-bit coordinates reach far beyond any world. Z is ignored.
| Function | What it does |
|---|---|
GetCell(Location, CellSize) | The cell of a location |
GetCellMin, GetCellCenter, GetCellBounds | Corner, center and bounds of a cell (X/Y) |
GetCellCullDistance(CellSize, Radius) | Net cull distance of an actor at the cell center, so every point of the cell within the radius counts |
GetDistanceSquaredToCell | Distance from a point to the nearest point of a cell |
GetCellsInRadius(Center, Radius, CellSize, MaxCells, OutCells) | Cells that touch a circle, nearest first, at most MaxCells |
GetCellsInDisk(Center, Radius, OutCells) | Cells within a radius counted in cells, the center first (rings of chunks) |
GetNeighbors, GetCellDistanceSquared | The 4 or 8 neighbors, distance in cells |
HashCell(Cell, Seed), Mix | A 64-bit hash per cell, the same on every machine (random values per cell come from it) |
GetWindowMin, GetWindowMove | A window of cells that follows a center and moves only beyond a slack |
Settings base class
Derive project settings from UPulseDeveloperSettings: they appear under Project Settings > Pulse. Give the plugin an accessor (Pulse::<Plugin>::GetSettings()).
One page per plugin
Project Settings > Pulse has one entry per plugin (Online, Core, Quest, …). Every settings class of the plugin is a block on its page, collapsed except the first; the settings of its extras (bridges) follow at the end, with the extra they come from. The classes keep their config sections: nothing in Default<Type>.ini changes.
- Block names come from the
DisplayNameof the class without the name of the page and without “Bridge”: “Online Sessions” on Online is Sessions, “AI Dialog Bridge” on AI is Dialog. The class named like the page is General and comes first. Categories of a class show as small headings inside its block. - Extras show on the page of the plugin named first in their name: Pulse Quest HUD Bridge on Quest.
- Changed values: a block header counts the values that differ from the defaults of the plugin (the values the C++ constructor sets). The arrow next to such a value sets the default again.
- Search, saving, check out and undo are the ones of the Project Settings; the bar “Save changes to” (Config files) shows once at the top of a page.
- Editor Preferences use the same pages (Editor Preferences > Pulse > Foundation: Cards, Config Workshop, …).
- Topics: every block has one (General, Gameplay, HUD & UI, Online, Saving, World), from its name and plugin: HUD, UI, View Models, Settings Menu and Cards are HUD & UI, Save blocks and Pulse Save are Saving, Online blocks and Pulse Online are Online, Time, Water, Stater, Camera and Faction are World, Pulse Foundation is General, the rest is Gameplay. A class can name its own with
meta = (PulseSettingsTopic = "Online"). The Pulse window shows the settings of every plugin by topic (All plugins > Settings) and filters the page of a plugin by them. - Settings derived directly from
UDeveloperSettingskeep a page of their own. Settings objects a module registers by hand under the category Pulse (a plainUObject) are taken onto the page of their plugin.
Texts that name a place use Pulse::Editor::Settings::GetSettingsLocation (“Project Settings > Pulse > Online > Sessions”, the General block is the page itself), buttons that open settings use Pulse::Editor::Settings::ShowSettings(Class): it opens the page with the block expanded. In the console: Pulse.Settings <Plugin|Settings class>. The old layout (one page per class) comes back with [PulseSettingsPages] bEnabled=False in EditorPerProjectUserSettings.ini after a restart.
Settings can hold extension points as instanced structs (FInstancedStruct with BaseStruct, also in arrays). The details panel of UE 5.8 compares the fields inside such a struct with the class default object of the parent class, which does not have them, and crashes when a struct is picked or an element is added. The editor module of PulseFoundation prevents this for every class derived from UPulseDeveloperSettings: the details panel compares these settings with themselves. Fields inside instanced structs of settings therefore show no reset arrow; they never had a default to reset to. Settings derived directly from UDeveloperSettings don’t get this fix.
Test helpers
Testing/PulseTestWorld.h (only with WITH_DEV_AUTOMATION_TESTS):
Pulse::Testing::FTestWorld: a game world that has begun play without game mode, so spawned actors getBeginPlay.Create,Destroy,Spawn<T>,SpawnWithComponent<T>.Pulse::Testing::SpawnTestPlayer(World): player controller with player state and possessed pawn.SpawnTestPlayer(UWorld*, Name)does the same in any world (alsoFTestWorldWrapperworlds) and names the player state, so its player id is that name.Pulse::Testing::SpawnTestGameState(UWorld*): the game state of the world; a world without one (anFTestWorld) gets a spawnedAGameStateBase, which lists every player state, soPulse::Player::FindPlayerStateByIdfinds them.Pulse::Testing::FTestObjects: keeps transient objects alive untilReset.
Testing/PulseTestMessages.h (only with WITH_DEV_AUTOMATION_TESTS): Pulse::Testing::FMessageRelayAccess tests the message relay in a world without a network. Receive(Relay, Rpc, bAsClient, Channel, Payload, Context) delivers a message as a machine gets it (with bAsClient the owner has no authority during the call); SetSendHook gets every context the relays send, also ForwardToClients in standalone, where no RPC is called (reset it with nullptr in AfterEach).
BEGIN_DEFINE_SPEC(FMySpec, "MyPlugin.Feature", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter)
Pulse::Testing::FTestWorld World;
END_DEFINE_SPEC(FMySpec)
void FMySpec::Define()
{
BeforeEach([this]() { World.Create(TEXT("MyTest")); });
AfterEach([this]() { World.Destroy(); });
It("works", [this]() { const Pulse::Testing::FTestPlayer player = Pulse::Testing::SpawnTestPlayer(World); ... });
}
Native tags
PulseFoundationTags.h: Pulse.Message, Pulse.Message.Settings.Changed, Pulse.Setting, Pulse.SettingCategory and its tabs Gameplay, Video, Audio, Controls, Interface, Accessibility.