Implementing commands
The command handler class, the two ways to register a command, the completion a waiting story hands you, and the one event that catches the rest.
All documentation — Playing a story
Start here
Concepts
Authoring for a build
Playing a story
The generated assets and code
The plugin and sync
On this page
A command declared in the TaleShard editor is something the story calls and your game answers. In
Unreal the answer lives in a command handler: a class you derive from TaleShardCommandHandler
— as a Blueprint or in C++ — and set as the Command Handler Class on the TaleShard Story actor or
the TaleShard Player component. The component creates one for its session, asks it to register its
commands before the story starts, and routes everything nothing registered to its On Command event.
In Blueprint: one event per command
In the handler Blueprint’s Event Graph, right-click and open TaleShard > Commands: the menu lists every command the project declares, and follows every sync. Each one places a red event, TaleShard Command: name, with one output pin per argument, typed from the command’s declaration, in the author’s order.
| The command is | The event has | The story |
|---|---|---|
| instant | the argument pins | carries on as soon as the event has run |
| awaited | the argument pins and a Done pin | waits until you call a Complete function, or Fail, on Done — see the completion |
Every event is registered for you when the session is built, after Register Commands, so an event
takes its command over from anything Register Commands wired for the same code. Two events for one
command stop the Blueprint compiling, and the event can only be placed in a Blueprint derived from
TaleShardCommandHandler. A command the writer removed or whose arguments changed is reported by
the next sync and marks the event with a compile error until you delete or refresh it.
Under the node, the event compiles into a function named TaleShardCommand_<code>; that name is
what the handler registers. A command with no event reaches On Command.
In C++: registering by hand
#pragma once
#include "CoreMinimal.h"#include "TaleShardCommandHandler.h"#include "TaleShardValue.h"#include "DocCommandHandler.generated.h"
class UTaleShardCommandCompletion;
// Where a game's command logic lives. Set it as the Command Handler Class on the player// component; the component creates one for its session and asks it to register its commands.UCLASS()class UDocCommandHandler : public UTaleShardCommandHandler{ GENERATED_BODY()
public: virtual void RegisterCommands_Implementation(UTaleShardSession* Session) override; virtual void OnCommand_Implementation(FGameplayTag Code, const TArray<FTaleShardValue>& Args) override;
private: // "playsound" - one field, sound_name (text). Answers at once. UFUNCTION() FTaleShardValue HandlePlaySound(const TArray<FTaleShardValue>& Args);
// "fade_out" - the story waits until the completion is settled. UFUNCTION() void HandleFadeOut(const TArray<FTaleShardValue>& Args, UTaleShardCommandCompletion* Completion);};#include "DocCommandHandler.h"
#include "Support/DocFictions.h"#include "TaleShardCommandCompletionObject.h"#include "TaleShardSession.h"
void UDocCommandHandler::RegisterCommands_Implementation(UTaleShardSession* Session){ FText Reason;
// A command that answers at once. The delegate returns the command's value; return the // absent value when the command has nothing to say. FTaleShardCommandDelegate PlaySound; PlaySound.BindDynamic(this, &UDocCommandHandler::HandlePlaySound); Session->RegisterCommand(FGameplayTag::RequestGameplayTag(TEXT("TaleShard.Command.playsound"), false), PlaySound, Reason);
// A command the story waits for. The story stays suspended until the completion is // settled, with no timeout: a fade may legitimately take as long as it takes. FTaleShardAsyncCommandDelegate FadeOut; FadeOut.BindDynamic(this, &UDocCommandHandler::HandleFadeOut); Session->RegisterAsyncCommand(FGameplayTag::RequestGameplayTag(TEXT("TaleShard.Command.fade_out"), false), FadeOut, NAME_None, Reason);}
void UDocCommandHandler::OnCommand_Implementation(FGameplayTag Code, const TArray<FTaleShardValue>& Args){ // Whatever the story called that nothing above registered. It runs to completion here and // answers nothing. FDocGameLog::Note(FString::Printf(TEXT("unhandled command %s"), *Code.ToString()));}
FTaleShardValue UDocCommandHandler::HandlePlaySound(const TArray<FTaleShardValue>& Args){ // Arguments arrive as story values, in the order the command declares its parameters. Read // them through the value library, which applies the language's own coercions: playsound(3) // reaches here as the number 3, and reading it as text gives "3". const FString SoundName = Args.Num() > 0 ? UTaleShardValueLibrary::Conv_ValueToString(Args[0]) : FString(); FDocAudio::Play(SoundName); return UTaleShardValueLibrary::MakeNullValue();}
void UDocCommandHandler::HandleFadeOut(const TArray<FTaleShardValue>& Args, UTaleShardCommandCompletion* Completion){ // Hand the completion to whatever takes the time, and settle it when that finishes. A // completion settled twice is ignored, and one whose playthrough has since restarted // reports itself stale. FDocScreenFade::Begin(Completion);}FDocAudio, FDocScreenFade and FDocGameLog are not plugin API. They stand in for your own game
code, and they are declared in a shim header on this site so that is visible rather than implied.
Registering by hand in a Blueprint handler is the same shape: two events — Register Commands and On Command — and the session’s Register Command and Register Async Command nodes, each taking an event you create from the node’s delegate pin. The TaleShard Command event above does all of that for you.
The two registrations
| Registration | The story | Your function |
|---|---|---|
| Register Command | carries on as soon as your function returns, and reads what it returned | takes the arguments, returns a story value — the absent value when there is nothing to say |
| Register Async Command | stays suspended until the completion is settled | takes the arguments and a completion, and settles it later |
Whether the story waits is decided by how the command is registered. A TaleShard Command event registers it the way the author declared it: awaited when the author marked the command Awaited in TaleShard, which is why that event has a Done pin. Registering it yourself, as above, is your own choice of the two kinds and ignores the author’s mark. Registering a code twice replaces the earlier registration, whichever kind it was.
A registration is by the command’s tag, TaleShard.Command.<Code>, which the sync registers for
every command the project declares. In Blueprint that is the tag picker.
The completion
An awaited command’s function is handed a completion object. Keep it wherever the work happens — a timeline, a latent action, a widget — and settle it exactly once:
| Call | What the story reads |
|---|---|
| Complete With Value, Complete With Text, Complete With Number, Complete With Bool, Complete With Text List | done, with a value the engine writes into the command’s result variable, when one was declared |
| Complete With Nothing | done, with no value |
| Fail | the command failed, with a reason. A failure never carries a result |
Settling twice is ignored. Is Stale is true when the playthrough that issued the completion has since restarted or stopped — Stop deliberately stales every in-flight token, so an abandoned fade cannot resume the next playthrough.
The engine imposes no timeout. A command may legitimately last minutes, and resuming the story behind the player’s back would be worse than waiting.
The rest: On Command
A command the story calls that nothing registered reaches On Command — on the handler, and on the session — with its tag and its arguments. It runs to completion during script execution and answers nothing; the story never suspends for one, because an unregistered command has no registration to say it should. Register the command as awaited when the story must wait.
Arguments
Arguments arrive as story values, in the order the command declares its parameters, exactly as the story passed them. An omitted optional argument is simply not there, so a function that reads a parameter the author may leave out checks the count first. Read each one through the TaleShard Value library — as text, a number, a bool, or a list — which applies the expression language’s own coercions rather than a cast that would fail on the same call.
Firing a call a field holds
A commandCall field holds a call the story never fires by itself: the game fires it, when it
decides to. Get Command reads it off the field as a Command Ref — Code, Tag, Args
and Arg Variables — and the session’s Fire Command sends it to the registered synchronous
function, or to On Command when nothing is registered. A command registered as awaited is refused
there, because only the story can wait for one.
An argument the author bound to a project variable holds the variable’s value as of the moment
the step was composed, and Arg Variables names that variable at the same position (the name
as an author writes it, context.code for a scoped one; empty for every other argument). A game
that fires the call later — after the story has moved on, or from a menu the player opens
afterwards — passes it through Reread Command Arguments first: it returns the same call with
every bound argument read from the session’s variables now and every other argument unchanged. In
C++ the same holds for a raw payload reference: FTaleShardCommandCall::FromRef(Reference, Runtime, OutCall, OutReason) reads the bound arguments from the runtime it is given, and the overload
without a runtime leaves them absent.
Never re-register a project command in a second place
The one registration on this site made outside a command handler is retry(), the command the
Puzzles & world pack brings, declared as awaited: the
Checkpoint handler registers it itself — as awaited,
matching that declaration — the first time the story reaches a checkpoint, because the handler holds
the snapshot the command returns to. Its registration replaces whatever registered retry before,
a generated command class’s included, so implement nothing for retry in your own handler. Until a
checkpoint has been reached, a retry() goes where your command handler sends it: with generated
types on, to the unimplemented OnRetryAsync, whose default fails the wait and reports an error
naming the command; with none, to On Command. Either way the story then carries on.
Next
Starting a session — where the handler class is installed.