Play your first story
The smallest program that starts a session and prints a line — no actor, no widget, about forty lines of C++, or four Blueprint nodes.
All documentation — Start here
Start here
Concepts
Authoring for a build
Playing a story
The generated assets and code
The plugin and sync
Nothing on this page needs a level, an actor or a widget. It needs a sync output directory — the
folder the sync tool writes, with runtime.json inside it — and a game instance.
In Blueprint
Four nodes, on any Blueprint that has a game instance to hand:
- Get TaleShard — the game-instance subsystem.
- Create Session From Directory with the directory, no policy and no language. The Invalid pin carries the reason when the payload cannot be read.
- Bind Event to On Step Presented on the session, with a custom event that prints the step’s Get Plain Text.
- Play, with an empty board and node — the author’s own start.
The first line prints. Wire a key to Advance and the next one does.
In C++
#pragma once
#include "CoreMinimal.h"#include "TaleShardSessionTypes.h"#include "TaleShardStepTypes.h"#include "UObject/Object.h"#include "DocFirstStory.generated.h"
class UGameInstance;class UTaleShardSession;
// The smallest program that plays a story: build a session from a sync output directory, bind// the step event, and play.UCLASS()class UDocFirstStory : public UObject{ GENERATED_BODY()
public: bool Begin(UGameInstance* GameInstance, const FString& SyncOutputDirectory);
private: UFUNCTION() void HandleStepPresented(UTaleShardSession* Session, const FTaleShardPlayStep& Step);
UFUNCTION() void HandleErrorRaised(UTaleShardSession* Session, const FTaleShardEngineError& Error);};#include "DocFirstStory.h"
#include "Engine/GameInstance.h"#include "Support/DocFictions.h"#include "TaleShardSession.h"#include "TaleShardStepLibrary.h"#include "TaleShardSubsystem.h"
bool UDocFirstStory::Begin(UGameInstance* GameInstance, const FString& SyncOutputDirectory){ UTaleShardSubsystem* TaleShard = GameInstance->GetSubsystem<UTaleShardSubsystem>();
// The error handler is an argument rather than an event because applying the policy IS a // check, and it runs while the session is being built - a binding made afterwards is // already late. With no policy there is nothing to refuse, but the shape is the same. FTaleShardErrorRaisedHandler OnError; OnError.BindDynamic(this, &UDocFirstStory::HandleErrorRaised);
// Refuses rather than degrades: a missing file, a malformed body or an unsupported format // version answers false with the reason, rather than producing a story that plays nothing. UTaleShardSession* Session = nullptr; FText Reason; if (!TaleShard->CreateSessionFromDirectory(SyncOutputDirectory, nullptr, NAME_None, OnError, Session, Reason)) { FDocGameLog::Note(Reason.ToString()); return false; }
// Bind before Play: a step presented before the binding is not replayed. Session->OnStepPresented.AddDynamic(this, &UDocFirstStory::HandleStepPresented);
// The author's start board and node. Every verb answers false with a reason when refused. return Session->Play(FGameplayTag(), NAME_None, Reason);}
void UDocFirstStory::HandleStepPresented(UTaleShardSession* Session, const FTaleShardPlayStep& Step){ // Presented, not Fields: the engine has already decided what belongs on screen. Advance // comes from the player - a button, a key - never from inside this handler. FDocGameLog::Note(UTaleShardStepLibrary::GetPlainText(Step).ToString());}
void UDocFirstStory::HandleErrorRaised(UTaleShardSession* Session, const FTaleShardEngineError& Error){ // Not the end of the playthrough: the engine could not do what the data asked and carried on. FDocGameLog::Note(Error.Message);}FDocGameLog is not plugin API. It stands in for your own logging, and it is declared in a shim
header on this site so that is visible rather than implied.
What just happened
| Line | What it did |
|---|---|
| Create Session From Directory | read runtime.json from the directory, parsed it, replayed the payload’s own sync-time diagnostics into the session’s channel, and built the traversal over it |
| On Step Presented | the one event a presenter needs. Every step arrives resolved: its fields, its choices, its cast, already looked up — see reading a step |
| Play | began at the board and node the author chose. A project with no start board is a real answer, and Play refuses it with a reason rather than guessing |
The session survives a level transition, because the subsystem that built it lives as long as the game instance. End Session on the subsystem releases it — after Stop, if ending the story is what you mean.
What to do differently in a real game
- Use the component. The TaleShard Player component owns a session for as long as its actor lives, and its events can be bound before a story exists — starting a session.
- Load the payload as an asset. The sync writes a payload asset into your project; the directory route above is for a tool, a test, or a build that ships content beside the executable.
- Draw with the presenter widget rather than printing plain text — drawing a line of text.
Next
Your first attempt — the ordering rules that cost an afternoon each.