Unreal Engine plugin

VgScript for Unreal Engine

Import your .vgs scripts into Unreal Engine and play them as subtitles, choices, conditions, facts and game actions, with per-language subtitle tables and CSV translation.

Get it on FabExample scripts on GitHubBack to documentation
  • v0.3.2
  • Unreal Engine 5.8
  • Win64
  • Blueprint & C++
On this page

The VgScript plugin for Unreal Engine plays the scenes you write in VgScript. Talks become subtitles, decisions become choices, conditions read the game's state, and actions run your Blueprint or C++ code. It is available on Fab and ships with a showcase level and the short example story used throughout this page.

This page covers the Unreal side. For the language itself (tags, sublines, flow arrows, decisions) see the VgScript documentation.

The one rule to remember: the game plays what you last imported. After editing a script, click Extract Data in Unreal. It runs vgs, which the VgScript IDE extension installs, to number new entries and update the .vgs.meta sidecars, then imports them.

Version 0.3.2, in beta. Built for Unreal Engine 5.8 on Windows. Class names, settings and data formats can still change between releases, and Unreal shows a beta notice when you enable the plugin.

What you need

  • Unreal Engine 5.8 on Windows. Both of the plugin's modules are Win64-only: there is no macOS or Linux editor support, and a game packaged for another platform does not include the plugin.
  • The VgScript plugin, installed as in Install.
  • The free VgScript IDE extension, for JetBrains IDEs (Rider, IntelliJ IDEA, WebStorm…) or VS Code. It installs vgs, the tool that numbers entries and writes the .vgs.meta files Unreal imports, so you need it to import new or changed scripts. It also adds coloring, live checks, navigation and the travel view. The showcase plays without it. More IDEs are added as people ask for them: tell us which one.
PartWhat it contains
VgScript module (Runtime)Playback: the actions subsystem, the dialogue base classes, built-in actions and conditions, facts and localization. It depends only on engine modules: Core, CoreUObject, Engine and DeveloperSettings (plus AssetRegistry internally).
VgScriptEditor module (Editor)The import tool, its menu entry and toolbar button, and the entry picker shown in the Details panel.
Content/The import tool widget EUW_VgScript and the showcase in Showcase/.
Scripts/vgscript-example/The showcase's scripts: Scenes.txt, S1_Room.vgs, S2_End.vgs and their sidecars. The same story, with its translation CSVs, is public on GitHub as vgscript-example.

Install

  • From Fab: add the plugin to your library on Fab, install it to Unreal Engine 5.8 from the Epic Games Launcher, enable VgScript in Edit → Plugins, and restart the editor.
  • Into a project: copy the plugin folder into your project's Plugins/ folder and rebuild the project, which needs Visual Studio with C++ support.
  • For C++: add "VgScript" to your module's PublicDependencyModuleNames. Headers live under VgScript/, for example #include "VgScript/Actions/Base/VgScriptActionBase.h".

Quick start: play the showcase

  1. Enable VgScript in Edit → Plugins and restart the editor.
  2. In the Content Browser, open Settings and turn on Show Plugin Content.
  3. Open the map L_VgScriptShowcase in the plugin's Showcase/Levels folder (/VgScript/Showcase/Levels).
  4. Press Play. Vee and Gee arrive early in a room with a laptop. Click an option when Gee has to answer, or press 1, 2 or 3; click, Space or Enter skip ahead. The HUD switches the language between English, Spanish, Catalan and Chinese, lists the facts set so far and restarts the story.
  5. Open Scripts/vgscript-example/S1_Room.vgs in the plugin folder to read along. Every comment in it explains the feature the next lines use.

The showcase is already imported, so nothing needs to be built. To change its story, see Editing the showcase scripts.

Your first project

From script to game in four steps: write the .vgs in your IDE, Fix + Reconcile it with vgs, import it in Unreal with Extract Data and Update Subtitles, then play it from a Scene Player or the project settings.

1. Write the scripts

Create a folder for your narrative, for example <Project>/Narrative/, with a Scenes.txt and one .vgs file per scene. Copying Scripts/vgscript-example is the quickest start. Keep it outside Content/: Unreal reads the files from disk, not as assets.

// Scenes in play order.
S1_Intro.vgs
S2_Kitchen.vgs

The importer reads Scenes.txt like the vgs tools do. Each line names a scene by its path (acts/S1_Intro.vgs), its file name (S1_Intro.vgs) or its name without the extension ([1] /S1_Intro), matched without regard to case against the .vgs files under the folder of Scenes.txt. The name must match the whole file name: /Intro does not find S1_Intro.vgs. A path that does not exist falls back to a name match, and when two scripts share a name, the first one found is used. [n] prefixes at the start of a line and // comments are allowed.

Give each scene its own key, such as [S1] and [S2]: two scripts that declare the same key are merged into one scene without a warning. The IDE extension flags it.

Write your scenes. In your IDE, run VgScript → Fix + Reconcile All (Scenes.txt) on the folder to number new entries as you go, or leave it to the next import. If a scene jumps to another one, write the jump as ->(S2-A1) and run Resolve Cross-Script Links (or vgs resolve-links .): it replaces the code with the destination's stable hash, so renumbering scene 2 never breaks the jump. The importer turns those hashes back into scene keys. Both scenes must be listed in the same Scenes.txt: a jump to a scene that is not imported is dropped, with an error in the Output Log.

2. Create the two data assets

In the Content Browser choose Add → Miscellaneous → Data Asset twice:

  • a VgScriptScenesDataAsset, for example DA_Scenes, which receives the imported scenes;
  • a VgScriptActionRegistry, for example DA_VgScriptRegistry, which names the classes that present dialogue and run your own actions.

3. Make the dialogue classes

Talks and decisions need three small Blueprints that show subtitles and choices: a talk line, a talk and a decision. Presenting dialogue builds them step by step. To start faster, duplicate the showcase's B_ShowcaseTalkLine, B_ShowcaseTalkAction and B_ShowcaseDecision, with the widgets they use, into your project.

Then open the registry and set Talk Action Class to your talk Blueprint and Decision Action Class to your decision Blueprint. Without them, talks and decisions do not play.

4. Import

Open Tools → VgScript, or click the VgScript button on the level editor toolbar, next to Play. Fill in the settings:

The VgScript import tool beside the showcase level. Extract Data and Update Subtitles are at the top; Count Elements, Export CSVs, Import CSVs and the audio buttons sit at the end of the setting they use.

SettingWhat to enter
Vgs Scenes File PathYour Scenes.txt. Absolute, or relative: a relative path is tried against the project folder first, then the plugin folder. The … button fills in an absolute path, so type a relative one if you share the tool's values with your team.
Fix Scripts Before ImportOn by default. Extract Data first runs vgs fix-all on the scenes, like Fix + Reconcile All in the IDE extension: it numbers new entries, fills decision text and rewrites each .vgs.meta. It edits the .vgs files, so save them in your editor first. Turn it off to import the sidecars as they are, which is also what happens when vgs isn't installed.
Actions Data AssetThe scenes asset to fill.
Action Named Classes DataThe registry. Optional for importing: without it only the built-in actions and conditions resolve.
Data Tables PathContent folder for the subtitle tables. A value starting with / is a full path such as /Game/Narrative/Data/DataTables; anything else is relative to /Game. Default: Narrative/Data/DataTables.
Subtitle Data Table PathSubfolder of the tables path that holds one table per language. Default: Text.
Count File PathText file that Count Elements writes, resolved like the scenes path. Empty writes Count.txt next to Scenes.txt.
Cultures (under Localization)Languages to keep tables and CSVs for, such as en, es, zh-Hans. Empty uses Languages to Package from Project Settings → Packaging, then en.
Localization Folder (under Localization)Folder of the translation CSVs: absolute, or relative to the Scenes.txt folder. Default: Localization.

Then use the buttons. Extract Data and Update Subtitles are at the top of the tool; the others sit at the end of the setting they use.

ButtonWhereWhat it does
Extract DataTopFixes the scripts with vgs (see Fix Scripts Before Import), then empties the scenes asset and rebuilds it from every scene listed in Scenes.txt. A listed scene that can't be found, or that has no .vgs.meta yet, is skipped with a warning in the log. The asset is marked as changed but not saved. If vgs fails, nothing is imported.
Update SubtitlesTopCreates any missing subtitle table, one per culture, at <Data Tables Path>/<Subtitle Data Table Path>/<culture>, then fills each one with every talk line and decision option. Existing translations are kept; rows of lines that are no longer in the scenes asset are removed, with their translations.
Count ElementsCount File Path rowWrites the number of talks, talk lines, decisions and options to the count file.
Export CSVs / Import CSVsLocalization Folder rowThe translation round trip, see Localization.
Import Voice Files, Find Unused Audio, Update Audio TablesAudio rowsRecorded dialogue, see Voice-over.

For a first import: Extract Data, Update Subtitles, then File → Save All. Both actions also write the tables folder into the scenes asset, so the game finds the text. Every message, including errors, goes to the Output Log under LogVgScript.

Where the tool keeps its values

Values typed into the tool are saved as you change them, in Config/DefaultEditor.ini in your project. Plugin updates leave that file alone; commit it to share the values with your team. If the file is read-only, for example because source control has not checked it out, a notification says the settings were not saved.

A project can have more than one tool, such as the showcase's and one for your game. To add one, right-click /VgScript/EUW_VgScript, choose Create Child Blueprint Class and save the child in your project's content. To make Tools → VgScript open it, pick it in Project Settings → VgScript → Editor → Tool Widget. The showcase's EUW_VgScriptShowcase is made this way.

Each tool saves its values in its own section of the ini, named after the Blueprint's path, such as [/VgScript/Showcase/EUW_VgScriptShowcase], and they never affect another tool. Class Defaults are the starting values, which a child inherits from its parent:

  • The first value you change in an open tool saves all of its values. From then on the ini wins, so later changes to its Class Defaults, including the showcase's defaults in a plugin update, no longer apply to it.
  • To go back to Class Defaults, close the editor and delete the tool's section from the ini.
  • Close the tool's tab before editing its Class Defaults, or the open tool saves the new value to the ini too.
  • Moving or renaming a tool Blueprint leaves its old section behind, and the tool starts again from Class Defaults.

The vgs command-line tool

The plugin does not include vgs: it ships with the VgScript IDE extension, which bundles it for Windows x64. The JetBrains plugin copies it to %LOCALAPPDATA%\vgscript\bin when it opens a project, and the VS Code extension when it opens a .vgs file. The import tool runs that copy, or else a vgs.exe on your PATH, as vgs fix-all . --scenes-file <name> from the folder of your scenes file, so the file can have any name. Its output goes to the Output Log. The editor reads PATH when it starts, so restart it after changing PATH.

Without vgs, Extract Data logs a warning and imports the .vgs.meta files as they are: a changed script imports its old sidecar, and a new script with no sidecar is skipped.

To use vgs yourself, from a terminal, a build script or an AI agent, add that folder to your PATH; the JetBrains plugin offers to do it. Then, from the folder with your Scenes.txt:

vgs fix-all .              number entries and update every .vgs.meta
vgs check-all .            find mistakes in every scene
vgs travel .               follow every jump and report dead ends and loops
vgs analyze S1_Room.vgs    what the editor underlines in one script
vgs --help                 every command

5. Choose what the game plays

Pick one of the two:

  • Per level. Drop a VgScript Scene Player into the level from the Place Actors panel. Set Scenes Data and Registry, add an element to Start Actions and pick S1-A1 from its list, which shows the entries of Scenes Data. With Auto Start on (the default), it starts those entries one frame into Play, after the rest of the level's Begin Play, so your UI exists when the first line arrives. Reset Facts On Start clears the facts first. It overrides the project settings for that level.
  • Project-wide. In Project Settings → VgScript → Actions Subsystem, set Actions Data to the scenes asset and Action Named Classes Data to the registry. Then start the first entry from Blueprint: Get Actions System → Create Action Key with the key S1-A1, for example from your level or game mode. On Begin Play the HUD may not exist yet, so create your UI first or wait a frame.

Log Actions, in the same settings section, lists the playing and recently played entries on screen while you play.

6. Play

Press Play. If nothing happens, see Troubleshooting.

Starting and checking scenes

Every world has a VgScript actions subsystem. In Blueprint, get it with Get Actions System.

FunctionUse
CreateActionKey(Key, bExecute)Starts an entry by key, such as S1-A1 or S2-T4, and returns it. With bExecute off, it creates the entry without starting it. An entry that is already playing is not started twice: this logs an error.
CreateAndExecuteActionHash(Ref)Starts an entry by its stable hash, which survives renumbering. An ActionHashRef variable shows a picker in the Details panel, listing the entries of the scenes asset set in Project Settings → VgScript → Editor → Action Data Asset, or of the project's Actions Data when that is empty. The picker can be searched and used with the keyboard.
CreateAndExecuteActionHashes(Refs)The same for several entries at once.
CreateAction(Class, bExecute)Starts any action class with no script entry behind it.
HasPlayedSceneKeyCode(Key)True once an entry has ended, or once a decision option such as S1-D1-2 was chosen. An entry that was cancelled counts as played; one that is still playing does not yet.
HasPlayedHashAction(Hash)The same, by hash.
SetScenesData(Data, Registry)Uses other scenes, and another registry when one is given, for this world. It starts and resets nothing.
GetScenesData / GetRegistryThe scenes and registry this world is using.
RestartFrom(Actions)Cancels everything that plays, forgets what played and starts those entries, picked like Start Actions. Facts are kept.

The VgScript Scene Player has the Blueprint functions Play (starts its Start Actions that are not playing) and Restart (cancels everything, forgets what played and starts them again). With Reset Facts On Start on, both also clear the facts.

What has played is remembered in memory, per world, and is lost when the level changes. Saving it is up to your game; facts, below, live for the whole session. Changing level does not end what is playing either: actions get no End BP, On Hide Line or On Options Closed, so close your dialogue UI yourself when you leave a level.

Presenting dialogue

The plugin decides which line comes next; your classes decide what it looks like. The three dialogue base classes are abstract: you make a Blueprint (or C++) child of each.

The Blueprint events for each class: On Show Line for a talk line, On Present Options for a decision, and Handle BP Event Action for a custom action.

In Blueprint

Talk line (child of VgScriptTalkLineAction). One is created per line.

  1. Implement the event On Show Line. Its Line holds Text (already translated), Speaker, Directed To (a list: [P] Vee -> Gee, Narrator gives two names), Line Key (S1-T2-3), Hash and Is Hold.
  2. Show the subtitle, then call Finish Line when it has been read, for example after a Delay of Get Reading Duration. The talk then moves to its next line.
  3. Implement On Hide Line to remove the subtitle. It runs once per line, whether the line finished or was cancelled.

Get Reading Duration is the number of characters divided by Reading Letters Per Second, clamped between Min Reading Duration and Max Reading Duration (defaults 10, 1 and 7 seconds). Change them in Class Defaults; at 0 letters per second or less, every line takes the maximum. Calling Finish Line from a click or key press works too, and calling it twice is harmless. Get Line View and Get Line Text return the line at any time, and the Character Talker and Directed To variables hold who speaks and to whom.

Talk (child of VgScriptTalkAction). Set Talk Line Action Class to your talk line Blueprint in Class Defaults. Nothing else is needed.

Decision (child of VgScriptDecisionAction).

  1. Implement On Present Options. Each option has Index, Text, Line Key (S1-D1-2), Hash, Is Hold and Is Repeated (it was chosen on an earlier visit).
  2. Show one button per option and, when the player picks one, call Select Option with its Index. Stale or invalid indexes are ignored.
  3. Implement On Options Closed to remove the buttons.

Options are shuffled each time they are shown. To keep the script's order, write {!rand} on its own line under the options, or untick Is Randomized in Class Defaults for every decision of that class. {repeat} on its own line makes every option stay after it is chosen, even one marked {!repeat}. Get Decider returns who chooses, from the [P] above the decision, and Get Presented Options returns the options on screen, with their text in the current language.

[P] Gee -> Vee[D1][1] 'Let's have a look.' ->(T24)[2] {!repeat} 'Why did we come in here?' ->(T25){!rand}

Finally, set the registry's Talk Action Class and Decision Action Class. Input mode and mouse cursor for the choices are your game's, as with any widget.

In C++

  • Talk line: derive from UVgScriptTalkLineAction. Override Execute() (call Super first) to show the line and call TryStop() or FinishLine() when it is done; override Stop() to hide it before calling Super. Override SetCharacter() to look up the speaker and call Super to keep the name.
  • Talk: derive from UVgScriptTalkAction and set TalkLineActionClass. CloseTalkLineSubtitle() is a hook for closing presentation when the talk ends.
  • Decision: derive from UVgScriptDecisionAction and override PresentDecisions(). Call Super to build GetPresentedOptions() and answer with SelectOption(Index), or work from the decision data and answer with SelectDecision(). Release your UI in End() and call Super. HasSelectedDecision() and RecordSelectedDecision() let a game keep its own choice history.

Both paths can live together: the Blueprint events do nothing when not implemented. A missing or abstract dialogue class logs an error instead of playing.

How talks behave

  • Lines play in order. A line that starts with if(...) is skipped when its condition is false.
  • A ->(...) on a line starts when that line ends, while the talk goes on to its next line. The story usually continues from the last line's arrow.
  • Only one talk speaks at a time: a talk that starts while another is speaking waits for it, then resumes about a second later. That includes a talk started from the arrow of a line in the middle of another talk. Decisions never make a talk wait. The talk modifiers below change that.
  • Talks waiting for the same talk all resume when it ends, together, so they can overlap. Chain them with arrows when their order matters.
  • {hold} on a line sets Is Hold on its view, and on an option. What holding means is up to your presentation, for example hold-to-confirm.

Talk modifiers go on their own line under the talk's lines:

ModifierEffect
{parallel}Plays over another speaker's talk instead of waiting for it.
{priority}Interrupts a talk without {priority} that is speaking. The interrupted talk is cancelled unless it has {resume}. A {priority} talk waits for another {priority} talk.
{resume}When a priority talk interrupts this one, it waits and continues afterwards instead of being cancelled, replaying the interrupted line from its start (On Hide Line runs for it first). {continue} is the same.
{resumeTime: 0.5}Seconds a waiting talk pauses before it continues. Default 1.

The showcase starts two talks at once and waits for both:

[A5] Node{waitParallel} &(T49, T50) ->(T51)[P] Vee -> Gee[T49][1] It opened!{parallel}[P] Gee -> Vee[T50][1] No way.{parallel}

Built-in actions

These work with no setup. Action names are case-insensitive, and an [A] entry or an inline action can use any of them.

ActionWhat it does
Timer{2.5}Finishes after a number of seconds, or a size: XS 1 s, S 3 s, M 4 s, L 6 s, XL 10 s. With no number it waits 10 s. A key, as in Timer{M, T5}, is started when the timer ends.
Event{A1, name}Waits until the playing entry A1 raises name, then finishes on the next frame. onEnd instead waits for A1 to end. Options: waitAction waits for A1 to start if it is not playing yet; onTriggered: A5 starts A5 afterwards. If A1 is not playing, the event counts as passed.
Trigger{A1, value}Sends each value to the playing entry A1, then finishes. A Blueprint action receives it in Handle BP Event Action. If A1 is not playing, the trigger is cancelled and its continuation does not run.
Execute{A1}Starts A1 (or joins it if it is already playing) without waiting for it, then finishes. It takes exactly one key: any other argument cancels it with an error.
NodeDoes nothing. Useful as a hub for arrows, or with {waitParallel} to wait for several branches.
Cancel{A1, T2}Cancels those playing entries. Their continuations do not run.
CancelAllCancels every playing entry. {narrative} cancels only talks and decisions, {!narrative} spares them, {scene: S2} limits it to a scene. CancelAllActions is the same action.
CancelAllExcept{A3, scene: S2}Cancels every playing entry except the keys and scenes given. Takes the same narrative flags.
AddFact{x, y} / RemoveFact{x}Sets or clears facts.
Print{text}Writes the text to the log and on screen for 30 seconds. Several arguments are joined without spaces.
WaitForActionEnd{A1, T2}Finishes once every listed entry that is playing now has ended or was cancelled. An entry that had already played before, on an earlier loop, is not waited for.
WaitForNoNarrative{1}Finishes once no talk, decision or timer has played for the margin: seconds, or a size from XS (0 s) to XL (1 s). Default 2 s. Write the margin as a bare number or size. A key argument is started afterwards.
Hash{GUID}Legacy jump to the entries with those hashes. New scripts write ->(S2-A1) and resolve the link.

The two WaitFor… actions accept timeOut: 5, which cancels the wait after 5 seconds, and timeOutKey: A4, which then starts A4.

Argument keywords are case-sensitive: write onEnd, onTriggered, waitAction, timeOut, timeOutKey, narrative and scene: exactly so, and sizes in upper case.

Cancel, CancelAll and CancelAllExcept target entries with a key. Inline actions and talk lines belong to the entry that started them: cancelling an entry also cancels its current line and the &(...) branches it started that are still running. An inline action started by the arrow of an entry that has already ended, such as the Timer in ->(Timer{5} ->(A9)), has no key and keeps running; Restart From cancels it.

Any action accepts {waitParallel}, which delays its completion until the branches it started with &(...) have finished. The showcase uses it to make a talk and a pause both end before moving on:

[A6] Node{waitParallel} &(T83, Timer{L}) ->(A7)

On a talk line, {waitParallel} holds that line's ->(...) until the talks it started end, as in [1] Ha! {waitParallel} &(T75) ->(T77). Write it on the line itself: on its own line under the talk it does nothing.

Conditions

Conditions appear in two places: an if(...) at the start of a talk line or a decision option, and [C] condition groups.

[C9][1] if(CF{second-act}) T70[2] if(T32) T71[3] T76

A condition group runs the first entry whose condition holds; an entry without if(...) is the else. Without an else, a group where nothing holds ends without continuing. Each entry names its targets directly, and may add inline actions and arrows, as in [1] if(CF{world-ended}) AddFact{epilogue} ->(A11). Do not write ->(T8) as a whole [C] entry: the importer rejects it, and vgs fix currently mistakes such lines for decision options.

A decision option whose condition is false is hidden. Inside if(...):

  • && means and, || means or, and && binds tighter. A comma also means and.
  • if!(...) negates the whole condition.
  • Parentheses cannot be nested.
  • A bare word that isn't a key or a registered name is a fact: if(met-guide) is the same as if(CF{met-guide}).
ConditionTrue when
T5, D1-2, S2-A1That entry has ended, or that option was chosen. Write keys bare.
CF{x} / HasFact{x, y}Every listed fact is set.
CA{A1, T2}Every listed entry has played. Use it for codes of the same scene; write options and other scenes' keys bare.
CA_any{A1, A2}Any listed entry has played.
Check{A1, value}The playing C++ action A1, which implements IStringCondition, answers true for that value. Only the last value is checked.

A bare key is checked by the registry's Action Condition Class and a bare fact by its Fact Condition Class. Both are read at play time from the registry the game uses, so changing them needs no new import.

Facts

Facts are named flags for the story's state: AddFact{laptop-open} sets one, RemoveFact{laptop-open} clears it and CF{laptop-open} checks it. Names are case-insensitive. Write them bare: AddFact{chapter: 2} stores the fact 2, not chapter.

They live in a game instance subsystem, so they last for the whole session, across level loads. In Blueprint, use Get VgScript Facts:

FunctionUse
AddFact, RemoveFact, HasFactChange or read one fact from game code.
GetFacts / SetFactsAll facts, sorted, and replacing them all. Save the first with your save game; restore it with the second.
ClearFactsForgets every fact, for a new game.
OnFactsChangedFires after any change, for example to refresh a debug list.

The plugin does not save facts to disk.

Events and triggers

A scene handler is an action that stays alive during a scene and reacts to the script. Other entries talk to it with Trigger, and wait for it with Event. In the showcase, RoomHandler opens the laptop and switches its screen on:

[A1] RoomHandler &(C1)[A4] Move{Vee, Laptop} ->(AddFact{laptop-open}, Trigger{A1, screen-on}, Event{A1, screen-ready} ->(T36))

Trigger sends screen-on to A1. Event waits until A1 raises screen-ready, then T36 starts. On the Blueprint side, RoomHandler is B_ShowcaseSceneHandler, a custom action registered under that name:

  1. Handle BP Event Action receives each triggered value. A bare word such as screen-on arrives with Code Name value and Value screen-on; key: value arrives as Code Name key and Value value.
  2. When the screen is on, the handler calls Raise Script Event with screen-ready. Every Event{A1, screen-ready} waiting on it finishes.

A few details matter:

  • An event raised while no Event{...} waits for it is not stored. Raise it once the work it announces is done (after an animation or a delay), or write the Event before the Trigger in the flow.
  • Handle BP Event Action also receives the event's name, as Code Name value, when an Event{A1, name} starts waiting on the action. Ignore the names you do not handle.
  • If A1 ends without raising the name, the event waits forever. If A1 is cancelled, the event is cancelled with it and its continuation does not run.
  • A handler can also answer an event with a child action: if it calls Create Action with the event's name, Event{A1, name} waits for that child to end.

Custom actions

Any [A] entry or inline action with a name that is not built in runs your own class.

In Blueprint

  1. Create a Blueprint class whose parent is VgScriptActionBase.
  2. Implement Execute BP: it runs when the entry starts. Read the arguments with Get Action Value (speed in {speed: 2} returns 2) or Get Action Values for all of them.
  3. When the work is done, call Stop. The action finishes and its ->(...) continuation starts. An action that should stay alive, like a scene handler, simply does not call it.
  4. Implement End BP to clean up. It runs when the action finishes and when it is cancelled. Cancelling clears the action's delays and timers; finishing with Stop does not, so clear any that are still pending.
  5. Optionally, implement Handle BP Event Action for triggers and call Raise Script Event for events, as above.

The showcase's Move{Vee, Laptop} is one: B_ShowcaseMoveAction reads who and where with Get Action Values, walks that character to the spot and calls Stop when it arrives.

In Get Action Values, a bare word such as {loud} arrives with Code Name value and Value loud, so Get Action Value with value returns the first bare argument. An entry key such as {A3} arrives as Code Name SceneKeyCode and the full key S1-A3 as Value.

Every action also has the events On Action Ended and On Action Cancelled to bind to, and can run child actions: Create Action starts one under a name, Get Action finds it and End Action ends it. Children are cancelled with their parent.

In C++

#include "VgScript/Actions/Base/VgScriptActionBase.h"
#include "OpenDoorAction.generated.h"

UCLASS()
class UOpenDoorAction : public UVgScriptActionBase
{
	GENERATED_BODY()

public:
	virtual void SetData(const TArray<FActionValue>& ActionValues) override
	{
		Super::SetData(ActionValues);
		DoorName = GetActionValue(TEXT("door"), TEXT("Front"));
	}

	virtual void Execute() override
	{
		Super::Execute();
		if (HasEnded() || IsCancelled())
		{
			return;
		}

		// Start opening the door; call TryStop() when it is open.
	}

protected:
	virtual void Stop() override
	{
		// Clear timers and bindings here.
		Super::Stop();
	}

private:
	FString DoorName;
};

Super::Execute() starts the &(...) branches and the Blueprint Execute BP event. Finish with TryStop() when you clean up in Stop(), or with TryEnd(); finish a named child action with EndAction(Name). Cancelling runs Stop() and never starts continuations.

More hooks for C++ games:

  • Implement IStringEvent (TriggerEvent, SetupEvent) to receive triggers or expose events from any action.
  • Override UVgScriptTimerAction::GetTimeScale to scale every timer, for example with a game speed setting.
  • Override UVgScriptWaitForNoNarrativeAction::IsNarrativeAction to decide what counts as narrative.
  • A C++ subclass of UVgScriptActionsSubsystem replaces the built-in one.

Register the name

Open the registry and add an entry to Actions Class Map: the name used in scripts (OpenDoor, case-insensitive) and your class. A mapped name overrides a built-in of the same name; Get Built In Action Names and Get Built In Condition Names list the built-ins. Classes are resolved when you import, so after changing the map, run Extract Data again and save.

Custom conditions

Conditions are C++: derive from UConditionActionBase, override SetData() (call Super) and Check(), and add the class to the registry's Condition Class Map under the name scripts use, as in if(HasKey{red}). Mapping CF replaces the built-in fact check; a subclass of UCheckFactBase only has to implement HasFact(). The registry's Action Condition Class (default UCheckAction) decides what a bare key in if(...) checks. Re-import after changing the Condition Class Map.

Localization and translation

The CSV round trip: export one CSV per culture, translators fill the Translation column, import them back, and switch language at runtime with Set Culture.

Subtitle tables

Every talk line and decision option becomes a row in a subtitle table: one DataTable per culture at <Data Tables Path>/<Subtitle Data Table Path>/<culture>, for example /Game/Narrative/Data/DataTables/Text/es. A row is named by a hash of the line's text and holds Key (S1-T2-1), Hash, Source, AutoTranslation, Translation, Speaker and Comment.

  • Lines with the same text share one row, and one translation.
  • A quoted option, such as 'Who are you?' ->(T4), shares the row of the first line of the talk it leads to.
  • Changing a line's text gives it a new row, so that line needs translating again.
  • Update Subtitles removes the rows of lines that are not in the scenes asset, with their translations. Importing the wrong scenes asset, or a Scenes.txt missing a scene, can empty a table. Nothing is lost until you save, and an exported CSV is a backup.

The CSV round trip

  1. Extract Data, then Update Subtitles, then save.
  2. Export CSVs writes one file per culture into the localization folder, such as Localization/es.csv next to Scenes.txt, and offers to open the folder. A language with no subtitle rows yet gets its table first, so a new language needs no extra step. Export replaces the files already there, except one with translations its table doesn't have yet: it leaves that file alone and tells you. Click Import CSVs to keep them, or delete the file to replace it.
  3. Translators fill the Translation column, or AutoTranslation for machine output. They must not change the first column (the row name) or Source.
  4. Import CSVs copies only the Translation and AutoTranslation columns back, matching rows by name. It never adds or removes rows, and it saves the tables it changed.

The files are comma-separated UTF-8 with the header ---,Key,Hash,Source,AutoTranslation,Translation,Speaker,Comment. When importing:

  • An emptied Translation cell clears that translation; a missing column keeps the table's values.
  • Only <culture>.csv files of the configured cultures are read, into tables that already exist.
  • A file saved with semicolons, as some spreadsheet apps do, is kept by Export and rejected by Import: save it again as comma-separated CSV (UTF-8).

The source language needs no translation: a row without one shows its Source text.

At runtime

The text in On Show Line and On Present Options is already resolved. For each line the plugin uses the Translation, then the AutoTranslation, for the exact culture and then its parents (es-ES, then es; zh-CN, then zh-Hans, then zh), then the Source, then the script text.

In Blueprint, use Get VgScript Localization:

FunctionUse
SetCulture("es")Language for script text. An empty name follows the engine's language again, which is the default.
GetCultureThe language in use.
GetAvailableCultures(Data)The cultures that have a table, for a language menu. Pass the scenes asset, from Get Actions System → Get Scenes Data.
GetLineText(Data, Hash, Fallback)The text of a line or option by its hash, for example to refresh a subtitle on screen.
OnCultureChangedFires when SetCulture changes the language.
RegisterTable(Data, Culture, Table)Supplies a table directly instead of loading it from the tables folder. A null table removes it again.

A line keeps the text it was shown with. To switch what is on screen at once, listen to On Culture Changed, then call Get Line Text with the line's Hash for a subtitle, and Get Presented Options on the decision for its options.

Unreal's default UI font draws Chinese through its built-in fallback, so the showcase needs no extra font. If your UI uses its own font, check that it, or a fallback you add to it, covers your languages.

Voice-over

Record one .wav per talk line, named after the line's key, in a folder per culture: Voice/es/S1-T2-1.wav, with Voice/ next to Scenes.txt. Only files directly inside each culture's folder are read. The Audio settings of the import tool say where everything goes:

SettingWhat to enter
Voice Files FolderFolder of the recordings: absolute, or relative to the Scenes.txt folder. Default: Voice.
Sounds Asset PathContent folder for the imported sounds, one subfolder per culture. Default: Narrative/Sounds/Dialogue.
Audio Data Table PathSubfolder of the tables path that holds one audio table per language. Default: Audio.

Then use the buttons at the end of those rows:

ButtonWhat it does
Update Audio TablesFills one audio table per culture with the lines to record: Key, Speaker and the Spoken Text in that culture (the translation, or the source text). Talk lines only; decision options are not spoken. Run Update Subtitles first, since the text comes from the subtitle tables.
Import Voice FilesImports new and changed recordings as sounds named after each line's text hash and links them in the audio tables, which it updates first. Lines with the same text share one recording, and a file that matches no line is reported in the log.
Find Unused AudioSelects, in the Content Browser, the imported sounds no line uses anymore.

Save the new sounds and the audio tables afterwards: they are marked as changed, not saved. Audio table rows are named by text hash, like the subtitle tables. The plugin does not play them: in your talk line Blueprint, look up the line's Hash in the audio table of the current culture and play its Sound. Reference the audio tables from your own assets, or add their folder to Additional Asset Directories to Cook, so packaged builds include them.

Script → engine reference

In the scriptIn Unreal
[S1] The RoomScene key S1 in the scenes asset, named The Room. Keys read S1-A1, S1-T2, and S1-T2-3 for a line.
[A1] Name{...}An action entry. Name is resolved to a class at import: the registry first, then the built-ins. The {...} arguments are its action values.
[T1]A talk of the registry's Talk Action Class; each line creates its Talk Line Action Class.
[D1]A decision of the registry's Decision Action Class; each option has the key S1-D1-n.
[C1]A condition group, played by the plugin's own class.
[P] Vee -> GeeSpeaker and Directed To of the following talk lines; the decider (Get Decider) of the following decisions. [P] Vee -> Gee, Narrator directs the lines to both.
if(...) / if!(...)Skips a talk line or hides an option when false; picks the entry of a condition group.
{!repeat} on an optionHidden once chosen.
{!rand}, {repeat} under a decision's optionsKeep the script's order; let every option repeat.
{hold} on a line or optionIs Hold on its view.
{parallel}, {priority}, {resume} under a talkHow the talk shares the stage with other talks, see How talks behave.
Line textA subtitle row named by the text's hash, in every culture's table.

Flow, as the runtime plays it:

  • ->(X) starts X after the line or action completes. &(X) starts X alongside it, without waiting.
  • A comma starts siblings in written order without waiting for each: ->(AddFact{asked-why}, T26) stores the fact and starts T26. Use an arrow when the next step depends on completion; AddFact{x} T5 is invalid.
  • Flows nest: ->(Timer{2} ->(A4)) waits two seconds, then starts A4.
  • An arrow after an entry that has its own continuation adds targets to it rather than replacing them: in ->(T5 ->(A9)), A9 starts when T5 finishes, along with whatever T5 already leads to.
  • An arrow to an entry that is already playing does nothing: the entry is not started a second time.
  • Cancelling never starts continuations, and it also cancels the &(...) branches that are still running.

The showcase

The showcase plays the example story in Scripts/vgscript-example. In S1_Room, Vee and Gee arrive early in a room with a laptop, guess its password and find their own script on it. S2_End walks them out and starts the room again with the fact second-act, which unlocks new lines and options the second time. Between them they use every entry type and flow feature at least once.

AssetWhat it is
Levels/L_VgScriptShowcaseThe level. Its VgScript Scene Player starts S1-A1 one frame into Play.
EUW_VgScriptShowcaseThe import tool set up for the example scripts, a child of EUW_VgScript. Right-click it and choose Run Editor Utility Widget.
Data/DA_ShowcaseScenes, Data/DA_ShowcaseRegistryThe imported scenes and the registry (talk and decision classes, RoomHandler and Move).
Data/Text/en, es, ca, zh-HansThe subtitle tables. Their CSVs are in the vgscript-example repository, under Localization/; the Fab package does not include them.
Blueprints/B_ShowcaseSceneHandlerThe scene handler, registered as RoomHandler: a custom Blueprint action. It opens the laptop and turns the screen on for Trigger{A1, screen-on}, raises screen-ready a second later, and closes it for Trigger{A1, screen-off}.
Blueprints/B_ShowcaseMoveActionRegistered as Move: Move{Vee, Laptop} walks a character to the target point tagged with that spot (Left, Right, Laptop, Door) and finishes when it arrives.
Blueprints/B_ShowcaseCharacterVee and Gee, found by name.
Blueprints/B_ShowcaseTalkAction, B_ShowcaseTalkLine, B_ShowcaseDecisionThe Blueprint dialogue classes. The line and decision forward OnShowLine and OnPresentOptions to the HUD.
UI/WBP_ShowcaseHudSubtitles, choices, the language switch (EN, ES, CA, ZH), the live list of facts and Restart. It waits GetReadingDuration before calling FinishLine; click, Space or Enter skip ahead, and 1, 2 and 3 pick options.
UI/WBP_ShowcaseOptionOne choice button, with hold-to-confirm for {hold} options.
Blueprints/BFL_ShowcaseUIGetShowcaseHud: finds or creates the HUD, so the dialogue classes never depend on spawn order.
Blueprints/B_ShowcaseGameMode, B_ShowcasePlayerControllerNo pawn; the controller shows the cursor and looks through the camera tagged ShowcaseCamera.
Materials/The materials of the room, the characters, the laptop and its screen.

The showcase UI is example content: copy it into your project and change it rather than referencing it from the plugin.

Editing the showcase scripts

A plugin installed from Fab lives in the engine folder, which may be read-only, and the showcase tool writes there: Extract Data runs vgs fix-all on the plugin's scripts, Count Elements and Export CSVs write next to them, and the subtitle tables are in the plugin's content. To change the story:

  • copy the plugin into your project's Plugins folder and edit it there; or
  • copy Scripts/vgscript-example into your project, create your own scenes asset and your own tool (not EUW_VgScriptShowcase, whose Data Tables Path is the plugin's /VgScript/Showcase/Data), point it at the copy, and follow Your first project. The showcase level plays DA_ShowcaseScenes, so copy the level too, or set its Scene Player's Scenes Data to your asset.

Then edit a .vgs, and in the import tool click Extract Data and Update Subtitles and save.

Troubleshooting

A red message on screen says something "is not defined"

An action name matched no class when the scripts were imported. Check that the name is in the registry's Actions Class Map (or is built in), that the import tool has the registry in Action Named Classes Data, and run Extract Data again. The Output Log, under LogVgScript, has the details.

Nothing plays

  • No scenes data: set Actions Data in the project settings or Scenes Data on a Scene Player.
  • Wrong key: the log says Scene S1 is not in Actions Map! or that an entry is missing. Keys use the scene code, as in S1-A1.
  • Talks or decisions do not start: the log asks for a concrete Talk Action Class or Decision Action Class. Set them on the registry, and make sure the registry is assigned where the game reads it.
  • Lines do not show: the talk class needs a Talk Line Action Class.
  • The first line plays before your UI exists: start the story from a Scene Player, or a frame after Begin Play.

My script changes do not show

Unreal reads only the .vgs.meta files. Save the scripts in your editor, click Extract Data, and save the scenes asset; Extract Data does not save it for you. With Fix Scripts Before Import off, run Fix + Reconcile All (Scenes.txt) (or vgs fix-all .) first.

A scene is missing after Extract Data

Extract Data rebuilds the scenes asset from scratch, and a listed scene it can't find, or one with no .vgs.meta yet, is skipped with a warning. Check the Output Log, the names in Scenes.txt (they must match the whole file name) and that vgs is installed.

Extract Data imports nothing

The Output Log, under LogVgScript, says vgs fix-all exited with code … and then vgs fix-all failed, nothing imported, followed by each vgs: message. A scene listed in Scenes.txt that does not exist is the usual cause. Fix it, or turn off Fix Scripts Before Import to import the existing sidecars. Messages that mention ActionsDataAsset or VgsScenesFilePath mean the Actions Data Asset or Vgs Scenes File Path setting is empty.

The tool ignores new Class Defaults

Once you change a value in an open tool, it saves every value to Config/DefaultEditor.ini, and the ini wins over Class Defaults. Close the editor and delete the tool's section, such as [/VgScript/Showcase/EUW_VgScriptShowcase], to go back to them. See Where the tool keeps its values.

Text is missing, or a translation does not appear

  • No tables yet: click Update Subtitles and save. It also writes the tables folder into the scenes asset; if the game shows only script text, run it again and save the scenes asset.
  • The culture has no table: add it to Cultures or, when that is empty, to Languages to Package. Then click Update Subtitles or Export CSVs.
  • The line's text changed after translating: it has a new row. Export, translate and import again.
  • Import CSVs skipped a file: it must be named after a configured culture, comma-separated, and its table must already exist.
  • In a packaged game: the scenes asset brings every subtitle table in its tables folder into the cook, so the scenes asset itself must be cooked. A Scene Player in a cooked level is enough. If only Project Settings name it, check a packaged build, and add its folder to Additional Asset Directories to Cook if the text is missing.

The tables cannot be created

A plugin installed from Fab is in the engine folder, and its content may be read-only. Point Data Tables Path at your project, such as /Game/Narrative/Data/DataTables.

The story stops

  • At a decision: when every option is hidden by its if(...) or by {!repeat}, the decision ends without continuing. Keep one option that is always available.
  • At a condition group: when no entry's condition holds and there is no else.
  • At an Event: it was raised before anyone waited for it, the entry it waits on ended without raising it, or that entry was cancelled. See Events and triggers.
  • After a Trigger: its target was not playing, so the trigger was cancelled.
  • At an arrow: its target was already playing, so nothing new started.
  • Run Travel / Find Loose Ends in your IDE to find dead ends and missing fallbacks before playing.

A condition is ignored

Keep if(...) free of nested parentheses, and write keys of options and other scenes bare (D1-2, S2-A1) rather than inside CA{...}. A bare fact name that matches a registered action or condition name is resolved as that class, not as a fact: use CF{name} to be explicit.

Limitations

  • Beta. APIs and data formats may change between releases. Built for Unreal Engine 5.8.
  • Windows only. The plugin's modules are Win64-only, and vgs is bundled with the IDE extension for Windows x64 only.
  • {!rand} on a [D] header line, such as [D1]{!rand}, is not read. Write it on its own line under the options.
  • Event{A1, onCancelled} never continues in 0.3.2: the event is cancelled together with A1. Use onEnd, or have the action raise an event before it is cancelled.
  • WaitForNoNarrative reads any number in its braces as the margin, including a timeOut: value, and only checks again when an entry with a key ends.
  • Voice-over is imported and linked to lines, but playing it is your talk line's job.
  • Saving is the game's job: facts and played entries live in memory only.
  • Level changes do not end what is playing, so dialogue UI is not closed for you.
  • vgs is not part of the plugin; it comes with the IDE extension.
  • Custom conditions, and Check{...}, need C++.

For the language, the editor actions and the vgs commands, see the VgScript documentation.