VNLEWIKIBuild stories. Connect worlds.Deutsch
VNLE Handbook

Unreal Engine

← Bring content into your game

Unreal Engine 5 · C++

Start with one visible sentence. This small example reads the real Harbour export directly; it is not a complete dialogue player. Then connect the data to your game UI.

1 · Prepare files and scene

Use a C++ project and your own Actor placed in the open map. Add Json to your module dependencies in .Build.cs. Put story.json in Content/Story. Add the helper below to the Actor .cpp and call UE_LOG(LogTemp, Display, TEXT("%s"), *ReadWelcome()); in BeginPlay after Super::BeginPlay(). Read the result in Output Log.

story.json · Export files explained

2 · Show the first text

// Add these includes at the top of your existing Actor .cpp file.
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

// File-local helper; call from your Actor's BeginPlay.
static FString ReadWelcome()
{
    FString Raw;
    if (!FFileHelper::LoadFileToString(Raw, *(FPaths::ProjectContentDir() / TEXT("Story/story.json"))))
        return TEXT("Cannot load story.json");
    TSharedPtr<FJsonObject> Story;
    const auto Reader = TJsonReaderFactory<>::Create(Raw);
    if (!FJsonSerializer::Deserialize(Reader, Story) || !Story.IsValid())
        return TEXT("Invalid JSON");
    // Following accesses expect the supplied, validated Harbour export.
    const auto Content = Story->GetObjectField(TEXT("content"));
    const FString Id = TEXT("fd7b61a3-8233-4ba7-8f77-b88b119a91bf");
    auto Message = Content->GetObjectField(TEXT("texts"))->GetObjectField(Id)->GetObjectField(TEXT("sourceMessage"));
    const FString Locale = TEXT("en");
    const TSharedPtr<FJsonObject>* Table = nullptr;
    const TSharedPtr<FJsonObject>* Translation = nullptr;
    if (Content->GetObjectField(TEXT("translations"))->TryGetObjectField(Locale, Table)
        && (*Table)->TryGetObjectField(Id, Translation))
        Message = (*Translation)->GetObjectField(TEXT("message"));
    FString Line;
    for (const auto& Segment : Message->GetArrayField(TEXT("segments")))
    {
        const auto Part = Segment->AsObject();
        if (Part->GetStringField(TEXT("kind")) != TEXT("text"))
            return TEXT("Text-only example");
        Line += Part->GetStringField(TEXT("text"));
    }
    return Line;
}

Expected: “Welcome to the harbour. I am Mira.” With locale = "de" or the language parameter de: “Willkommen im Hafen. Ich bin Mira.” A missing language uses source text. This sample supports text segments; it deliberately does not silently discard placeholders.

3 · From one sentence to a conversation

The fixed text ID above is for the first test. In your game choose flowRef/entryRef from story-index.json, read execution.entries[entryRef].nodeRef, then execution.nodes[nodeRef]. The current dialogue supplies text.textRef and speakerRef. Continuations depend on node kind.

For a dialogue bar use UMG: UTextBlock::SetText(FText::FromString(Line)), UButton::OnClicked and UImage. The widget needs the UMG module dependency and bound widget fields. End: RemoveFromParent() or change visibility and restore game input mode. The JSON reader above does not require UMG.

  1. Continue and End: follow connections, close the text box
  2. Choices: answer ID, order and selected continuation
  3. Conditions and variables: ten or two gold
  4. Language: the same IDs, newly resolved content
  5. Command: give an item or open a shop
  6. Call/Return: visit a substory and return
Inspect answers and the gold check in this language

Read the data for a choice

Insert this fragment where story is available after loading (for RPG Maker: const story = $gameTemp.vnleExample.story;). It prints IDs for inspection. Replace the output with your answer buttons. It covers the two Harbour answers and their following gold check, not a general interpreter.

// Inside ReadWelcome, after Story has been parsed successfully.
const auto Nodes = Story->GetObjectField(TEXT("execution"))->GetObjectField(TEXT("nodes"));
const auto Choice = Nodes->GetObjectField(TEXT("42edfff8-b8fe-46bf-ae0d-9e2347b6e726"));
for (const auto& IdValue : Choice->GetArrayField(TEXT("optionOrder")))
{
    const FString OptionId = IdValue->AsString();
    const auto Option = Choice->GetObjectField(TEXT("options"))->GetObjectField(OptionId);
    UE_LOG(LogTemp, Display, TEXT("%s: %s"), *OptionId, *Option->GetObjectField(TEXT("text"))->GetStringField(TEXT("textRef")));
}
const auto Branch = Nodes->GetObjectField(TEXT("c1c3c372-0a73-4cd0-851d-ede59d5709bb"));
const double Minimum = Branch->GetObjectField(TEXT("condition"))->GetObjectField(TEXT("right"))->GetObjectField(TEXT("value"))->GetNumberField(TEXT("value"));
const int32 Gold = 10; // Repeat with 2.
const auto Next = Branch->GetObjectField(Gold >= Minimum ? TEXT("whenTrue") : TEXT("whenFalse"));
UE_LOG(LogTemp, Display, TEXT("%s"), *Next->GetStringField(TEXT("nodeRef")));

With 10 gold, next refers to “Pay five gold”; with 2 it refers to “Not enough gold”. In the actual game, run the check only when the selected path reaches this node. Each selected answer supplies its own continuation.

4 · Map your own images

This supplementary excerpt expects a current dialogue object named node and the UI elements named in its comments. For C++ and MonoGame, speakerRef has already been read as SpeakerId or speakerId. The key is Mira’s actual character ID; the image name and location belong to your game. Insert the lines at the indicated locations.

// In your UUserWidget header; assign the imported texture in its Blueprint defaults.
UPROPERTY(EditDefaultsOnly, Category="Dialogue")
TMap<FString, TObjectPtr<UTexture2D>> Portraits;
// Set the map key to: 807b2957-7bab-4a28-83a4-1f3d1589bbc2
// In widget code, PortraitImage is your bound UImage; SpeakerId comes from speakerRef.
auto* Texture = Portraits.Find(SpeakerId);
PortraitImage->SetBrushFromTexture(Texture ? Texture->Get() : nullptr);
PortraitImage->SetVisibility(Texture && Texture->Get()
    ? ESlateVisibility::Visible : ESlateVisibility::Hidden);

Resolve speaker names through content.characters[speakerRef].nameTextRef just like dialogue text. Missing mappings should hide the previous portrait or show a placeholder. Images are not selected by language.

Use image references from the complete export

5 · Ship files with the game

Add Story under Project Settings → Packaging → Additional Non-Asset Directories to Package. FFileHelper uses Unreal file access. Do not import this JSON as a DataTable: its nested ID maps are not DataTable rows. This guide uses C++; exposing results to Blueprint is a project decision.

Check your result

  1. English and German greetings display correctly; an unavailable language falls back to English.
  2. When adding flow: try both answers, gold 10/2, the return from the substory and End.
  3. Run the built game outside your project folder. JSON, fonts and portraits must be available there too.

Official documentation

API and setup references for this approach: