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:

  • EnemyNames must reference existing prefab names (see the prefab files under Assets/Resources/Prefabs/Enemies/).
  • validIndicesList defines the pool of possible turn indices; typical values are new[] { 0, 1, 2 } for three-enemy battles.
  • The coroutine iterates over the indices array and delegates to each enemy's own _EnemyTurns method, 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:

  1. Save the prefab for the new enemy under Assets/Resources/Prefabs/Enemies/MyNewEnemy.prefab.
  2. Reference the exact prefab name ("MyNewEnemy") in the EnemyNames array 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 IMultiEnemiesConfig to define enemy groups, turn orchestration, and valid indices.
  • Place enemy prefabs in Assets/Resources/Prefabs/Enemies/ with names matching your config's EnemyNames array exactly.
  • TurnController automatically discovers all configurations via reflection at runtime—no registration required.
  • Individual enemies implement IEnemy with a TurnGenerator (such as WeightedRandomTurnNumber or FixedTurnNumber) and an _EnemyTurns coroutine.
  • Reference existing enemies like those in Npc1Enemy.cs or Npc2Enemy.cs when 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →