Skip to content

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
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 isThe event hasThe story
instantthe argument pinscarries on as soon as the event has run
awaitedthe argument pins and a Done pinwaits 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

DocCommandHandler.h
#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);
};
DocCommandHandler.cpp
#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

RegistrationThe storyYour function
Register Commandcarries on as soon as your function returns, and reads what it returnedtakes the arguments, returns a story value — the absent value when there is nothing to say
Register Async Commandstays suspended until the completion is settledtakes 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:

CallWhat the story reads
Complete With Value, Complete With Text, Complete With Number, Complete With Bool, Complete With Text Listdone, with a value the engine writes into the command’s result variable, when one was declared
Complete With Nothingdone, with no value
Failthe 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.

Written for
Unity developers
Last updated