Skip to content

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
On this page

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:

  1. Get TaleShard — the game-instance subsystem.
  2. Create Session From Directory with the directory, no policy and no language. The Invalid pin carries the reason when the payload cannot be read.
  3. Bind Event to On Step Presented on the session, with a custom event that prints the step’s Get Plain Text.
  4. 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++

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

LineWhat it did
Create Session From Directoryread 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 Presentedthe one event a presenter needs. Every step arrives resolved: its fields, its choices, its cast, already looked up — see reading a step
Playbegan 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.

Written for
Unity developers
Last updated