How to Create Custom Enemy Battle Configurations in the Undertale Changer Template
You create custom enemy battle configurations by implementing the IMultiEnemiesConfig interface to define enemy groups and turn logic, which TurnController automatically discovers at runtime via reflection.
The Undertale Changer Template provides a plug-in architecture for defining multi-enemy encounters without modifying core battle logic. By leveraging the IMultiEnemiesConfig interface and the reflection-based discovery system in TurnController, you can add new enemy combinations by simply creating a new class. This guide explains how to implement custom enemy battle configurations using the actual source code from the arch-aik/undertale-changer-template repository.
Understanding the Multi-Enemy Architecture
The IMultiEnemiesConfig Interface
The contract for enemy groups is defined in Assets/Scripts/UCT/Battle/MultiEnemiesConfigs/IMultiEnemiesConfig.cs. This interface requires three key members: EnemyNames (a string array mapping to prefab names), validIndicesList (an int array defining valid turn indices), and the _EnemyTurns coroutine that orchestrates enemy actions during the battle phase.
TurnController Discovery Mechanism
TurnController.cs located at Assets/Scripts/UCT/Battle/TurnController.cs handles battle initialization. It calls GetAllImplementationsOf<IMultiEnemiesConfig>() to automatically find all concrete implementations at runtime. This reflection-based approach means new configurations are registered immediately without manual registry updates or scene modifications.
Creating a Custom Enemy Group Configuration
To define a new enemy combination, create a C# class that implements IMultiEnemiesConfig. The example below shows a minimal configuration for a battle featuring NPC1 alongside a custom enemy:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UCT.Battle.MultiEnemiesConfigs;
using UCT.Battle.Enemies;
public class MyCustomConfig : IMultiEnemiesConfig
{
// Names must match prefab folder names under Assets/Resources/Prefabs/Enemies/
public string[] EnemyNames => new[] { "NPC1", "MyNewEnemy" };
// Valid indices correspond to the order of EnemyNames
public int[] validIndicesList => new[] { 0, 1 };
// Coroutine that runs each enemy's turn sequence
public IEnumerator<float> _EnemyTurns(int[] indices, ObjectPool bulletPool, ObjectPool boardPool)
{
foreach (int i in indices)
{
IEnemy enemy = TurnController.Instance.enemiesControllers[i].Enemy;
yield return Timing.WaitForOneFrame;
yield return Timing.RunCoroutine(enemy._EnemyTurns(i, bulletPool, boardPool));
}
}
}
Key implementation details:
EnemyNamesmust reference existing prefab names (see the prefab files underAssets/Resources/Prefabs/Enemies/).validIndicesListdefines the pool of possible turn indices; typical values arenew[] { 0, 1, 2 }for three-enemy battles.- The coroutine iterates over the
indicesarray and delegates to each enemy's own_EnemyTurnsmethod, preserving the existing turn-order system.
Implementing Individual Enemy Logic
For new enemy types, implement the IEnemy interface defined in Assets/Scripts/UCT/Battle/Enemies/IEnemy.cs. Below is a simple enemy that fires a projectile every turn:
using System.Collections;
using UnityEngine;
using UCT.Battle;
using UCT.Battle.Enemies;
public class MyNewEnemy : MonoBehaviour, IEnemy
{
public IEnemyTurnNumber TurnGenerator { get; set; } = new FixedTurnNumber(0);
public EnemyState state { get; set; } = EnemyState.Default;
public IEnumerator<float> _EnemyTurns(int index, ObjectPool bulletPool, ObjectPool boardPool)
{
var bullet = bulletPool.GetObject();
bullet.transform.position = transform.position;
bullet.GetComponent<Rigidbody2D>().velocity = Vector2.right * 5f;
yield return Timing.WaitForSeconds(0.5f);
}
public string[] GetActOptions() => new[] { "Talk", "Inspect" };
}
After creating the script:
- Save the prefab for the new enemy under
Assets/Resources/Prefabs/Enemies/MyNewEnemy.prefab. - Reference the exact prefab name (
"MyNewEnemy") in theEnemyNamesarray of your custom configuration class.
Runtime Selection and Testing
While the UI automatically populates available configs discovered by TurnController, you can force a specific configuration programmatically for testing purposes:
// Inside TurnController.StartBattle() or a test script
var config = new MyCustomConfig();
StartCoroutine(config._EnemyTurns(indices, bulletPool, boardPool));
This bypasses the automatic discovery system and immediately launches your custom encounter with the specified enemy group.
Summary
- Implement
IMultiEnemiesConfigto define enemy groups, turn orchestration, and valid indices. - Place enemy prefabs in
Assets/Resources/Prefabs/Enemies/with names matching your config'sEnemyNamesarray exactly. TurnControllerautomatically discovers all configurations via reflection at runtime—no registration required.- Individual enemies implement
IEnemywith aTurnGenerator(such asWeightedRandomTurnNumberorFixedTurnNumber) and an_EnemyTurnscoroutine. - Reference existing enemies like those in
Npc1Enemy.csorNpc2Enemy.cswhen designing complex multi-enemy encounters.
Frequently Asked Questions
What naming convention should I follow for enemy prefabs?
The strings in your configuration's EnemyNames array must exactly match the file or folder names under Assets/Resources/Prefabs/Enemies/. For example, if your prefab is named "NPC1.prefab", the array should contain the string "NPC1" (without the file extension).
How does the turn order system work for multiple enemies?
Each enemy implements IEnemyTurnNumber through concrete generators like WeightedRandomTurnNumber, CyclicTurnNumber, or FixedTurnNumber. Your configuration's validIndicesList defines which indices are valid for that encounter, and the generators determine the sequence in which enemies take turns within those constraints.
Do I need to register my new configuration in a central registry?
No. The TurnController automatically discovers all classes implementing IMultiEnemiesConfig using reflection when the battle initializes. Simply compiling the new class into your project makes it available for selection without touching TurnController.cs or any other manager.
Can I combine existing template enemies with my custom ones?
Yes. The EnemyNames array can reference any combination of existing enemies (such as "NPC1" or "NPC2" from Npc1AndNpc2Config.cs) alongside your custom implementations, provided all corresponding prefabs exist in the Assets/Resources/Prefabs/Enemies/ directory.
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 →