Skip to content

Dialogue system

Assets/Scripts/DialogueSystem, namespace Monologue.*. A self-contained Ink-driven dialogue framework of eleven scripts.

Nothing here is in a scene

No component of this system is instantiated in either scene, no .ink story exists in the project other than the Ink plugin's own demo, and the Panel / OptionPrefab / GroupPanelPrefab prefabs do not exist in the repo and have to be authored. Treat this as an unwired library. Several concrete bugs are listed under Known bugs — read them before wiring anything.

Ink integration

The plugin is inkle's ink-Unity integration 1.1.8 at Assets/Plugins/Ink. It provides two assemblies — Ink-Libraries (runtime Ink.Runtime) and InkEditor (compiler and Ink Player window) — and auto-compiles: saving a .ink file writes a sibling .json TextAsset, which is what the runtime actually loads.

Project code touches only Ink.Runtime.Story, VariablesState and Value.

The demo stories under Assets/Plugins/Ink/Demos are the stock inkle samples. They declare none of this project's EXTERNAL functions, so pointing DialogueManager at one will fail on binding.

Architecture

flowchart TD
    Trigger[TriggerDialogue<br/>or EnterDialogueMode] -->|EnterDialogMode inkJSON| DM[DialogueManager<br/>singleton]
    DM -->|new Story| Story[Ink.Runtime.Story]
    DM -->|BindFunctions| SF[StoryFunctions<br/>static]
    DM -->|StartListening| Vars[Variables<br/>plain C# class]
    DM -->|DialogueText / DialogueOptions| Panel
    Panel --> Option[OptionPrefab rows]
    Option -->|click| DM
    SF -->|HandleTags| Panel
    SF -->|OnEmojiEvent| Emoji[EmojiTelegraph]
    SF -->|OnMoveToEvent| Movable[MovableCharacter]
    SF -->|InputText| Input[StoryInputTextFieldManager]
    Vars -->|OnGlobalsChangeEvent| Emoji
    DM -.->|OnDialogueStart/End<br/>static events| Cam[PlayerCameraDirector]

DialogueManager

Dialogue/DialogueManager.cs. Singleton via Instance, set in Awake; a duplicate destroys itself.

Serialised fields:

Field Purpose
TextAsset m_GlobalsJSON a compiled Ink story used only as a variable store
Variables GlobalVars constructed in Awake from the globals JSON
Panel _DialoguePanel the UI panel component

Static events

OnDialogueStartEvent
OnDialogueEndEvent
OnDialogueContinuedEvent
OnDialogueTryingToContinueEvent
OnChoiceEvent(List<string>)

Static, so subscribers need no reference to the manager. PlayerCameraDirector subscribes to start and end to force third person for the duration of a conversation — that is the only cross-system coupling in the project.

Flow

  1. EnterDialogMode(TextAsset inkAsset) — fires OnDialogueStartEvent, constructs new Story(inkAsset.text), calls StoryFunctions.BindFunctions, GlobalVars.StartListening, _DialoguePanel.EnterDialogueMode(), shows the panel, then ContinueStory().
  2. Update() — polls legacy Input.GetKeyDown for Space, E and F, or the left mouse button when there are no choices pending. See the warning in Input.
  3. ContinueStory()Story.Continue(), pushes currentChoices into _DialoguePanel.DialogueOptions, sets DialogueText, then StoryFunctions.HandleTags.
  4. ExitDialogMode() — unbind, stop listening, hide the panel, fire OnDialogueEndEvent.
  5. ChoiceSelected(OptionPrefab) / ChoiceSelected(int) — take a choice and continue.

ActiveDialoguePanel is a property wrapping _DialoguePanel.gameObject.activeSelf. The author's own comment calls it a pseudo-flag that creates edge cases; the _isAlreadyContinued guard around ContinueStory carries three author-flagged FIXMEs.

Triggers

TriggerDialogue — a world trigger holding a _VisualCue GameObject and an _InkJSON TextAsset.

This component cannot fire as written

It uses OnTriggerEnter2D / OnTriggerExit2D2D physics in a 3D project — and CompareTag("Player"), while the framework player object is untagged. Port it to OnTriggerEnter(Collider) plus GetComponentInParent<HelloMarioFramework.Player>(), the same detection CameraZone already uses.

EnterDialogueMode — the manual alternative. A plain EnterDialogue() method to wire to a UI Button or UnityEvent. (It carries a stray using Ink.Parsed;, a compile-time parser namespace that has no business in a runtime script.)

UI

Panel owns TMP_Text m_DialogueText, TMP_Text m_DialogueDisplayName, Image m_ProfilePicture, and prefab references to OptionPrefab and GroupPanelPrefab. The DialogueOptions setter destroys the old option rows and instantiates new ones through GroupPanelPrefab.Create. A static OnChoiceSelectedEvent re-broadcasts clicks.

OptionPrefab is one choice row: IPointerClickHandler / Enter / Exit, a hover tint, a public int index and a TMP_Text.

GroupPanelPrefab (namespace Monologue, note — not Monologue.Dialogue) is a thin wrapper over a LayoutGroup with a generic T Create<T>(T prefab).

Variables

Dialogue/Variables.cs. A plain C# class, not a MonoBehaviour. It holds a second Story built from the globals JSON and mirrors variables between it and the active story, so state survives across conversations.

  • StartListening(Story) / StopListening(Story) hook variablesState.variableChangedEvent.
  • object this[string key] indexer, plus SetGlobalVariable(string, Ink.Runtime.Object) and SetGlobalVariable(string, object).
  • Static OnGlobalsChangeEvent(key, value, previousValue), consumed by EmojiTelegraph.

StoryFunctions

StoryInput/StoryFunctions.cs, static. The bridge between Ink and the game.

Tags

HandleTags(Story) parses key: value tags on the current line:

Tag Effect
speaker sets Panel.DialogueDisplayName, fires OnSpeakerEvent
animation fires OnAnimationEvent
image shows the profile image — but hard-codes Resources.Load<Sprite>("Characters/Sample"). Assets/Resources/Characters/ does not exist, so this returns null.
format replaces <var> placeholders in the line from GlobalVars
cutscene empty stub

Localization reserves #id:

The localization plan adds an id tag carrying the string-table key for each line. HandleTags must learn to consume it, and it must be stripped before display.

EXTERNAL functions

BindFunctions(Story) binds five; UnbindFunctions mirrors the list.

Ink signature Effect
InputText(question, key, profile) StoryInputTextFieldManager.Instance.EnterInputMode(...)
Emoji(emoteName, characterTag) fans OnEmojiEvent out per character, with brace-list syntax via TagtoList
SetCamera(cameraTag) fires OnCameraSetEvent(tag, goBack: false)
MoveTo(characterTag, x, y, delay, disappear) fires OnMoveToEvent
ChangeScene(sceneName) SceneManager.LoadScene

CreateQuest and a two-argument SetCamera are commented out.

Story input

StoryInputTextFieldManager — singleton owning a TextFieldPanel and the static OnStoryInputStartEvent / OnStoryInputEndEvent that DialogueManager uses to hide and restore the dialogue panel. EnterInputMode(question, key, profileImage = "", placeholder = "Enter text..."); on submit it writes into DialogueManager.Instance.GlobalVars[key].

TextFieldPanel — two TMP_InputField variants (with and without a profile picture) and a static OnSubmitInputTextFieldEvent(key, value). Carries an author-flagged race condition.

Characters

Type Behaviour
ICharacter string CharacterTag { get; set; } — the tag Ink addresses characters by
DefaultCharacter trivial MonoBehaviour, ICharacter implementation
MovableCharacter listens to StoryFunctions.OnMoveToEvent and moves the transform via LeanTransition (transform.localPositionTransition(...)), optionally destroying itself afterwards
EmojiTelegraph plays a state on an emoji Animator. Two triggers: StoryFunctions.OnEmojiEvent (direct from Ink) and Variables.OnGlobalsChangeEvent, where a change to <tag>_love maps to HeartBroken / HeartSingle / HeartMultiple

Known bugs

Fix these before building content on top of the system.

Where Bug
TriggerDialogue 2D trigger callbacks and a tag check in a 3D project with an untagged player — the component can never fire
MovableCharacter.Awake, EmojiTelegraph.Awake if (_self.Equals(null)) dereferences a null interface field and throws NullReferenceException. Must be if (_self == null)
StoryFunctions.TagtoList calls tagValue.Remove(0), which removes everything from index 0 onward instead of stripping the leading brace, so brace-list parsing does not work
StoryFunctions image tag hard-coded Resources.Load<Sprite>("Characters/Sample") against a path that does not exist
Variables the private Globals setter reads Globals while assigning to it. Dead and incorrect; never called
DialogueManager.Update legacy Input polling in an Input System project
EnterDialogueMode stray using Ink.Parsed;

To make this usable

  1. Author the Panel, OptionPrefab and GroupPanelPrefab prefabs.
  2. Create a globals .ink for m_GlobalsJSON and a first real story.
  3. Fix the bugs above, starting with TriggerDialogue and the two Awake null checks.
  4. Replace legacy input polling with Input System actions and an action-map switch.
  5. Add #id: tags and route text through the localization facade.