How to Implement Custom Dialogue Using Ink in Undertale-Changer-Template
You can implement custom dialogue in Undertale-Changer-Template by authoring stories in Ink, placing them in language pack folders, and triggering them with the <loadInk=FileName> tag, which the TypeWritterTagProcessor parses to load stories via InkService and manage branching through TypeWritterSelectController.
Undertale-Changer-Template is an open-source Unity framework that embeds the Ink narrative scripting language to handle complex dialogue trees. By leveraging the Ink runtime, you can implement custom dialogue using Ink in Undertale-Changer-Template without writing additional C# logic for every conversation. The integration automatically handles compilation, loading, and choice presentation through a tag-based system.
Understanding the Ink Integration Architecture
The template’s Ink integration consists of three core components that handle compilation, tag-based loading, and choice management.
InkService Compilation and Loading
The InkService class located in Assets/Scripts/UCT/Service/InkService.cs (lines 19-45) handles the heavy lifting of Ink file processing. When you request a story, InkService checks whether a compiled JSON version exists or if the source .ink file is newer. If compilation is required, it uses Ink’s Compiler to generate the JSON runtime format. The service provides two primary methods: ReadInkJsonFileFromLocalPath for user-added language packs stored in the local file system, and ReadInkJsonFileFromResources for built-in packs stored in Unity’s Resources folder.
TypeWritterTagProcessor Tag Parsing
The rich-text tag system in Assets/Scripts/UCT/Core/TypeWritterTagProcessor.cs (lines 587-605) enables runtime loading of Ink stories through the custom <loadInk=FileName> tag. When the type-writer encounters this tag, the processor extracts the filename, determines whether to load from the Resources folder (for built-in language packs) or a local path (for user-added packs), and calls InkService to retrieve the compiled story. The processor then attaches the story to the TypeWritterSelectController and immediately appends the first block of dialogue to the current text stream.
TypeWritterSelectController Choice Management
Once loaded, the Ink story is managed by TypeWritterSelectController in Assets/Scripts/UCT/Core/TypeWritterSelectController.cs (lines 10-33). This controller wraps the Ink Story object, exposes currentChoices, and provides GetStoryDialogue() to advance text until choice points. When the player makes a selection, the controller calls Story.ChooseChoiceIndex(index) to advance the narrative, and the type-writer continues rendering the next dialogue block.
Step-by-Step Implementation Guide
Follow these steps to implement custom dialogue using Ink in your project:
-
Create an Ink file. Write your dialogue narrative in a
.inkfile (e.g.,MyScene.ink). Place it underAssets/LanguagePacks/<LangId>/Ink/for built-in language packs, or in a custom folder for user-added packs. -
Compile the story (optional). At runtime,
InkService.ReadInkJsonFileFromLocalPathautomatically compiles the.inkfile to JSON if a newer version is missing. Alternatively, pre-compile using the Ink CLI and ship the.jsondirectly to skip runtime compilation. -
Insert the load tag. Inside any type-writer string, add the
<loadInk=FileName>tag (without extension). For example:"The forest is quiet... <loadInk=MyScene>". The processor strips the tag and injects the Ink story content. -
Run the scene. When the type-writer reaches the tag,
TypeWritterTagProcessorcallsInkServiceto load the story (from Resources or local path depending onlanguagePackId) and attaches it totypeWritter.SelectController.SetStory(story). -
Handle choices. When the story reaches a choice point,
TypeWritterqueriesSelectController.Story.currentChoicesand renders the UI. Player selection triggersSelectController.Story.ChooseChoiceIndex(index), andGetStoryDialogue()retrieves the next text block. -
Test in Unity. Play the scene in the editor. Verify the Ink block appears, choices display correctly, and the story advances on selection. Check the console for Ink compile warnings forwarded via
Debug.LogWarning.
Code Implementation Examples
Loading Ink Files via the Tag Handler
The following excerpt from TypeWritterTagProcessor.cs demonstrates how the <loadInk=...> tag is processed internally:
// 1️⃣ Load an Ink file at runtime (handled by the <loadInk=…> tag)
private static int LoadInkHandler(object[] args)
{
// args[2] is the raw tag text, e.g. "<loadInk=MyScene>"
string fileName = ((string)args[2])[9..^1]; // strip "<loadInk=" and trailing ">"
string pathPrefix;
// Language pack detection – see TypeWritterTagProcessor for full logic
if (MainControl.Instance.languagePackId < MainControl.LanguagePackageInternalNumber)
{
// Built‑in pack: Resources folder
pathPrefix = $"TextAssets/LanguagePacks/{DataHandlerService.GetLanguageInsideId(languagePackId)}/Ink/";
story = InkService.ReadInkJsonFileFromResources(pathPrefix + fileName);
}
else
{
// User‑added pack: local file system
pathPrefix = $@"{Directory.GetDirectories(Application.dataPath + "/LanguagePacks")
[languagePackId - MainControl.LanguagePackageInternalNumber]}\Ink\";
story = InkService.ReadInkJsonFileFromLocalPath(pathPrefix + fileName);
}
// Attach story to the current type‑writer
var typeWritter = (TypeWritter)args[0];
typeWritter.SelectController.SetStory(story);
// Pull the first block of dialogue and append it to the running text
string dialogue = typeWritter.SelectController.GetStoryDialogue();
typeWritter.originString += dialogue;
return (int)args[3];
}
Sample Ink Dialogue Script
Create your narrative logic in a dedicated .ink file. This example demonstrates basic branching:
// 2️⃣ Simple Ink file (MyScene.ink)
=== start ===
You meet a mysterious figure.
* [Greet them] -> greet
* [Ignore them] -> ignore
=== greet ===
"Hello, traveler!" -> END
=== ignore ===
You walk away, hearing their whisper fade. -> END
Triggering Dialogue in Unity
Invoke the Ink story by including the load tag in any string passed to the type-writer:
// 3️⃣ Using the tag in a Unity UI string
string cutsceneText = "The forest is quiet... <loadInk=MyScene>";
typeWritter.StartTyping(cutsceneText);
Key Source Files and Their Roles
| File | Purpose |
|---|---|
Assets/Scripts/UCT/Service/InkService.cs |
Compiles .ink files to JSON at runtime (if needed) and returns Ink.Runtime.Story objects via ReadInkJsonFileFromLocalPath or ReadInkJsonFileFromResources. |
Assets/Scripts/UCT/Core/TypeWritterTagProcessor.cs |
Parses the <loadInk=FileName> tag (lines 587-605), determines whether to load from Resources or local paths, and injects the story content into the type-writer stream. |
Assets/Scripts/UCT/Core/TypeWritterSelectController.cs |
Wraps the Ink Story object, exposes currentChoices, and provides GetStoryDialogue() to retrieve text blocks until choice points are reached. |
Assets/LanguagePacks/<LangId>/Ink/ |
Default location for packaged Ink files intended for built-in language packs. |
Assets/Resources/TextAssets/LanguagePacks/<LangId>/Ink/ |
Resources-based path for built-in packs accessed via ReadInkJsonFileFromResources. |
Summary
- Ink Integration: The template embeds the open-source Ink runtime to handle branching dialogue through three core components:
InkServicefor compilation,TypeWritterTagProcessorfor tag-based loading, andTypeWritterSelectControllerfor choice management. - Tag-Based Loading: Use the
<loadInk=FileName>tag in any type-writer string to dynamically load Ink stories from either built-in Resources folders or user-added local language packs. - Runtime Compilation:
InkServiceautomatically compiles.inkfiles to JSON at runtime if no up-to-date compiled version exists, or you can pre-compile with the Ink CLI for faster loading. - Choice Handling: The
TypeWritterSelectControllerexposes Ink choices throughcurrentChoicesand advances the narrative viaChooseChoiceIndex(), seamlessly integrating branching paths into the Undertale-style type-writer UI.
Frequently Asked Questions
Where do I place custom Ink files in the project structure?
Place built-in Ink files under Assets/LanguagePacks/<LangId>/Ink/ or Assets/Resources/TextAssets/LanguagePacks/<LangId>/Ink/ for packaged language packs. For user-added packs that load at runtime, place them in a custom folder under Application.dataPath + "/LanguagePacks" with an Ink/ subdirectory. The TypeWritterTagProcessor automatically detects whether to use ReadInkJsonFileFromResources (built-in) or ReadInkJsonFileFromLocalPath (user-added) based on the current languagePackId.
How does the template handle Ink compilation at runtime?
The InkService class in Assets/Scripts/UCT/Service/InkService.cs handles compilation automatically. When you call ReadInkJsonFileFromLocalPath, the service checks if a compiled JSON exists and whether the source .ink file is newer. If compilation is required, it uses Ink’s Compiler class to generate the JSON runtime format. You can also pre-compile your Ink files using the official Ink CLI and ship only the JSON files to eliminate runtime compilation overhead.
Can I use Ink choices and branching in Undertale-Changer-Template?
Yes, the template fully supports Ink choices and branching narratives. When the story reaches a choice point, the TypeWritterSelectController exposes the currentChoices collection from the underlying Ink Story object. The TypeWritter renders these choices using its built-in UI, and when the player selects an option, the controller calls Story.ChooseChoiceIndex(index) to advance the narrative. The next call to GetStoryDialogue() retrieves the subsequent text block until the next choice or the end of the story.
What is the difference between built-in and user-added language packs for Ink?
Built-in language packs reside in Unity’s Resources folders (Assets/Resources/TextAssets/LanguagePacks/) and are accessed via InkService.ReadInkJsonFileFromResources. These are packaged with the build and identified by languagePackId values less than MainControl.LanguagePackageInternalNumber. User-added packs are stored in the local file system under Application.dataPath + "/LanguagePacks" and are accessed via InkService.ReadInkJsonFileFromLocalPath. These allow players to add custom language packs at runtime without rebuilding the project, and they use higher languagePackId values offset by LanguagePackageInternalNumber.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →