Skip to content

A Dialogue System & the Lighthouse ProOnly

You do not normally drive Logic Driver Assist by typing ld.* calls. You describe what you want to an agent and review what it built. This page gives you two prompts to copy. The first builds a dialogue system: the reusable classes and widget every conversation in your game runs on. The second authors one conversation, a branching talk with a lighthouse keeper, as data on that system.

Describe the system, not the session. An agent given a short prompt builds the smallest thing that satisfies it, and you spend the rest of the session steering. An agent given one long prompt that names every behavior the player sees, and every piece meant to be reused, builds the whole system at once. The prompts below are long for that reason.

About the screenshots

The screenshots on this page come from one run of the prompts below. The assets are not part of the plugin. Run the prompts in your own project to produce them, and expect small differences in names and layout.

The dialogue system prompt

Give the agent the authoring conventions in its session instructions, then:

Using the Logic Driver Assist operations and the engine tools, build a reusable dialogue system in a fresh folder /Game/DialogueSystem. This system will drive every conversation in the game, so build it as generic, conversation-agnostic classes. Individual conversations will be authored later as their own state machine assets whose nodes only carry data: who is speaking, what they say, and what the player can choose.

  • A conversation is a Logic Driver state machine. Each speaking beat is a state that carries its data on the node itself, editable per-state in the graph editor: the speaker's display name and the line text. Create a dialogue line state class for this.
  • Dialogue displays on screen in a UMG widget, not as printed debug text. Create a dialogue widget showing: the speaker name, the line text, a small "continue" hint, and three choice buttons, each with its own label text, hidden whenever the current beat offers no choices.
  • The player advances an ordinary line by pressing Space. A line holds on screen until the player advances; never advance on a timer. One press advances exactly one beat, even if the next beat would also accept it. A press made during a beat that does not advance on input must not carry over into the next line: a line state discards any pending press when it begins.
  • A choice beat is a hub state that presents up to three labeled choices on the widget's buttons (unused slots hidden). Clicking a button picks that choice and the conversation takes the matching branch. The choice buttons must never take keyboard focus, so a Space press cannot activate one. Create a choice hub state class whose per-node data is the choice labels, and a reusable transition class for "the player picked choice N", where N is data on the edge.
  • Create a reusable transition class for "the player advanced" for line-to-line edges.
  • A conversation can include another conversation asset as a nested sub-conversation (a state machine reference), with a reusable transition class for "the nested conversation has finished" on the edge leaving it.
  • A dialogue host actor runs conversations: it has a state machine component whose conversation asset is assignable, creates the dialogue widget on begin play, shows the mouse cursor and routes input so both the Space key and button clicks work, and starts the conversation only after the widget exists. States and transitions reach the widget and the player's recorded input through this host (the machine's context), so the same classes serve any conversation on any host without editing the classes.
  • Wire the data flow: entering a line state pushes its speaker and line to the widget and hides the choices; entering a hub state pushes its labels and shows its buttons; the player's advance and choice input is recorded on the host, and the transition classes read it and consume it when they fire.
  • Prove the system with a small smoke conversation SM_SystemSmoke in the same folder: two lines from two different speakers, then a two-choice hub where choice 1 leads to one more line and then the end, and choice 2 ends immediately. Place a host in a test level wired to run it.

Lay the graphs out cleanly, compile everything clean, save everything, and then verify in play-in-editor: the widget shows the first line, Space advances through the lines, the hub shows two buttons, and clicking each button takes its branch.

Everything the player experiences is named. Dialogue displays in a widget. A line holds until the player advances it, and one press advances one beat. A choice is a click on a labeled button. Conversations can nest. Everything meant to be reused is named as a class. What you leave unsaid, the agent decides, and it decides toward the smallest valid reading.

What the system prompt produces

The folder holds a complete, running system:

Piece Kind What it carries
BP_DialogueLineState State class SpeakerName, LineText, editable on the node. Entering the state pushes both to the widget and hides the choices.
BP_DialogueChoiceHub State class Three choice labels, editable on the node. Entering pushes the labels and shows the buttons; an empty label collapses its button.
BP_Transition_PlayerAdvanced Transition class Fires when the player has advanced; consumes the press so one press moves one beat.
BP_Transition_ChoicePicked Transition class Per-edge ChoiceIndex. Fires when the player clicked that button; consumes the choice.
BP_Transition_ConversationFinished Transition class Fires when the nested conversation on the state behind it reaches its end state.
WBP_Dialogue Widget Speaker, line, continue hint, three buttons. Clicks report back to the host.
BP_DialogueHost Actor The state machine component, the widget's owner, and the input recorder the transition classes read.
SM_SystemSmoke Conversation The system's proof: two lines, a two-choice hub, two endings, all data on the nodes.

"Editable on the node" is literal. A line state's speaker and text sit on the node body in the graph editor:

The smoke conversation's first line state up close: Speaker Name and Line Text carried as editable properties directly on the node body

In play, the widget shows that data and holds until the player advances:

The smoke conversation's first line in play in its test level: the dialogue widget showing the speaker name, the line text, and the continue hint

SM_SystemSmoke root graph: Entry to Line1 and Line2 carrying their speaker and line text on the nodes, a two-choice hub, choice 1 through Line3 to the end, choice 2 routed below the row straight to the end

stateDiagram-v2
    [*] --> Line1
    Line1 --> Line2 : PlayerAdvanced
    Line2 --> ChoiceHub : PlayerAdvanced
    ChoiceHub --> Line3 : ChoicePicked 1
    ChoiceHub --> End : ChoicePicked 2
    Line3 --> End : PlayerAdvanced
    End --> [*]

    Line1 : Line1 (Ava)
    Line2 : Line2 (Ben)
    ChoiceHub : ChoiceHub (two labels, third empty)
    Line3 : Line3 (Ava)

Three behaviors to check for in your own build:

  • Input is recorded, then consumed. The host stores a Space press or a button click as pending input. The transition classes read it in their condition and clear it when the edge fires, so one press advances one beat. A line state also clears any pending press when it begins, so a press made during a beat that ignores input cannot skip the line that follows.
  • Space never clicks a button. The choice buttons are not focusable. If they were, a Space press on a hub would pick a choice as well as advance.
  • The machine starts after the widget exists. A component that starts on begin play starts before the host actor has created its widget, so the first line would display nothing. The host starts the machine explicitly at the end of its begin-play chain. If your first line ever vanishes, check this ordering first.
For the agent: the calls behind the system

The Logic Driver work is all ld.*: ld.create_blueprint for the smoke conversation, ld.add_state with the line and hub classes and their per-node text via ld.set_node_property, ld.add_transition with the gate classes and per-edge ChoiceIndex, ld.layout_states, ld.compile. The pieces around it use the generic surfaces: blueprint.* creates the five node classes (subclasses of SMStateInstance / SMTransitionInstance) and the host actor, and wires their graphs; ui.* builds the widget tree and its show/hide functions; editor.* places the host and saves the level. The node classes reach the host with GetContext plus a cast, so nothing holds a hard reference to a level or a conversation. There is no input-event node on the generic surface, so the host polls WasInputKeyJustPressed on tick.

The generic-surface limits an agent meets on this path are listed under Known gaps on the Monolith surface.

The Lighthouse conversation prompt

With the system in place, a conversation is a much smaller ask. This is the prompt shape you reuse every time your game needs another one: point at the system, describe the conversation, write the dialogue's intent. The agent inspects the system's classes to learn their property names, so the prompt does not restate them.

A reusable dialogue system already exists at /Game/DialogueSystem (the line and hub state classes, the advance/choice/finished transition classes, the widget, and the host actor). Inspect those assets to learn their exact property names and behavior. Do not modify any of them.

Using only those system classes for dialogue behavior, author a branching conversation in /Game/DialogueSystem/Lighthouse: a talk with a lighthouse keeper.

  • The keeper greets a first-time visitor differently from a returning one: the conversation remembers how many times it has looped back to its start, and the greeting changes on later passes. Author whatever this needs (the counter, its writer, and the two greeting gates) as part of the Lighthouse folder; it is this conversation's own logic, not a system class.
  • After the greeting, a hub offers three choices: hear about the storm, talk to the sailor, or say farewell.
  • The storm choice: the keeper tells a short storm tale (two lines, advanced by the player like any line), then the conversation loops back to the start.
  • The sailor choice: a separate, reusable rumor conversation asset SM_RumorSub (two lines, sailor then keeper) plays as a nested sub-conversation, then the talk loops back to the start.
  • Farewell: the keeper says a goodbye line and the conversation ends.
  • Write real dialogue for every line: a weathered lighthouse keeper, a first-time greeting, a returning greeting, a two-line storm tale, the sailor's rumor and the keeper's reply, a farewell.
  • Set up a level with a placed dialogue host running the Lighthouse conversation, without breaking the smoke-test level.

Lay the graphs out cleanly (route any loop-back edge as a rail rather than crossing the flow), compile clean, save everything, and verify in play-in-editor by walking every branch.

What the conversation prompt produces

This is the finished root graph. The capture was taken after the Advanced Features page added its camera and animation beats. It therefore shows a StormGesture state and stacked entries that this prompt alone does not create:

SM_TheLighthouse root graph: Start branching on the visit count to the two greetings, the three-choice hub fanning out through the storm beats, the sailor sub-conversation reference and the farewell end state, with the loop-back routed as a rail below the flow

What this page's prompt returns on its own:

stateDiagram-v2
    [*] --> Start
    Start --> GreetFirst : VisitCount == 0
    Start --> GreetReturn : VisitCount > 0
    GreetFirst --> ChoiceHub : PlayerAdvanced
    GreetReturn --> ChoiceHub : PlayerAdvanced
    ChoiceHub --> StormTale1 : ChoicePicked 1
    ChoiceHub --> SailorRumorSub : ChoicePicked 2
    ChoiceHub --> Farewell : ChoicePicked 3
    StormTale1 --> StormTale2 : PlayerAdvanced
    StormTale2 --> LoopBack : PlayerAdvanced
    SailorRumorSub --> LoopBack : ConversationFinished
    LoopBack --> Start : always (increments VisitCount)
    Farewell --> [*]

    Start : Start (empty branch point)
    LoopBack : LoopBack (empty counter writer)
    SailorRumorSub : SailorRumorSub (reference to SM_RumorSub)
    Farewell : Farewell (end)

The conversation is instance data on the system. The only logic authored here belongs to this conversation. A VisitCount variable on the machine is written by an empty LoopBack state that both return paths converge on, and read by two inline gates out of the start state. Every line and label lives on a node:

Start fanning out to the two greeting states, each carrying the keeper's name and its own greeting line as editable properties on the node body

SM_RumorSub root graph: the sailor's rumor line, the keeper's reply, and an empty terminal end state after the reply

The choice hub in play: the widget showing the three authored choice buttons over the scene

Three details to check for in your own build:

  • The sub-conversation ends on an empty terminal state. The "conversation finished" gate fires when the reference reaches its end state. If the last spoken line were the end state, the parent would leave it the moment it appeared. An empty state after the last line makes the sub-conversation finish only after the player advances past the reply.
  • The loop counts itself. Both return paths converge on one empty LoopBack state whose entry increments the counter, then an unconditional edge returns to the start. One writer, no double counting.
  • Each placed host picks its conversation. The host's component template stays pointed at the smoke conversation. The Lighthouse level's placed host overrides the machine on its own component instance.
For the agent: the calls behind the conversation

All of it is the ld.* surface, because the system classes already exist: ld.create_blueprint twice, ld.add_state with the system's state classes (plus the empty Start, LoopBack, and end states), ld.add_reference for the nested rumor, ld.add_transition carrying the system's gate classes with per-edge ChoiceIndex, ld.add_sm_variable for VisitCount, ld.set_node_property for every line and label, the greeting gates authored inline with the bound-graph ops (ld.get_local_graph, ld.add_local_graph_node with EqualEqual_IntInt / Greater_IntInt, ld.connect_local_graph_pins), the counter written off LoopBack's On State Begin, ld.layout_states, ld.compile. The only non-ld steps are placing the host and saving the level (editor.*), and overriding the placed component's machine per instance, which has no operation and is done with a two-line editor python snippet.

ld.layout_states carries the loop-back edge on a rail of reroute nodes when its straight wire would cross a state, so a fresh run needs no hand-placed reroutes. Confirm with ld.get_graph_view; its overlaps array is empty when nothing collides. Engine comparison functions have K2 names (EqualEqual_IntInt, not Equal_IntInt); the error message lists the fix.

After the first run

The classic first-run failures are all behaviors: a choice nothing writes, lines that advance on timers, a machine that starts before its widget exists. Naming the behavior in the prompt prevents them. What remains for your eye:

  • Read the graph back. Dialogue states carrying visible property widgets are far taller than plain states. ld.get_graph_view reports overlapping nodes and stacked transition markers without a screenshot; a capture shows whether the graph reads well. If the agent did not check both, one prompt is enough: "check the root graph for overlaps, then capture it and fix any crossing edges".
  • Tune feel by prompt. Numbers like a blend duration or where a camera sits are taste, not correctness. Run it, then say what you saw: "ease into the framed angle over about three quarters of a second" is a complete refinement prompt.
  • Anything you left unsaid is built minimal. If a behavior is missing, the fix is usually a sentence naming it, not a redesign.