How to Extend the Core Systems of Undertale-Changer-Template: A Complete Guide
Extend Undertale-Changer-Template by implementing custom tag handlers in TypeWritterTagProcessor.cs, creating new SettingsLayerBase subclasses for UI panels, adding utility services to the UCT.Service namespace, and implementing IBattleConfig or IState interfaces for battles and player states.
Undertale-Changer-Template (UCT) is a modular Unity framework designed for Undertale-style RPG development. The repository separates game logic, UI, services, and configuration into distinct namespaces, allowing developers to extend core systems without altering base engine code. This guide provides concrete implementation paths using actual source files from arch-aik/undertale-changer-template, covering everything from custom dialogue tags to finite state machines.
Extending the TypeWriter Tag Processor
The tag processor in UCT.Core.TypeWritterTagProcessor parses rich-text tags for the dialogue system. UCT supports three extension points: static tags (pre-processed), half-tags (inline with parameters), and full tags (complex game state triggers).
Adding Static Tags (Preprocessor)
Static tags resolve before the typewriter animation begins. They are ideal for inserting dynamic values like player statistics into dialogue text.
Locate the StaticTagHandlers dictionary around line 500 in Assets/Scripts/UCT/Core/TypeWritterTagProcessor.cs. Add an entry using MethodWrapper<string>:
["<playerHP>"] = new MethodWrapper<string>(args =>
(string)args[0] + MainControl.Instance.playerControl.hpCurrent.ToString()),
The delegate receives the accumulated text in args[0] and returns the modified string. Now <playerHP> resolves to the current health value in any Ink or TextMeshPro dialogue.
Adding Half-Tags (Dynamic Inline)
Half-tags process during typewriter execution and accept parameters. They can temporarily modify typing behavior or trigger immediate UI changes.
Define half-tags in the HalfTagHandlers dictionary around line 660. These use MethodWrapper<int> and receive the TypeWritter instance as args[0]:
["<speed="] = new MethodWrapper<int>(args =>
{
var typeWritter = (TypeWritter)args[0];
var speedParam = ((string)args[2]).TrimEnd('>');
typeWritter.speed = speedParam switch
{
"fast" => 0.05f,
"slow" => 0.2f,
_ => typeWritter.speed
};
return (int)args[3];
}),
["</speed>"] = new MethodWrapper<int>(args =>
{
var typeWritter = (TypeWritter)args[0];
typeWritter.speed = SettingsStorage.TypingSpeed switch
{
TypingSpeed.Slow => 0.2f,
TypingSpeed.Medium => 0.1f,
TypingSpeed.Fast => 0.05f,
_ => typeWritter.speed
};
return (int)args[3];
}),
Use <speed=fast> to accelerate typing and </speed> to restore defaults from SettingsStorage.
Adding Full Tags (Game State Triggers)
Full tags handle complex logic that affects game state, such as spawning NPCs or starting battles. They utilize MethodWrapper<FullTagData> and are processed by TypeWritterExecuteHalfTag.
First, create the event method in Assets/Scripts/UCT/EventSystem/EventController.cs:
public static void SpawnHiddenNpc()
{
var prefab = Resources.Load<GameObject>("Prefabs/HiddenNPC");
Object.Instantiate(prefab, new Vector3(10, 0, 5), Quaternion.identity);
}
Then register the tag in FullTagHandlers around line 640:
["<spawnHiddenNpc>"] = new MethodWrapper<FullTagData>(args =>
{
var data = (FullTagData)args[3];
EventController.SpawnHiddenNpc();
data.ProceedToDefault = false;
return data;
}),
Creating Custom Settings Layers
The settings system uses SettingsLayerBase subclasses to render UI panels. To add a new "Gameplay" settings tab without modifying existing controllers:
- Create a new class inheriting from
SettingsLayerBase:
// Assets/Scripts/UCT/Settings/GameplaySettingsLayer.cs
using UnityEngine.UIElements;
public class GameplaySettingsLayer : SettingsLayerBase
{
public override string LayerName => "Gameplay";
protected override VisualElement CreateContent()
{
var container = new VisualElement();
var autoSaveToggle = new Toggle("Enable Auto‑Save");
autoSaveToggle.value = SettingsStorage.AutoSaveEnabled;
autoSaveToggle.RegisterValueChangedCallback(evt =>
{
SettingsStorage.AutoSaveEnabled = evt.newValue;
});
container.Add(autoSaveToggle);
return container;
}
}
- Register the layer in
Assets/Scripts/UCT/Settings/SettingsController.cs:
private readonly List<SettingsLayerBase> _layers = new()
{
new HomeSettingsLayer(),
new VideoSettingsLayer(),
// ... existing layers ...
new GameplaySettingsLayer(),
};
- Add persistent storage in
Assets/Scripts/UCT/Settings/SettingsStorage.cs:
public static bool AutoSaveEnabled { get; set; } = true;
Adding Service Layer Utilities
Reusable utilities belong in the UCT.Service namespace. Create static classes in Assets/Scripts/UCT/Service/ for cross-cutting concerns like probability calculations or text manipulation.
Example implementation of ProbabilityService.cs:
using System;
public static class ProbabilityService
{
private static readonly System.Random _rng = new();
public static bool Roll(float percent) =>
percent >= 100f || (percent > 0f && _rng.NextDouble() < percent / 100.0);
}
Reference this anywhere in the project:
if (ProbabilityService.Roll(25f))
{
EventController.SpawnHiddenNpc();
}
Implementing Custom Battle Configurations
UCT discovers battle configurations automatically via reflection. Implementing IBattleConfig requires no registration boilerplate.
Create a new battle definition:
// Assets/Scripts/UCT/Battle/SecretBossConfig.cs
public class SecretBossConfig : IBattleConfig
{
public string BattleName => "Secret Boss";
public int EnemyId => 999;
public int MaxTurns => 30;
}
The EventController.GetIBattleConfig method (around line 170 in EventController.cs) discovers this class automatically. Trigger it via a full tag:
["<startSecretBattle>"] = new MethodWrapper<FullTagData>(args =>
{
var data = (FullTagData)args[3];
GameUtilityService.StartBattle(new SecretBossConfig());
return data;
}),
Extending Overworld Player States
The player controller uses a finite state machine (FSM) defined in UCT.Overworld.FiniteStateMachine. Add new behaviors by implementing IState and registering them in the player controller.
Create a crouch state:
// Assets/Scripts/UCT/Overworld/FiniteStateMachine/CrouchState.cs
using UnityEngine;
[CreateAssetMenu(menuName = "Fsm/State/Crouch")]
public class CrouchState : IState
{
public void Enter(FiniteStateMachine fsm)
{
fsm.Animator.SetBool("Crouch", true);
}
public void Execute(FiniteStateMachine fsm)
{
if (!InputService.GetKey(KeyCode.C))
fsm.ChangeState(fsm.PreviousState);
}
public void Exit(FiniteStateMachine fsm)
{
fsm.Animator.SetBool("Crouch", false);
}
}
Register in Assets/Scripts/UCT/Overworld/OverworldPlayerBehaviour.cs:
private void Awake()
{
RegisterState<IdleState>();
RegisterState<WalkState>();
RegisterState<CrouchState>();
}
Define the input key in your key bindings to expose it to the settings UI if desired.
Summary
- Tag Processor: Extend
TypeWritterTagProcessor.cswithMethodWrapperdelegates for static, half, and full tags to inject dynamic text or trigger game events. - Settings UI: Derive from
SettingsLayerBase, register inSettingsController.cs, and persist values throughSettingsStorage.cs. - Services: Add static helper classes to
Assets/Scripts/UCT/Servicefor project-wide utilities. - Battles: Implement
IBattleConfigin any class;EventControllerdiscovers it automatically via reflection. - Player States: Implement
IStateand register viaRegisterState<T>()inOverworldPlayerBehaviour.csto extend player mechanics.
Frequently Asked Questions
What is the difference between static, half, and full tags in UCT?
Static tags preprocess text before typewriter animation starts using MethodWrapper<string>. Half-tags execute during animation with MethodWrapper<int>, receiving the active TypeWritter instance to modify fields like typing speed. Full tags use MethodWrapper<FullTagData> for complex state changes like spawning objects or starting battles, and they control whether the typewriter proceeds to default behavior via the ProceedToDefault flag.
How do I add a new settings panel without modifying the core UI code?
Create a class inheriting from SettingsLayerBase in the UCT.Settings namespace, implement CreateContent() to return a VisualElement tree, and add your subclass to the _layers list in SettingsController.cs. Store persistent values as static properties in SettingsStorage.cs to ensure they save via Unity's PlayerPrefs.
Can I create custom battle encounters without changing the event system architecture?
Yes. Implement the IBattleConfig interface in a new class (e.g., SecretBossConfig). The EventController.cs automatically discovers implementations via reflection around line 170. Trigger battles using GameUtilityService.StartBattle(new YourConfig()) inside a full tag handler or any game logic.
Where should I place utility helper functions in the project structure?
Place static utility classes in Assets/Scripts/UCT/Service/ following the existing service pattern (e.g., TextProcessingService, MathService). These classes compile automatically with the project and remain accessible throughout the UCT namespace without creating tight coupling to specific game objects.
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 →