Overview
The conversation_device plays a conversation tree you author in the Conversation Graph and assign to the device in UEFN. On its own, it can pop a dialogue bubble over an NPC and let the player click choices. The power comes from Verse: you can start a conversation on cue (after a cutscene, when a quest unlocks), listen for which choice the player picked via OnConversationEvent, and end or hide the conversation when gameplay demands it.
Reach for this device whenever you want narrative or menu-style choices to change the game — give a quest, open a door, grant a weapon, branch an objective. Each Conversation Event node you place in the graph carries an integer EventNumber; when a player selects the choice tied to that node, your Verse handler receives the agent who chose and that number, and you run the matching logic.
Key mental model:
InitiateConversation(Agent)— start the assigned conversation for one player.OnConversationEvent— firestuple(agent, int)when a choice node is selected. This is your branch dispatcher.EndEvent/CancelEvent— tell you the conversation finished normally vs. was force-ended.ShowConversation/HideConversation— toggle the UI for an agent.IsAgentInConversation,CanInitiateConversation,GetActiveConversationsCount— guard rails so you don't double-start or talk to a busy player.
API Reference
conversation_device
Used to set and trigger conversations made via the Conversation Graph. The conversation triggered is assigned to the device.
Full public surface, resolved verbatim from the live Epic digest (Fortnite.digest.verse).
conversation_device<public> := class<concrete><final>(creative_device_base, enableable):
Events (subscribe a handler to react):
| Event | Signature | Description |
|---|---|---|
OnConversationEvent |
OnConversationEvent<public>:listenable(tuple(agent, int)) |
Signaled when a choice node tied to this event is selected by an agent. Sends the agent that triggered the event, and EventNumber is the number specified in the Conversation Event node. |
EndEvent |
EndEvent<public>:listenable(agent) |
Signalled when a conversation has ended. |
CancelEvent |
CancelEvent<public>:listenable(agent) |
Signalled when a conversation has ended early by EndConversation or EndConversationForAll. |
Methods (call these to make the device act):
| Method | Signature | Description |
|---|---|---|
InitiateConversation |
InitiateConversation<public>(Agent:agent):void |
Begins the assigned conversation with the agent. The conversation will not start if the device or the agent are otherwise incapable of having a conversation. |
IsAgentInConversation |
IsAgentInConversation<public>(Agent:agent)<transacts><decides>:void |
Check if an agent is in an active conversation. |
CanInitiateConversation |
CanInitiateConversation<public>(Agent:agent)<transacts><decides>:void |
Check if an agent can initiate a conversation. |
EndConversation |
EndConversation<public>(Agent:agent):void |
Ends the assigned conversation with the agent. |
EndConversationForAll |
EndConversationForAll<public>():void |
Ends all active conversations with this device. |
ShowConversation |
ShowConversation<public>(Agent:agent):void |
Show the conversation UI for an 'agent'. This will work with Box and Custom UI. |
HideConversation |
HideConversation<public>(Agent:agent):void |
Hide the conversation UI for an 'agent'. Responses cannot be selected while the UI is hidden. This will work with Box and Custom UI. |
GetActiveConversationsCount |
GetActiveConversationsCount<public>()<transacts>:int |
Returns the count of currently-active conversations with this device. |
Walkthrough
Scenario: A village quest-giver NPC. A trigger volume in front of the NPC starts the conversation when a player walks up. The conversation graph has two choice nodes wired to Conversation Event nodes: EventNumber 1 = "Accept the quest" (we grant a weapon) and EventNumber 2 = "Decline" (we just end the talk). When the conversation finishes either way, we clean up.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
quest_giver := class(creative_device):
# The conversation_device with the quest dialogue graph assigned to it.
@editable
QuestTalk : conversation_device = conversation_device{}
# A trigger placed in front of the NPC — starts the chat when stepped on.
@editable
ApproachTrigger : trigger_device = trigger_device{}
# Hands the player a weapon when they accept the quest.
@editable
SwordGranter : item_granter_device = item_granter_device{}
OnBegin<override>()<suspends> : void =
# When a player approaches the NPC, try to start the conversation.
ApproachTrigger.TriggeredEvent.Subscribe(OnApproach)
# React to whichever choice node the player selects.
QuestTalk.OnConversationEvent.Subscribe(OnChoiceMade)
# Clean-up hooks for normal end and forced end.
QuestTalk.EndEvent.Subscribe(OnTalkEnded)
QuestTalk.CancelEvent.Subscribe(OnTalkCancelled)
OnApproach(Agent : ?agent) : void =
# The trigger hands us an optional agent — unwrap it first.
if (Player := Agent?):
# Only start if this player isn't already mid-conversation and can talk.
if:
not QuestTalk.IsAgentInConversation[Player]
QuestTalk.CanInitiateConversation[Player]
then:
QuestTalk.InitiateConversation(Player)
# OnConversationEvent hands us a tuple of (who chose, which EventNumber).
OnChoiceMade(Result : tuple(agent, int)) : void =
Player := Result(0)
EventNumber := Result(1)
if (EventNumber = 1):
# "Accept the quest" branch — grant the sword.
SwordGranter.GrantItem(Player)
else if (EventNumber = 2):
# "Decline" branch — end this player's conversation cleanly.
QuestTalk.EndConversation(Player)
OnTalkEnded(Agent : agent) : void =
# Conversation finished on its own. Log how many are still active.
Active := QuestTalk.GetActiveConversationsCount()
Print("Conversation ended. Still active: {Active}")
OnTalkCancelled(Agent : agent) : void =
Print("Conversation was force-ended for a player.")
Line by line:
- The three
@editablefields let you drag your placed devices onto this script in UEFN.QuestTalkis the conversation device; the others drive the scenario. - In
OnBeginwe subscribe handlers for the trigger and for three conversation signals. Subscriptions must happen here, in the asyncOnBegin. OnApproachreceives?agentfrom the trigger;if (Player := Agent?)unwraps the option. We then guard withIsAgentInConversation(skip if already talking) andCanInitiateConversation— both are<decides>, so they go inside anifas[Player]failable calls.InitiateConversation(Player)starts the assigned graph for that player.OnChoiceMadeis the heart of branching. We pullResult(0)(the agent) andResult(1)(the EventNumber set on the Conversation Event node) and dispatch.EventNumber = 1grants the sword;EventNumber = 2ends the talk.EndEventandCancelEventgive us tidy hooks;GetActiveConversationsCount()returns a plainintwe can log.
Common patterns
Pattern 1 — Show / Hide the UI during a scripted beat. Hide the dialogue while a short cinematic plays, then bring it back so the player can choose.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
cutscene_dialogue := class(creative_device):
@editable
Talk : conversation_device = conversation_device{}
@editable
StartButton : button_device = button_device{}
OnBegin<override>()<suspends> : void =
StartButton.InteractedWithEvent.Subscribe(OnPressed)
OnPressed(Agent : agent) : void =
# Begin the conversation, hide the UI for a beat, then reveal it.
Talk.InitiateConversation(Agent)
spawn { RevealAfterBeat(Agent) }
RevealAfterBeat(Agent : agent)<suspends> : void =
Talk.HideConversation(Agent) # no responses selectable while hidden
Sleep(3.0) # play your cinematic here
Talk.ShowConversation(Agent) # bring the choices back
Pattern 2 — A global "close all dialogue" panic button. When a round ends, force-end every open conversation. The CancelEvent from the previous example would fire for each affected player.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
round_end_cleanup := class(creative_device):
@editable
Talk : conversation_device = conversation_device{}
@editable
EndRoundButton : button_device = button_device{}
OnBegin<override>()<suspends> : void =
EndRoundButton.InteractedWithEvent.Subscribe(OnEndRound)
OnEndRound(Agent : agent) : void =
# How many conversations are open right now?
Open := Talk.GetActiveConversationsCount()
Print("Closing {Open} active conversations.")
# Force every one of them shut.
Talk.EndConversationForAll()
Pattern 3 — Guarded re-initiate. Only start a conversation if the player is allowed to and isn't already in one — useful when several triggers could fire the same NPC.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
guarded_npc := class(creative_device):
@editable
Talk : conversation_device = conversation_device{}
@editable
Proximity : trigger_device = trigger_device{}
OnBegin<override>()<suspends> : void =
Proximity.TriggeredEvent.Subscribe(OnNear)
OnNear(Agent : ?agent) : void =
if (Player := Agent?):
if (Talk.CanInitiateConversation[Player]):
# CanInitiateConversation already fails if they're busy,
# so a single guard is enough here.
Talk.InitiateConversation(Player)
Gotchas
- The graph still lives in UEFN. Verse starts/stops/reads the conversation, but the actual dialogue text, choice nodes, and Conversation Event nodes are authored in the Conversation Graph and assigned to the device. The
EventNumberyou switch on inOnConversationEventis the integer you typed into each Conversation Event node — keep your Verseif (EventNumber = …)values in sync with the graph. OnConversationEventislistenable(tuple(agent, int)), notlistenable(?agent). Its handler takes onetuple(agent, int)parameter — index it withResult(0)andResult(1). There is no option to unwrap here, unlike trigger events.- Trigger events DO hand you
?agent.TriggeredEventgives?agent, so alwaysif (Player := Agent?):before using the player. Forgetting this is the #1 "Unknown identifier / type mismatch" error. IsAgentInConversation,CanInitiateConversation, andIsReferencedare<decides>. Call them inside anif (...)orif:block as failable expressions with square brackets (Talk.IsAgentInConversation[Player]). Calling them like a normalvoidfunction won't compile.HideConversationblocks input. While hidden, the player cannot select responses. Always pair aHideConversationwith a laterShowConversation(or an end) so players aren't stuck staring at a frozen NPC.- You must declare the device as an
@editablefield. A bareconversation_device.InitiateConversation(...)fails with 'Unknown identifier'. Drag the placed device onto the field in the Details panel. GetActiveConversationsCount()returnsint, not a count per player. It's the total across the device. UseIsAgentInConversation[Player]when you need to know about one specific player.