Conditions, costs, rewards and the context
The Pulse plugins describe “what is this about” in one way: the context. Conditions, costs, rewards and messages all get it, so the same condition can guard an interaction, unlock a skill and filter a message.
Context
FPulseContext (Context/PulseContext.h, module PulseFoundation):
| Field | What it is |
|---|---|
Player | Any actor of the player: controller, player state or pawn. Conditions find the player’s components on all three. |
Instigator | Who caused it, often the player’s pawn (the attacker, the crafter). |
Target | What it is aimed at (the damaged actor, the interactable, the speaker of a dialog). |
Source | Where it comes from: a recipe, an item definition, an ability, a quest. |
Tags | Tags of what happened (damage type, item tags, the kind of an event). |
Amount | An amount: items added, damage dealt, experience gained. Messages without an amount of their own carry 1, so adding up their values counts them (message channels); other contexts have 0 without. |
Location | Where it happens when no actor tells it: items that have no player to go to drop here (the refund of a destroyed building piece). Zero when not set. |
Extra | Extra data of the plugin as an instanced struct (station and count when crafting, the rank of a skill, the payload of a message). |
CatchUp | Whether the event only catches up with an earlier state instead of happening now (see below). |
Ids | The ids of the player and the source, for machines and times where the pointers are empty (see Ids). Hidden on Break Pulse Context by default; read them with Get Context Ids. |
The object fields are weak pointers: a context never keeps an actor alive; its ids name the player and the source when the objects are gone.
Ids
The pointers are fast on the machine that has the objects, but they go away: a pawn dies, a player leaves, a far pawn is not relevant to a client, an entity has no actor at all. Ids (FPulseContextIds, Context/PulseContextIds.h) keeps three values next to them:
| Id | What it is |
|---|---|
PlayerId | The stable id of the player (player resolution: unique net id, else the player name). |
SourceAsset | The asset the source is (a recipe, an item definition, a quest) as a soft object path. Only the source’s own path: a source that is no asset (a component, an ability instance) has none. |
SourceTag | A tag that names the source where no object or asset does (a place, a quest tag). Only the maker of the context sets it. |
Read them with GetIds(), GetPlayerId() and GetSourceAsset() (Blueprint Get Context Ids, Get Context Player Id). They prefer the live pointers, one rule per field:
- Player id: the id of a live
Playerwins when it gives one, else the stored id. A live pawn without player state (unpossessed while dying) keeps naming its player through the stored id. - Source asset: while
Sourceis set, its own path (empty for a non-asset); the stored path only whenSourceis empty. - Source tag: always the stored one.
Who stores them:
- The message bus, on messages with at least two listeners: every listener gets the player id, also after an earlier listener destroyed the pawn (Message bus). A single listener gets the context as it was sent.
- The area messages
Pulse.Message.Area.Entered/Left(Areas). - Make Pulse Context and Make Pulse Cost Context store the player id, so a context kept in a Blueprint variable keeps naming its player when the pawn dies.
- Makers without objects (an entity that knows the path, a place) set
Idsthemselves (Blueprint: Make PulseContextIds and Set Members in PulseContext, whereIdsis shown through the pin options). - Code that keeps a context beyond the call that made it (a member, a timer, a lambda) calls
Pulse::Context::FillPlayerId(Context)(the player id) orPulse::Context::FillIds(Context)(all ids it names).
Conditions, costs and rewards evaluated in the maker’s own call need nothing: the getters derive the id from the live pointer.
The way back: Pulse::Context::ResolveFromIds(World, Context) sets empty pointers from the stored ids. The player becomes its player state (Pulse::Player::FindPlayerStateById; every machine has the player states of all players), the source an asset that is loaded already (nothing is loaded). Pointers that are set stay. Makers without objects call it before Evaluate, CanPayAll, PayAll or GrantAll, and before they broadcast when server listeners need pointers: a context that names its player only by an id has no authority (see Authority below).
Copies keep the stored ids. To change the player or the source of a copy, use SetPlayer / SetSource (Blueprint Set Pulse Context Player / Set Pulse Context Source), which drop the ids of the old ones. Setting Player by hand to an actor of another player is safe (its id wins), but clearing it keeps the old player’s id; Set Members keeps it too.
Catching up
A client that joins gets the quests, unlocks and achievements of the server by replication; a save brings them back; a player who joins later gets the rewards the others got. Plugins send these as messages too, but with CatchUp set:
| Value | When |
|---|---|
None | A new event: it happens now |
Replication | The state arrived by replication: a client joined, an actor or a region became relevant |
Load | The state came from a save |
Join | The server repeats an earlier event for a player who joined later |
Listeners that react to new events (toasts, sounds, counters) leave catch-up events out: IsCatchUp(), Blueprint Is Catch Up, the condition Catch Up, or message triggers, which skip them unless bIncludeCatchUp is on. No time window is needed, so a slow join does not bring old toasts.
The helpers find the actors and components of the player with player resolution, whichever of the three actors Player is:
| Function | Returns |
|---|---|
GetPlayerController(), GetPlayerState(), GetPawn() | The actors of the player, or null (another player’s controller on a client). |
FindPlayerComponent<T>() | The first component of the class on the player’s actors (the actor itself, controller, player state, pawn). |
GetExtra<T>() | The extra data as T, or null when it holds another type. |
// A context for a crafted item
FPulseContext context;
context.Player = PlayerController;
context.Instigator = PlayerController->GetPawn();
context.Source = Recipe;
context.Amount = 1.0;
if (UMyQuestComponent* quests = context.FindPlayerComponent<UMyQuestComponent>())
{
// ...
}
In Blueprint, Make Pulse Context builds one (player first, the other inputs under the arrow), Break Pulse Context reads it, and Get Context Player Controller, Get Context Player State, Get Context Pawn and Find Context Player Component find the player’s actors and components (category Pulse | Foundation | Context). Get Context Ids, Get Context Player Id, Set Pulse Context Player and Set Pulse Context Source read and change the ids.
Results
Conditions answer with EPulseConditionResult:
| Value | Meaning |
|---|---|
Passed | The condition holds. |
Failed | It does not hold. |
Unknown | The data it needs is not on this machine, usually on a client (the inventory of another player, a server-only flag). The UI shows the line; the server decides. |
Conditions
A condition is an instanced struct derived from FPulseCondition (Conditions/PulseCondition.h). Plugins put condition lists on their assets and components as FPulseConditionList:
| Field | What it does |
|---|---|
Mode | All: every condition must pass. Any: one passing condition is enough. |
Conditions | The entries. An empty list passes; an entry without a type does nothing. |
Evaluate(Context) returns the combined result. Unknown counts like this: with All the list is unknown when nothing fails but something is unknown; with Any when nothing passes but something is unknown. GetDisplayLines(Context, bOnlyNotPassed) gives one line per condition for the UI, optionally only the ones that do not pass (failed and unknown). An entry without its own line shows the name of its type.
Conditions also run on clients, for the UI. When the data they need is not there (another player’s inventory, an actor that is not replicated), they answer Unknown; the server always decides.
Conditions of Pulse Foundation
| Condition | Category | Passes when |
|---|---|---|
| All Of, Any Of | Logic | all, or one, of its nested conditions pass |
| Not | Logic | its condition fails (Unknown stays Unknown) |
| Context Tags | Context | the tags of the context match a query |
| Actor Has Tags | Context | the target, instigator or player has the tags (all or one of them), from IGameplayTagAssetInterface on the actor or its components (an ability system component) |
| Actor Tag Query | Context | the tags of the target, instigator or player match a query (has a key tag and is not stunned); Text is the line for the UI (“Locked”) |
| Actor Is Class | Context | the target, instigator or player is of a class |
| Compare Amount | Context | the amount of the context compares to a value (==, !=, <, <=, >, >=) |
| In Area | Area | the pawn of the player (else instigator, else target) is in a Pulse area with a tag and passes its conditions |
| Catch Up | Context | the event is new (default), or with bCatchUp only catches up with an earlier state (catching up) |
| Blueprint Condition | Blueprint | the Blueprint condition object of the entry passes (see below) |
For the player, both actor conditions read the tags of its controller, player state and pawn together, so a tag on an ability system of the player state counts as well (Pulse::Conditions::GetContextActorTags).
The other plugins bring their own conditions (Has Item, Quest Finished, Skill Rank, …). The editor shows condition lists as cards with the category of each type.
A condition in C++
// Passes when the player has at least MinLevel levels (a game's own component)
USTRUCT(meta = (DisplayName = "Player Level", Category = "My Game"))
struct FMyCondition_PlayerLevel : public FPulseCondition
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = "Condition")
int32 MinLevel = 1;
virtual EPulseConditionResult Evaluate(const FPulseContext& InContext) const override
{
const UMyLevelComponent* levels = InContext.FindPlayerComponent<UMyLevelComponent>();
if (!levels)
return Pulse::Conditions::GetMissingDataResult(InContext); // Unknown on a client, Failed elsewhere
return levels->GetLevel() >= MinLevel ? EPulseConditionResult::Passed : EPulseConditionResult::Failed;
}
virtual FText GetDisplayText(const FPulseContext& InContext) const override
{
return FText::Format(NSLOCTEXT("MyGame", "PlayerLevel", "Level {0} or higher"), MinLevel);
}
};
Every condition that shows up in a UI should give a line with GetDisplayText. Pulse::Conditions::Compare and GetCompareSymbol help with comparisons; GetActorTags collects the gameplay tags of an actor and its components, GetContextActorTags those of an actor of the context.
A condition in Blueprint
- Create a Blueprint class with the parent Pulse Condition Object.
- Override Evaluate (return Passed, Failed or Unknown) and Get Display Text. Variables of the Blueprint are the settings of the condition.
- In a condition list, add the entry Blueprint Condition and pick the Blueprint as its condition. Every entry holds its own instance, so its variables are set per entry.
While the functions run, Get World returns the world of the context, so nodes like Get Game State work.
Costs
A cost is an instanced struct derived from FPulseCost (Conditions/PulseCost.h). Plugins put cost lists on their assets as FPulseCostList (field Costs) and pay them with a payment context, FPulseCostContext: the context plus
| Field | What it is |
|---|---|
Fraction | Multiplier of the costs: number of pieces, a length, a payment stage, a repair or refund part. Default 1. |
Offset | Part of the same costs that was paid before, in the units of Fraction (the payment stages already paid). |
bReverse | Undo of an earlier action or rollback of a failed payment: payments round down, refunds round up. |
| Function | What it does |
|---|---|
CanPayAll(Context) | Passed, Failed, or Unknown when data is missing here. Each cost is checked alone. |
PayAll(Context) | Pays all costs or none (authority): checks them all, pays them in order and gives back what was paid when one fails. |
RefundAll(Context) | Gives the costs back (authority); Fraction is the part. |
GetDisplayLines(Context) | One line per cost (“3 Wood”); an entry without its own line shows the name of its type. |
GetLines(Context) | Lines with label, icon, what the player has and what it costs (FPulseCostLine, IsMet). |
An empty list costs nothing; an entry without a type does nothing. The editor shows cost lists as cards.
Rounding. Amounts are whole numbers. Pulse::Costs::GetAmount(Amount, Context, bPaying) rounds payments up and refunds down (reversed contexts the other way), over the whole cost: the part paid before (Offset) and this part together, minus the part before. So payment stages add up to exactly the full cost:
| 3 Wood | Context | Amount |
|---|---|---|
| First half paid | Fraction 0.5 | 2 |
| Second half paid | Fraction 0.5, Offset 0.5 | 1 |
| Half refunded | Fraction 0.5, refund | 1 |
| First half undone | Fraction 0.5, bReverse, refund | 2 (what was paid) |
Costs that share a resource (“2 Wood” and “4 Wood” with 5 wood) each pass CanPayAll alone. PayAll finds it: Pay checks again and returns false, and the list gives back what was paid. Put one entry per resource where you can.
Authority. PayAll, RefundAll and the reward functions below only act on the authority of the context’s actors (the first of player, instigator and target that is set). On a client they log a warning and do nothing. A context without actors counts as authority, unless it names its player by an id: then the world of its source decides, and without one nothing is paid or granted and conditions that miss data answer Unknown (Not keeps Unknown). Code that only blocks on Failed lets such contexts through, so resolve id-only contexts first (Pulse::Context::ResolveFromIds). Id-only contexts of players who left are not supported yet: their player state is not found, so nothing is granted.
A cost in C++
// Gold from a game's own wallet component
USTRUCT(meta = (DisplayName = "Gold", Category = "My Game"))
struct FMyCost_Gold : public FPulseCost
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = "Cost")
int32 Amount = 10;
virtual EPulseConditionResult CanPay(const FPulseCostContext& InContext) const override
{
const UMyWalletComponent* wallet = InContext.FindPlayerComponent<UMyWalletComponent>();
if (!wallet)
return Pulse::Conditions::GetMissingDataResult(InContext);
return wallet->Gold >= Pulse::Costs::GetAmount(Amount, InContext, true) ? EPulseConditionResult::Passed : EPulseConditionResult::Failed;
}
virtual bool Pay(const FPulseCostContext& InContext) const override
{
UMyWalletComponent* wallet = InContext.FindPlayerComponent<UMyWalletComponent>();
const int32 amount = Pulse::Costs::GetAmount(Amount, InContext, true);
if (!wallet || wallet->Gold < amount)
return false; // check again: another cost of the list may have used the same gold
wallet->Gold -= amount;
return true;
}
virtual void Refund(const FPulseCostContext& InContext) const override
{
if (UMyWalletComponent* wallet = InContext.FindPlayerComponent<UMyWalletComponent>())
wallet->Gold += Pulse::Costs::GetAmount(Amount, InContext, false);
}
virtual FText GetDisplayText(const FPulseCostContext& InContext) const override
{
return FText::Format(NSLOCTEXT("MyGame", "Gold", "{0} Gold"), Pulse::Costs::GetAmount(Amount, InContext, true));
}
};
Override GetLines to show what the player has next to what it costs.
A cost in Blueprint
- Create a Blueprint class with the parent Pulse Cost Object.
- Override Can Pay (Passed, Failed or Unknown), Pay (return false when it cannot be paid after all) and Refund, and Get Display Text for the UI. Round amounts with Get Cost Amount (it applies
Fraction,OffsetandbReverseof the payment like every Pulse cost). Variables of the Blueprint are the settings of the cost (instance editable). - In a cost list, add the entry Blueprint Cost and pick the Blueprint. Every entry holds its own instance.
Pay and Refund only run on the authority; while the functions run, Get World returns the world of the context.
Rewards
A reward is an instanced struct derived from FPulseReward (Conditions/PulseReward.h), in an FPulseRewardList (field Rewards). The context names who gets it (Player) and why (Source).
| Function | What it does |
|---|---|
GrantAll(Context) | Grants every reward (authority); returns false when it granted nothing on a client. |
ReapplyAll(Context) | Applies again what the rewards gave when that result is not saved itself, for example after loading (a skill tree a level gave). Rewards whose result is saved (items, experience) do nothing here. |
GetDisplayLines(Context) | One line per reward (“+50 XP”); an entry without its own line shows the name of its type. |
CanGrantAll(Context) | Whether every reward can be granted now (an inventory with room); exchanges check it before they take anything. |
RevokeAll(Context) | Takes the rewards back in reverse order (authority), when an exchange fails after granting them. |
The editor shows reward lists as cards.
A reward in C++
// Gold into a game's own wallet component
USTRUCT(meta = (DisplayName = "Gold", Category = "My Game"))
struct FMyReward_Gold : public FPulseReward
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = "Reward")
int32 Amount = 10;
virtual void Grant(const FPulseContext& InContext) const override
{
if (UMyWalletComponent* wallet = InContext.FindPlayerComponent<UMyWalletComponent>())
wallet->Gold += Amount;
}
virtual FText GetDisplayText(const FPulseContext& InContext) const override
{
return FText::Format(NSLOCTEXT("MyGame", "GoldReward", "+{0} Gold"), Amount);
}
};
Override Reapply only when the reward changes something that is not saved by itself. Override CanGrant when a reward can fail to fit (default: it always fits) and Revoke when it can be taken back (default: nothing).
A reward in Blueprint
- Create a Blueprint class with the parent Pulse Reward Object.
- Override Grant (and Reapply, Get Display Text when needed). Variables of the Blueprint are the settings of the reward.
- In a reward list, add the entry Blueprint Reward and pick the Blueprint. Every entry holds its own instance.
While the functions run, Get World returns the world of the context. Grant is only called on the authority.
Lists in your own Blueprints
Condition, cost and reward lists can be variables of any Blueprint (a door with conditions, a shop with costs and rewards). The nodes are in the categories Pulse | Foundation | Conditions, Costs and Rewards:
| Node | What it does |
|---|---|
| Evaluate Conditions | The result of a condition list (switch on it: Passed, Failed, Unknown). |
| Get Condition Lines | The lines for the UI, optionally only the conditions that do not pass. |
| Make Pulse Cost Context | A payment context from a context, with Fraction (and Offset, bReverse under the arrow). |
| Can Pay Costs, Pay Costs, Refund Costs | Check, pay all or nothing, give back (paying and refunding on the authority). |
| Get Cost Lines | Lines with what the player has and what it costs. |
| Get Cost Amount | A whole amount for a payment, rounded like every Pulse cost (for Blueprint costs). |
| Grant Rewards, Get Reward Lines | Grant a reward list (authority), its lines for the UI. |