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¶
EnterDialogMode(TextAsset inkAsset)— firesOnDialogueStartEvent, constructsnew Story(inkAsset.text), callsStoryFunctions.BindFunctions,GlobalVars.StartListening,_DialoguePanel.EnterDialogueMode(), shows the panel, thenContinueStory().Update()— polls legacyInput.GetKeyDownfor Space, E and F, or the left mouse button when there are no choices pending. See the warning in Input.ContinueStory()—Story.Continue(), pushescurrentChoicesinto_DialoguePanel.DialogueOptions, setsDialogueText, thenStoryFunctions.HandleTags.ExitDialogMode()— unbind, stop listening, hide the panel, fireOnDialogueEndEvent.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 / OnTriggerExit2D — 2D 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)hookvariablesState.variableChangedEvent.object this[string key]indexer, plusSetGlobalVariable(string, Ink.Runtime.Object)andSetGlobalVariable(string, object).- Static
OnGlobalsChangeEvent(key, value, previousValue), consumed byEmojiTelegraph.
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¶
- Author the
Panel,OptionPrefabandGroupPanelPrefabprefabs. - Create a globals
.inkform_GlobalsJSONand a first real story. - Fix the bugs above, starting with
TriggerDialogueand the twoAwakenull checks. - Replace legacy input polling with Input System actions and an action-map switch.
- Add
#id:tags and route text through the localization facade.