Skip to content
Tuxedo CatSTUDIO
Menu

Quickstart — C++ Usage

Resolve the current GameInstance’s UProbabilityDirectorSubsystem, register your named Streams once during stable initialization, reuse those Streams through the public Sequential or Keyed APIs, check every returned status, and centralize game-specific Stream IDs and Decision Key conventions in a small project helper or Blueprint Function Library.

This guide shows the simplest C++ pattern for using Probability Director in gameplay code and organizing it cleanly as your project grows.

The main workflow is:

Get the current GameInstance subsystem
|
v
Create/register your Streams during stable initialization
|
v
Keep using those same Stream IDs
|
v
Call Sequential or Keyed operations
|
v
Check returned Status/Result
|
v
Remove Streams only when their real lifecycle ends

There is no process-global Probability Director singleton and no hidden default Stream.


For stateful Runtime operations:

#include "ProbabilityDirectorSubsystem.h"

Your C++ gameplay code normally works through:

UProbabilityDirectorSubsystem

which is owned by the current UGameInstance.


A simple helper:

#include "ProbabilityDirectorSubsystem.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
UProbabilityDirectorSubsystem* GetProbabilityDirector(UObject* WorldContextObject)
{
if (!WorldContextObject)
{
return nullptr;
}
UWorld* World = WorldContextObject->GetWorld();
if (!World)
{
return nullptr;
}
UGameInstance* GameInstance = World->GetGameInstance();
if (!GameInstance)
{
return nullptr;
}
return GameInstance->GetSubsystem<UProbabilityDirectorSubsystem>();
}

At a call site:

UProbabilityDirectorSubsystem* Probability =
GetProbabilityDirector(this);
if (!Probability)
{
return;
}

3. Do Not Make a Process-Global Subsystem Pointer

Section titled “3. Do Not Make a Process-Global Subsystem Pointer”

Avoid:

static UProbabilityDirectorSubsystem* GProbabilityDirector;

Probability Director is GameInstance-scoped.

In PIE you can have:

GameInstance A
-> Probability Director A
GameInstance B
-> Probability Director B

A static/global pointer can easily refer to the wrong context or outlive its owner.


Create long-lived gameplay Streams during stable initialization.

Good owners include:

  • Your UGameInstance-owned manager.
  • A GameInstance subsystem.
  • A persistent gameplay service.
  • Another clearly authoritative long-lived owner.

Avoid creating/removing a Stream around every random call.


Probability Director uses:

FProbabilityStreamConfig

to describe a Stream.

Conceptually:

FProbabilityStreamConfig Config;
// Configure:
// - Stream ID
// - Seed Source
// - Decision Mode
// - Persistence Policy
// - Fixed Seed when Fixed is selected
Probability->CreateStream(Config);

Use the actual returned status/result declared by your installed ProbabilityDirectorSubsystem.h.

Do not assume registration succeeded merely because the Stream ID is non-empty.


A Stream is identified by:

FProbabilityStreamId

Example:

FProbabilityStreamId StreamId;
StreamId.Name = TEXT("Combat.Damage");

Use semantic IDs.

Good:

Combat.Damage
Combat.Critical
Loot.DropTable
World.Encounters
NPC.Names

Avoid temporary object names and pointer-based identity.


Do not duplicate string literals throughout the project.

Instead, create one project namespace/helper.

Example:

namespace MyGameProbabilityStreams
{
inline FProbabilityStreamId CombatDamage()
{
FProbabilityStreamId Id;
Id.Name = TEXT("Combat.Damage");
return Id;
}
inline FProbabilityStreamId LootDropTable()
{
FProbabilityStreamId Id;
Id.Name = TEXT("Loot.DropTable");
return Id;
}
}

Then gameplay uses:

const FProbabilityStreamId StreamId =
MyGameProbabilityStreams::CombatDamage();

This avoids spelling drift.


Setting Meaning
Stream ID Which gameplay system owns the random source
Seed Source Secure, Fixed, or Imported initialization
Decision Mode Sequential or Keyed
Persistence Policy Ephemeral, PersistState, or RotateOnSave

Each setting solves a different problem.


Need Use
Normal fresh gameplay randomness Secure
Reproducible automated test Fixed
Reproduce a deterministic bug Fixed
Approved imported state Imported

A Fixed Seed is deterministic and nonsecret.

Secure internal material remains private.


Use this rule:

Sequential
= "Give me the next result."
Keyed
= "Give me the result for this exact semantic decision."

A concrete Runtime sampling example:

#include "ProbabilityDirectorSubsystem.h"
void UMyGameplayComponent::SampleGameplayValue()
{
UProbabilityDirectorSubsystem* Probability =
GetProbabilityDirector(this);
if (!Probability)
{
return;
}
const FProbabilityStreamId StreamId =
MyGameProbabilityStreams::CombatDamage();
FProbabilityNormalDistribution Distribution;
// Configure Distribution through the public fields/constructor
// provided by the installed Runtime API.
double Sample = 0.0;
const FProbabilityDistributionSamplingStatus Status =
Probability->TrySampleSequentialNormal(
StreamId,
Distribution,
Sample);
// Check Status according to the public contract
// before using Sample.
}

The important pattern is:

Get subsystem
-> use already-created Stream ID
-> call operation
-> check Status
-> then use output

For a numeric Dice total, the public Runtime shape is:

Probability->ResolveSequentialNumericDicePipeline(
StreamId,
Pipeline,
FlatModifier,
DiceBias,
OutResult);

Use the public result/status contract declared by the installed Runtime header.

Do not treat OutResult as a successful roll unless the returned operation status says it succeeded.


A Keyed operation uses:

FProbabilityDecisionKey

and a Stream configured for Keyed Decision Mode.

Example shape:

FProbabilityDecisionKey DecisionKey;
// Fill through the approved public construction/validation path:
// Namespace
// Stable Subject ID
// Decision
// Occurrence
double Sample = 0.0;
const FProbabilityDistributionSamplingStatus Status =
Probability->TrySampleKeyedNormal(
StreamId,
DecisionKey,
Distribution,
Sample);

The caller owns Occurrence.

There is no hidden Keyed counter.


Field Purpose
Namespace Broad gameplay system
Stable Subject ID Stable subject/entity identity
Decision What is being decided
Occurrence Which repeated deterministic occurrence

Example:

Namespace:
Loot
Stable Subject ID:
Chest.Forest.0042
Decision:
Rarity
Occurrence:
0

Avoid:

FString::Printf(TEXT("%p"), ChestActor);

for a Stable Subject ID.

Pointer addresses:

  • Change between runs.
  • Are process-specific.
  • Do not represent gameplay identity.

Use stable semantic IDs instead.

Example:

Chest.World01.Room12.Chest04

If one semantic subject needs several deterministic decisions:

Decision Occurrence
First loot item 0
Second loot item 1
Third loot item 2

Your application owns that mapping.

Probability Director does not silently increment it.


Sequential mode is deliberately call-order sensitive.

Example:

Old:
A -> B -> C

After code change:

A -> X -> B -> C

If X consumes Sequential random state, B and C can change.

If B/C need stable semantic identity independent of X, consider Keyed mode or a separate Stream.


Avoid putting unrelated systems on one Sequential Stream.

Instead of:

Game.Random

prefer:

Combat.Damage
Loot.DropTable
World.Encounters

This reduces accidental call-order coupling.


Use:

Probability->HasStream(StreamId)

when you need to verify lifecycle state.

Example:

if (!Probability->HasStream(StreamId))
{
// Expected Stream was not initialized for this GameInstance.
return;
}

Use this for validation/debugging.

Do not turn every gameplay function into “create if missing.”

One owner should initialize the Stream.


Use:

GetStreamConfiguration

when you need to verify the actual registered contract.

Useful when:

  • A Keyed call is rejected.
  • You expected Sequential but registered Keyed.
  • You expected Fixed but registered Secure.
  • You need to inspect Persistence Policy.

Treat registered configuration as authoritative.


Use:

GetStreamIds

primarily for:

  • Developer tools.
  • Debug menus.
  • Validation.
  • Tests.

Gameplay systems should normally know their owned IDs directly.


Use:

RemoveStream

when the Stream’s real lifecycle ends.

Do not remove/recreate a long-lived Stream:

  • After every operation.
  • On every level transition.
  • Because one Actor was destroyed.

Remember: the Stream belongs to the current GameInstance.


23. Build a Small Project Probability Layer

Section titled “23. Build a Small Project Probability Layer”

For a larger C++ game, create a thin project layer around Probability Director.

Example:

MyGameProbability/
MyGameProbabilityStreams.h
MyGameProbabilityKeys.h
MyGameProbabilityLibrary.h
MyGameProbabilityLibrary.cpp

Use it to centralize:

  • Stream IDs.
  • Decision namespaces.
  • Stable Subject ID conventions.
  • Stream initialization.
  • Common operations.
  • Project-specific error handling.

Probability Director exposes generic APIs such as:

Resolve Sequential Numeric Dice Total
Try Sample Keyed Normal

Your game thinks in terms such as:

Roll Weapon Damage
Choose Chest Rarity
Choose Encounter
Generate NPC Name

A thin wrapper makes gameplay code easier to read.


If you want game-specific C++ helpers that are also available in Blueprint, derive from:

UBlueprintFunctionLibrary

Example:

#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyGameProbabilityLibrary.generated.h"
UCLASS()
class MYGAME_API UMyGameProbabilityLibrary
: public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(
BlueprintCallable,
Category="My Game|Probability",
meta=(WorldContext="WorldContextObject"))
static bool InitializeGameProbability(
UObject* WorldContextObject);
};

The implementation can resolve the current UGameInstance Probability Director subsystem and create the Streams owned by your game.


Use Unreal’s World Context pattern so your wrapper resolves the correct GameInstance.

Example helper:

UProbabilityDirectorSubsystem*
UMyGameProbabilityLibrary::ResolveProbabilityDirector(
UObject* WorldContextObject)
{
if (!WorldContextObject)
{
return nullptr;
}
UWorld* World = WorldContextObject->GetWorld();
if (!World)
{
return nullptr;
}
UGameInstance* GameInstance = World->GetGameInstance();
if (!GameInstance)
{
return nullptr;
}
return GameInstance->GetSubsystem<UProbabilityDirectorSubsystem>();
}

Do not store one global subsystem pointer inside the BFL.


Your project function can conceptually do:

InitializeGameProbability(WorldContext)
|
+--> Resolve subsystem
|
+--> Has Combat.Damage?
| |
| +--> No -> Create configured Combat.Damage
|
+--> Has Loot.DropTable?
|
+--> No -> Create configured Loot.DropTable

This gives your project one controlled initialization entry point.


28. Do Not Silently Auto-Create from Every Wrapper

Section titled “28. Do Not Silently Auto-Create from Every Wrapper”

Avoid a wrapper like:

RollWeaponDamage()
{
if (!HasStream(CombatDamage))
{
CreateStream(CombatDamage);
}
// roll...
}

That hides lifecycle problems.

Better:

InitializeGameProbability
-> owns Stream creation
RollWeaponDamage
-> expects Combat.Damage to exist
-> returns/records clear failure if it does not

You can also expose project IDs to Blueprint:

UFUNCTION(
BlueprintPure,
Category="My Game|Probability")
static FProbabilityStreamId GetCombatDamageStreamId();

Implementation:

FProbabilityStreamId
UMyGameProbabilityLibrary::GetCombatDamageStreamId()
{
FProbabilityStreamId Id;
Id.Name = TEXT("Combat.Damage");
return Id;
}

Now both C++ and Blueprint can use the same canonical ID.


A useful wrapper might be:

Roll Weapon Damage

instead of exposing every caller to all low-level Dice configuration.

Conceptually:

bool UMyGameProbabilityLibrary::RollWeaponDamage(
UObject* WorldContextObject,
int32 NumDice,
int32 FlatModifier,
int64& OutDamage)
{
UProbabilityDirectorSubsystem* Probability =
ResolveProbabilityDirector(WorldContextObject);
if (!Probability)
{
return false;
}
const FProbabilityStreamId StreamId =
GetCombatDamageStreamId();
// Build your approved Dice pipeline.
// Call ResolveSequentialNumericDicePipeline.
// Check the returned public status.
// Copy the successful final outcome to OutDamage.
return true;
}

Keep the wrapper thin.

Let Probability Director continue to own the actual probability/Dice logic.


A project can expose:

Make Chest Loot Key

to Blueprint/C++.

Conceptually:

FProbabilityDecisionKey MakeChestLootKey(
const FString& StableChestId,
FName Decision,
int32 Occurrence);

The helper should enforce your game convention:

Namespace = Loot
Stable Subject ID = supplied stable chest ID
Decision = supplied semantic decision
Occurrence = supplied explicit occurrence

This avoids every caller inventing a slightly different Key format.


Centralize Keyed namespaces too.

Example:

namespace MyGameProbabilityNamespaces
{
static const FName Loot(TEXT("Loot"));
static const FName Combat(TEXT("Combat"));
static const FName World(TEXT("World"));
}

Use whatever type the current public FProbabilityDecisionKey fields require.

The important design point is one canonical project convention.


Do not write:

double Value = 0.0;
Probability->TrySampleSequentialNormal(..., Value);
UseValue(Value);

without checking the returned status.

Preferred:

double Value = 0.0;
const auto Status =
Probability->TrySampleSequentialNormal(
StreamId,
Distribution,
Value);
if (!/* Status success according to public contract */)
{
return;
}
UseValue(Value);

A default-looking output can still be a valid result.

Status is authoritative.


34. Do Not Infer Stream Mode from the Function You Wanted to Call

Section titled “34. Do Not Infer Stream Mode from the Function You Wanted to Call”

Suppose you intended:

Loot.DropTable = Keyed

but it was actually registered Sequential.

Do not assume the Stream is Keyed because your caller chose a Keyed API.

Query the registered configuration when debugging.

The registered Stream contract wins.


Choose one of:

Policy Meaning
Ephemeral Runtime only
PersistState Restore approved deterministic state
RotateOnSave Commit prepared rotation after host save success

Persistence Policy is separate from:

  • Seed Source.
  • Decision Mode.

Read Persistence and Save Game Integration before choosing production save behavior.


A useful automated/manual test:

1. Create Fixed Sequential Stream with known Seed.
2. Perform N operations.
3. Record results.
4. Destroy/recreate equivalent clean runtime.
5. Create same Stream/configuration.
6. Repeat exact operation order.
7. Compare results.

This verifies your integration is reproducing the intended deterministic path.


For Keyed behavior:

1. Create Fixed/compatible Keyed Stream.
2. Build one semantic Decision Key.
3. Call operation.
4. Call same operation again with same Key.
5. Confirm deterministic repetition.
6. Change Occurrence.
7. Confirm it addresses a different deterministic coordinate.

Do not evaluate “random quality” from one individual Key.


38. Use the Runtime Inspector During Development

Section titled “38. Use the Runtime Inspector During Development”

During PIE:

Tools → Director Suite → Runtime Inspectors → Probability Director Runtime Inspector

Use it to verify:

  • Stream registration.
  • Seed Source.
  • Decision Mode.
  • Persistence Policy.
  • Recent retained decisions.
  • Sequential Effect.
  • Sanitized Keyed identity.

It is read-only and does not expose raw generator state.


Possible causes:

Cause Fix
Object has no valid World Call from valid Runtime context
World has no GameInstance Wait for appropriate lifecycle
Object belongs to teardown/stale context Re-resolve from current owner
Cached pointer outlived GameInstance Stop using global/static cache

Always null-check the resolved subsystem.


Check:

Did initialization run?
Is Stream ID exact?
Correct GameInstance?
Was Stream removed?
Did gameplay call before initialization?

Use:

HasStream(StreamId)

and the Runtime Inspector.


41. Common Problem — Duplicate Registration

Section titled “41. Common Problem — Duplicate Registration”

If initialization can run more than once:

if (!Probability->HasStream(StreamId))
{
// Create through your centralized initialization path.
}

Do this in the owner/initializer.

Do not spread duplicate-protection throughout unrelated gameplay functions.


42. Common Problem — Sequential Results Shifted

Section titled “42. Common Problem — Sequential Results Shifted”

Check whether a new operation was inserted earlier on the same Stream.

If unrelated systems are coupled:

  • Split them into separate Streams.
  • Or move stable semantic decisions to Keyed mode.

43. Common Problem — Keyed Result Repeats

Section titled “43. Common Problem — Keyed Result Repeats”

If the same:

Namespace
Stable Subject ID
Decision
Occurrence

was used, repetition is expected.

Change Occurrence only when the gameplay semantics say this is another occurrence.


44. Common Problem — Keyed Result Changes

Section titled “44. Common Problem — Keyed Result Changes”

Compare the actual Key fields.

A changed:

  • Namespace.
  • Stable Subject ID.
  • Decision.
  • Occurrence.

means a different deterministic coordinate.

Also confirm the same Stream/configuration is being used.


45. Common Problem — Pointer-Based Stable Subject ID

Section titled “45. Common Problem — Pointer-Based Stable Subject ID”

Do not use:

reinterpret_cast<uintptr_t>(Actor)

or an address-derived string.

Instead give the subject a stable gameplay identity.

Examples:

Chest.Forest.0042
NPC.Village.Blacksmith
Quest.Main.FindArtifact

46. Common Problem — Recreating Streams on Map Load

Section titled “46. Common Problem — Recreating Streams on Map Load”

Because the subsystem is GameInstance-scoped, map travel does not necessarily mean the Stream lifecycle ended.

Keep long-lived Stream ownership outside transient Level/Actor setup when appropriate.


47. Common Problem — Wrapper Becomes a Second Probability System

Section titled “47. Common Problem — Wrapper Becomes a Second Probability System”

Your helper layer should not:

  • Implement its own random generator.
  • Duplicate Probability Director Dice math.
  • Duplicate bias math.
  • Maintain a secret second Stream counter.
  • Reimplement persistence.

Your wrapper should express your game semantics and call Probability Director.


Source/MyGame/
Probability/
MyGameProbabilityStreams.h
MyGameProbabilityKeys.h
MyGameProbabilityLibrary.h
MyGameProbabilityLibrary.cpp
MyGameProbabilityManager.h
MyGameProbabilityManager.cpp

Possible responsibilities:

File Responsibility
Streams Canonical Stream IDs
Keys Key namespace/identity builders
Library Blueprint-friendly wrappers
Manager Startup/lifecycle ownership

Keep it small until the project actually needs more.


GameInstance created
|
v
Your probability owner initializes
|
+--> Create Combat.Damage
+--> Create Loot.DropTable
+--> Create World.Encounters
|
v
Gameplay begins

Then gameplay code only consumes the already-registered Streams.


Gameplay event
|
v
Resolve current Probability Director subsystem
|
v
Use canonical Stream ID
|
v
Build Decision Key if Keyed
|
v
Call public operation
|
v
Check Status
|
+--> Success -> use result
|
+--> Failure -> handle/log according to game policy

Good project logging:

Probability operation failed
Stream: Combat.Damage
Operation: Resolve Damage
Status: <public readable status>

Avoid logging private internals such as:

  • Secure root.
  • Raw RNG state.
  • Generator keys/nonces.
  • Raw persistence payloads.

Use the Runtime Inspector for approved diagnostic details.


52. Blueprint Exposure from Your C++ Layer

Section titled “52. Blueprint Exposure from Your C++ Layer”

Use your UBlueprintFunctionLibrary when designers should call game-semantic operations.

Examples:

My Game | Probability | Roll Weapon Damage
My Game | Probability | Choose Chest Rarity
My Game | Probability | Get Combat Damage Stream ID
My Game | Probability | Make Chest Loot Key

This can make Blueprint graphs much cleaner than exposing project-specific conventions everywhere.


Do not necessarily hide all Probability Director nodes.

Your BFL should provide convenience for common game semantics while advanced systems can still use the official public API directly where appropriate.

Example:

Common weapon
-> Roll Weapon Damage wrapper
Special procedural tool
-> direct Probability Director distribution API

Need API/concept
Get Runtime system UGameInstance::GetSubsystem<UProbabilityDirectorSubsystem>()
Register Stream CreateStream
Check Stream HasStream
Read config GetStreamConfiguration
Enumerate GetStreamIds
Remove RemoveStream
Next result Sequential API
Semantic deterministic result Keyed API + FProbabilityDecisionKey
Reproducibility Fixed Seed Source
Fresh RNG Secure Seed Source
Blueprint project wrappers UBlueprintFunctionLibrary
Runtime debugging Probability Director Runtime Inspector

  1. Resolve the subsystem from the current GameInstance.
  2. Never use a process-global Probability Director pointer.
  3. Create long-lived Streams during stable initialization.
  4. Centralize Stream IDs.
  5. Separate unrelated Sequential systems.
  6. Use Keyed identity for semantic deterministic decisions.
  7. Own Occurrence explicitly.
  8. Check every public operation’s result/status.
  9. Keep your project wrapper thin.
  10. Use the Inspector rather than exposing private RNG internals.

For reusable authored numeric behavior, reference or load an UNumericDiceResolutionRules Data Asset. The asset is shared immutable configuration; it does not contain Stream state or a Decision Key.

#include "ProbabilityDirectorResolutionRules.h"
#include "ProbabilityDirectorSubsystem.h"
UNumericDiceResolutionRules* Rules = /* project-owned asset reference */;
FProbabilityResolutionContext Context;
Context.StreamId = MyRegisteredStreamId;
FProbabilityNumericResolutionOverrides Overrides;
Overrides.AdditionalFlatModifier = CurrentModifier;
Overrides.bHasAdditionalDiceBias = true;
Overrides.AdditionalDiceBiasPercent = 15;
FProbabilityNumericDiceResolution Total;
const FProbabilityResolutionRulesStatus Status =
Probability->TryResolveNumericRulesTotal(Rules, Context, Overrides, Total);
if (Status.IsSuccess())
{
const int64 Value = Total.FinalOutcome;
}

Use TryResolveNumericRulesSuccessCount with FProbabilityNumericSuccessCountResolution when the asset uses Success Count, and read SuccessCount only after success. Always check FProbabilityResolutionRulesStatus before using outputs. Flat Modifier composition is checked additive composition; AdditionalDiceBiasPercent is optional and exact. The Data Asset is never mutated.

For Persistent Keyed resolution, set Context.DecisionPolicy = EProbabilityResolutionDecisionPolicy::PersistentKeyed, enable Context.bOverrideDecisionPolicy, and supply a valid FProbabilityDecisionKey with stable Namespace, StableSubjectId, Decision, nonnegative Occurrence, and KeySchemaVersion 1.

See DataAssets.md for the asset boundary and StreamsAndDecisionKeys.md for stream ownership.