Live Sync
Live Sync connects Unreal Engine to the StoryFlow Editor via WebSocket, letting you edit your project and see changes reflected in the engine in real time - no manual re-export and re-import after every change.
Overview
Live Sync establishes a WebSocket connection between the Unreal Editor and the StoryFlow Editor running on the same machine. When you save changes in the StoryFlow Editor, those changes are automatically pushed to Unreal and re-imported, keeping your in-engine project data up to date without any manual steps.
- Real-time synchronization - edit in StoryFlow, see results in Unreal immediately
- WebSocket-based communication using
ws://localhost:9000by default - Automatic re-import when changes are detected, writing only the assets whose source actually changed
- Blueprint and C++ accessible through
UStoryFlowEditorSubsystem
Editor-Only Feature
Live Sync is provided by UStoryFlowEditorSubsystem, which lives in the StoryFlowEditor module. It is only available in the Unreal Editor and is not included in packaged builds. This is intentional - Live Sync is a development workflow tool, not a runtime feature.
Architecture
Live Sync is built from three components that work together to manage the connection and synchronization lifecycle:
- UStoryFlowEditorSubsystem (UEditorSubsystem) - The main interface you interact with from Blueprint or C++. It exposes connection management, sync requests, and import functions. As an editor subsystem, it is automatically created and destroyed with the editor.
- FStoryFlowWebSocketClient - Handles the low-level WebSocket connection, including establishing the connection, reconnection logic (up to 5 attempts), the initial handshake with the StoryFlow Editor, and ping/keep-alive messages to maintain the connection.
- FStoryFlowSyncManager - Manages the synchronization workflow. It receives incoming project update messages from the WebSocket client, coordinates the re-import process through
UStoryFlowImporter, and fires completion delegates.
Setup
Getting Live Sync running takes just a few steps. Make sure both the StoryFlow Editor and Unreal Editor are open on the same machine.
Step 1: Open your project in the StoryFlow Editor
The StoryFlow Editor runs a sync server that listens for incoming WebSocket connections. Open the project you want to synchronize - the server starts automatically.
Step 2: Connect from Unreal
Call ConnectToStoryFlow() on the editor subsystem. You can do this from a Blueprint utility widget, an editor toolbar button, or C++ code.
// Connect with default settings (localhost:9000)
UStoryFlowEditorSubsystem* Subsystem =
GEditor->GetEditorSubsystem<UStoryFlowEditorSubsystem>();
if (Subsystem)
{
Subsystem->ConnectToStoryFlow();
} If you need to use a different host or port, pass them as parameters:
// Connect with custom host and port
Subsystem->ConnectToStoryFlow(TEXT("localhost"), 9000); Step 3: Request an initial sync
After connecting, call RequestSync() to pull the full project from the editor. This performs an initial import so your Unreal project matches the current state of the StoryFlow Editor.
// Request a full project sync
Subsystem->RequestSync(); Step 4: Edit and iterate
From this point on, any changes you save in the StoryFlow Editor are automatically pushed to Unreal. You do not need to call RequestSync() again unless you want to force a full refresh.
Blueprint Setup
You can call ConnectToStoryFlow and RequestSync from Blueprint as well. Get the editor subsystem using the Get Editor Subsystem node with UStoryFlowEditorSubsystem as the class, then call the functions directly from your event graph.
Connection Functions
The following functions are available on UStoryFlowEditorSubsystem and are all BlueprintCallable:
-
ConnectToStoryFlow(FString Host = "localhost", int32 Port = 9000)- Establishes a WebSocket connection to the StoryFlow Editor's sync server. Both parameters are optional and default tolocalhostand9000respectively. -
Disconnect()- Closes the active WebSocket connection and stops any reconnection attempts. -
IsConnected() -> bool(BlueprintPure) - Returns whether the WebSocket connection is currently active. -
RequestSync()- Sends a request to the StoryFlow Editor for a full project sync. The editor responds with the complete project data, which triggers a re-import.
Import Functions
The editor subsystem also provides direct import functions that Live Sync uses internally. You can call these yourself for manual imports outside of the sync workflow:
-
ImportProject(FString BuildDirectory, FString ContentPath = "/Game/StoryFlow") -> UStoryFlowProjectAsset*- Imports an entire StoryFlow project from a build directory. Returns the created project asset. -
ImportScript(FString JsonPath, FString ContentPath = "/Game/StoryFlow/Data") -> UStoryFlowScriptAsset*- Imports a single script file from a JSON path. Returns the created script asset. -
SetContentPath(FString Path)- Sets the content directory where imported assets are placed. -
GetContentPath() -> FString- Returns the current content path. -
GetProjectAsset() -> UStoryFlowProjectAsset*- Returns the most recently imported project asset.
Events and Delegates
The editor subsystem exposes three delegates you can bind to for responding to connection and sync events:
-
OnConnected- Fires when the WebSocket connection to the StoryFlow Editor is successfully established. -
OnDisconnected- Fires when the connection is lost, whether due to the editor closing, a network issue, or an explicitDisconnect()call. -
OnSyncComplete(UStoryFlowProjectAsset* Project)- Fires when a full project sync finishes. The parameter is the newly imported project asset, ready to be used by yourStoryFlowComponentinstances.
// Binding to Live Sync events in C++
UStoryFlowEditorSubsystem* Subsystem =
GEditor->GetEditorSubsystem<UStoryFlowEditorSubsystem>();
if (Subsystem)
{
Subsystem->OnConnected.AddDynamic(
this, &AMyEditorActor::HandleConnected);
Subsystem->OnDisconnected.AddDynamic(
this, &AMyEditorActor::HandleDisconnected);
Subsystem->OnSyncComplete.AddDynamic(
this, &AMyEditorActor::HandleSyncComplete);
}
void AMyEditorActor::HandleConnected()
{
UE_LOG(LogTemp, Log, TEXT("StoryFlow Live Sync connected"));
}
void AMyEditorActor::HandleDisconnected()
{
UE_LOG(LogTemp, Warning, TEXT("StoryFlow Live Sync disconnected"));
}
void AMyEditorActor::HandleSyncComplete(UStoryFlowProjectAsset* Project)
{
UE_LOG(LogTemp, Log, TEXT("StoryFlow project synced: %s"),
*Project->GetName());
} How It Works Internally
Understanding the internal flow helps when debugging sync issues or extending the system:
- Connection -
FStoryFlowWebSocketClientestablishes a WebSocket connection to the StoryFlow Editor's sync server at the specified host and port. - Handshake - On connection, the client sends a handshake message to identify itself to the editor. The editor acknowledges and begins monitoring for project changes.
- Change detection - When you save changes in the StoryFlow Editor, it pushes the updated project data over the WebSocket as a JSON message.
- Sync manager -
FStoryFlowSyncManagerreceives the incoming data and callsHandleProjectUpdated(), which writes the data to temporary files and triggers a full project re-import throughUStoryFlowImporter. - Re-import - The importer re-reads all project files (scripts, characters, assets), compares each source against the hash recorded on the asset it produced last time, and creates or updates only the assets whose source changed. See Incremental Sync.
- Completion - The
OnSyncCompletedelegate fires with the newUStoryFlowProjectAsset*, notifying any listeners that the project has been updated. - Component update - Active
StoryFlowComponentinstances automatically pick up the updated project asset through the editor subsystem, so running dialogues can reflect the latest changes.
Reconnection
If the WebSocket connection drops unexpectedly - for example, if the StoryFlow Editor is restarted or the connection is interrupted - the client automatically attempts to reconnect.
- Up to 5 reconnection attempts are made with increasing delay between attempts
- Connection state changes are broadcast through
FStoryFlowWebSocketClientdelegates UStoryFlowEditorSubsystemtranslates these internal delegates into its ownOnConnectedandOnDisconnecteddelegates for your code to respond to- If all reconnection attempts fail, the
OnDisconnecteddelegate fires and you will need to callConnectToStoryFlow()again manually
Monitoring Connection Status
Use IsConnected() to check the current connection state at any time. You can also bind to OnConnected and OnDisconnected to update a status indicator in your editor UI so you always know whether Live Sync is active.
Default Content Path
All assets imported through Live Sync are placed under a configurable content path. The default is /Game/StoryFlow.
- Scripts are imported to
/Game/StoryFlow/Data/ - Character assets go to
/Game/StoryFlow/Characters/ - The project asset is created at
/Game/StoryFlow/
To change the content path, call SetContentPath() before connecting or requesting a sync:
UStoryFlowEditorSubsystem* Subsystem =
GEditor->GetEditorSubsystem<UStoryFlowEditorSubsystem>();
if (Subsystem)
{
// Change where imported assets are placed
Subsystem->SetContentPath(TEXT("/Game/MyGame/Dialogue"));
Subsystem->ConnectToStoryFlow();
Subsystem->RequestSync();
} Script and Folder Names Become Package Names
A script's file name and every folder segment above it end up in the package path of the asset it produces, and Unreal does not allow every character in a package name. Since v1.2.2 the importer sanitizes each segment: a segment that is already valid passes through unchanged, and one that is not (a space, an apostrophe, a dot) is cleaned and given a short suffix so two distinct names cannot collide. When that happens the Output Log warns with the script and the path it imported as. Your Script property is unaffected either way - the project keys its script map on the raw relative path, so the path you pick in the Details panel dropdown does not change. On plugin versions before 1.2.2 those names went into the package path untouched, which is one of the causes covered in Dialogue Works in Editor but Not in Packaged Builds.
Data-Only Sync
Added in v1.1.0. When the StoryFlow Editor pushes a sync with Data Only enabled, the editor sends only the script graphs, variables, and character data — image and audio files are not copied into the Unreal build directory. The plugin's importer detects the missing source files and transparently reuses the existing imported assets in your project instead of failing or re-importing blanks.
This is intended for fast script-only iteration. Once your image and audio assets are imported once via a full sync, you can flip the editor into Data Only mode and keep editing the story graph without paying the cost of copying media files on every save.
How to enable it:
- Perform at least one full sync so the plugin has imported all media assets into the content path (default
/Game/StoryFlow/). - In the StoryFlow Editor, toggle Data Only in the live-sync options for your Unreal target.
- Continue editing — script and variable changes push instantly; media stays untouched.
What happens on the Unreal side:
- When the importer looks for a media source file and does not find it, it checks whether the corresponding asset already exists at the computed package path.
- If the asset exists, it is reused and registered against the script's resolved-assets map — no re-import, no warning.
- If the asset does not exist (you added a new image but haven't done a full sync yet), the importer logs
Source file not found: <path>and skips that asset. Trigger a full sync to pick it up.
When to Use Each Mode
Use full sync when you add or replace media assets, or when starting a new project. Switch to data-only sync once your media is stable and you are iterating on dialogue, branching logic, or variables — the round-trip from "save in editor" to "playing in Unreal" is significantly faster because no texture/audio re-import happens.
Incremental Sync
Added in v1.2.2. A sync re-reads the whole exported project, but it does not rewrite the whole project. Every script, character and project asset records a hash of the source it was imported from in its ImportedSourceHash field. When the incoming source hashes to the same value and the asset's .uasset is still on disk, the disk write is skipped and the package is left clean.
- A steady-state sync writes nothing. If nothing changed in the StoryFlow Editor, no asset is touched.
- A story edit writes only what it touched. Editing one script rewrites that script's asset. The other scripts and the characters are left alone, and the project asset is rewritten only when the edit changed something it holds itself, such as adding or removing a script, a character or a media asset.
- A missing file is rewritten anyway. The skip requires a matching hash and the
.uassetstill being present, so an asset whose file was removed from disk is written again even when its source has not changed. - Failed writes retry. The hash is kept only when the save actually lands. A save that fails, or one that is deferred, clears it so the next sync re-imports that asset instead of skipping it.
The First Sync After Upgrading Rewrites Everything
Assets imported by a plugin older than v1.2.2 carry no import hash, so the first sync after upgrading rewrites every script, character and project asset once. Under revision control that first sync checks out the whole StoryFlow folder. This is expected. Every sync after it touches only what actually changed.
Revision Control
Added in v1.2.2. When the Unreal Editor has a revision control provider enabled, the importer checks a tracked file out before overwriting it and marks a newly created file for add after writing it. A sync then lands in your pending changelist as exactly the assets the story edit touched, ready to review and submit.
- Existing tracked files are checked out before the save. A file that is untracked, brand new, already checked out or already marked for add passes straight through.
- Newly written files are marked for add once the save succeeds.
- No provider enabled means no change at all - every one of these calls is a success no-op, so projects without revision control behave exactly as before.
Two situations are reported without stopping the import:
- A file that cannot be checked out, for example one exclusively checked out by a teammate, is named in the log along with the reason. That one asset is skipped and the rest of the import continues.
- A file that did not exist locally but is already tracked by the provider produces a warning that your workspace is probably out of date, because a teammate added that asset and your local write landed without a checkout. The file itself is written; only the mark-for-add is skipped.
If the provider's state cannot be read at all (unreachable server, failed query), the situation is logged and the save is attempted regardless. It succeeds whenever the file was writable anyway, and otherwise fails with the usual read-only diagnosis. See Sync Cannot Write an Asset for the exact messages and what to do about them.
Limitations
There are a few limitations to be aware of when using Live Sync:
- Editor-only - Live Sync lives in the
StoryFlowEditormodule and is not available in packaged builds. It is strictly a development workflow tool. - Local only - The default connection targets
localhost, which requires the StoryFlow Editor to be running on the same machine as the Unreal Editor. Remote connections are not officially supported. - Full re-read, incremental write - Each sync re-reads the entire exported project, so the parsing cost still scales with project size, but since v1.2.2 only the assets whose source actually changed are written to disk. See Incremental Sync.
- Older plugins and map variables - When a pre-1.2.0 plugin connects while the open project uses map variables, the StoryFlow Editor shows a warning toast during the live-sync handshake naming the plugin version and saying it doesn't support Maps and should be updated to 1.2.0 or later. Update the plugin to 1.2.0 or later to run scripts that use maps. See Forward Compatibility Warnings for how older runtimes behave when they hit unsupported nodes.