How the Battle System Is Implemented in Undertale-Changer-Template: A Complete Technical Guide

The Undertale-Changer-Template battle system uses a data-driven ScriptableObject architecture that separates configuration, turn logic, and player heart mechanics, allowing developers to define enemy attacks via the IBattleConfig interface while the TurnController manages bullet spawning through object pools and the BattlePlayerController handles seven distinct heart colors with unique movement rules.

The Undertale-Changer-Template repository by arch-aik provides a modular Unity framework for creating Undertale-style bullet-hell encounters. Understanding how the battle system is implemented in Undertale-Changer-Template requires examining its layered architecture, from the configuration-driven IBattleConfig interface to the color-specific heart mechanics in BattlePlayerController.

Configuration Layer: IBattleConfig and Data-Driven Battles

The battle system is built around a data-driven, scriptable-object architecture that separates configuration from logic. The entry point for any battle is the IBattleConfig interface.

The IBattleConfig Interface

Located in Assets/Scripts/UCT/Battle/BattleConfigs/IBattleConfig.cs, this contract defines everything needed to bootstrap a battle:

public interface IBattleConfig {
    GameObject[] enemies { get; }
    Vector3?[] enemiesStartPosition { get; }
    GameObject backGroundModel { get; }
    Material skyBox { get; }
    VolumeProfile volumeProfile { get; }
    AudioClip bgmClip { get; }
    float volume { get; }
    float pitch { get; }
    IEnumerator<float> Turn(int turnNumber, ObjectPool bulletPool);
}

A config supplies enemy prefabs, visual assets (background, skybox, post-processing), audio settings, and the turn script—a coroutine that receives the current turn index and a bullet pool.

Example: DemoBattle Implementation

The DemoBattle class in Assets/Scripts/UCT/Battle/BattleConfigs/DemoBattle.cs demonstrates a minimal implementation that spawns two NPC enemies, loads a Vaporwave background, plays a BGM, and defines demo turns that animate the battle box and fire a "CupCake" bullet pattern.

Global State Management with BattleControl

BattleControl is a ScriptableObject (located in Assets/Scripts/UCT/Control/BattleControl.cs) that stores shared battle resources:

  • Color palettes for bullets and hearts (bulletColorList, playerColorList, playerMissColorList).
  • Turn-related data (actSave, mercySave, enemiesNameSave, turnTextSave, turnDialogAsset).
  • A reference to the active IBattleConfig (BattleConfig).

These lists are loaded at battle start from language packs, allowing designers to localize UI text without recompiling.

Turn Engine: TurnController and Object Pooling

The TurnController in Assets/Scripts/UCT/Battle/TurnController.cs orchestrates the alternating enemy-player loop.

Enemy Turn Execution

The turn flow follows this sequence:

  1. EnterEnemyTurn sets isMyTurn = false and runs _TurnExecute.
  2. _TurnExecute first processes any overlap turns (shared turn numbers for multi-enemy encounters).
  3. It then awaits the battle config's Turn coroutine, passing a bulletPool for spawning.
  4. After the coroutine finishes, the turn counter increments, and SelectUIController.EnterPlayerTurn() enables player input.

Bullet Pool Management

Object pools for bullets, boards (orange-heart platforms), and yellow-bullet effects are created once at start via ObjectPool instances, reducing runtime allocation and GC pressure.

Player Heart Mechanics: BattlePlayerController

The player heart is implemented in Assets/Scripts/UCT/Battle/BattlePlayerController.cs as a MonoBehaviour that supports seven distinct colors, each with unique movement and interaction rules.

The Seven Heart Colors

Color Behaviour
Red Standard movement, basic hit detection.
Orange Continuous movement while a key is held (PlayerContinuouslyMove).
Yellow Fires a yellow bullet when Z is pressed (YellowTimerMax).
Green Shows a "ghost" arrow for targeting (implemented in Update).
Cyan / Blue Gravity-based movement with board interaction; blue can jump.
Purple Moves along a line selected by the UI (PlayerMoveWithLine).

Color-Specific Movement Logic

Core methods include:

  • UpdatePlayer() – dispatches to the appropriate colour-specific handler.
  • FixedUpdatePlayer() – resolves physics, reads input, and moves the transform.
  • PlayerColorMoving() – a switch statement calling PlayerCommonMove, PlayerContinuouslyMove, PlayerWithGravity, etc.
  • ChangePlayerColor() – animates colour/shape changes and updates playerColor.

All heart logic is accessed through MainControl.Instance.battlePlayerController, instantiated during MainControl.InitializationBattle.

Bullet System: BulletController and Collision

Bullets are pooled objects managed by Assets/Scripts/UCT/Battle/BulletController.cs. Each bullet:

  • Holds a reference to its BulletControl data (sprite, collider sizes, colour).
  • Updates its collider size each frame unless followMode is NoFollow.
  • Detects collisions with the player heart using OnTriggerStay2D.
  • Calls HitPlayerInBattle when appropriate, applying damage based on the current heart colour (e.g., orange bullets only hit a moving heart).

Bullet colours are mapped to visual palettes from BattleControl.bulletColorList.

Scene Bootstrap: MainControl.InitializationBattle

When a scene's state is Battle, MainControl.InitializationBattle() in Assets/Scripts/UCT/Core/MainControl.cs executes:

  1. Loads the battle config (fallback to DemoBattle if none assigned).
  2. Reads language packs for turn text, act/mercy options, and enemy names.
  3. Instantiates references to battlePlayerController, selectUIController, and battle-specific cameras.
  4. Starts the BGM via AudioController.

The singleton pattern (MainControl.Instance) provides global access for all battle components.

Key Files Reference

Role File Link
Battle state (colors, turn data) Assets/Scripts/UCT/Control/BattleControl.cs BattleControl.cs
Configuration interface Assets/Scripts/UCT/Battle/BattleConfigs/IBattleConfig.cs IBattleConfig.cs
Example config (demo) Assets/Scripts/UCT/Battle/BattleConfigs/DemoBattle.cs DemoBattle.cs
Turn orchestration & object pools Assets/Scripts/UCT/Battle/TurnController.cs TurnController.cs
Player heart logic Assets/Scripts/UCT/Battle/BattlePlayerController.cs BattlePlayerController.cs
Bullet behaviour & collision Assets/Scripts/UCT/Battle/BulletController.cs BulletController.cs
Scene bootstrap & audio setup Assets/Scripts/UCT/Core/MainControl.cs MainControl.cs

Summary

  • The Undertale-Changer-Template battle system uses a data-driven architecture centered on the IBattleConfig interface, allowing developers to define enemies, visuals, and turn logic via ScriptableObjects.
  • Turn management is handled by TurnController, which executes coroutine-based turn scripts from the config, utilizing object pools for efficient bullet spawning.
  • Player mechanics are implemented in BattlePlayerController, supporting seven distinct heart colors (Red, Orange, Yellow, Green, Cyan, Blue, Purple) with unique physics and input rules.
  • Collision and damage are processed by BulletController, which detects hits via OnTriggerStay2D and applies color-specific damage rules (e.g., orange bullets only damage moving hearts).
  • Scene initialization occurs through MainControl.InitializationBattle, which loads configs, language packs, and instantiates all battle components via a singleton pattern.

Frequently Asked Questions

How do I create a custom battle configuration in Undertale-Changer-Template?

Create a new class implementing the IBattleConfig interface located in Assets/Scripts/UCT/Battle/BattleConfigs/IBattleConfig.cs. Define properties for enemies, background assets, and audio, then implement the Turn coroutine to script bullet patterns using the provided ObjectPool. Assign your config to BattleControl.BattleConfig via the Unity Inspector or at runtime.

What are the seven heart colors and how do they behave?

The BattlePlayerController supports seven colors defined in BattleControl.PlayerColor: Red (standard movement), Orange (continuous movement while key held), Yellow (shoots bullets when Z is pressed), Green (targeting arrow mode), Cyan (gravity-affected, stops on platforms), Blue (gravity with jump ability), and Purple (restricted to horizontal lines). Each color dispatches to specific methods like PlayerContinuouslyMove or PlayerWithGravity.

How does bullet pooling improve performance?

The TurnController creates ObjectPool instances for bullets, boards, and yellow-bullet effects at battle start. Instead of instantiating and destroying bullets during gameplay, BulletController objects are retrieved via bulletPool.GetFromPool<BulletController>() and returned to the pool when no longer needed. This eliminates runtime garbage collection spikes during intense bullet-hell patterns.

Can I implement multi-enemy encounters with overlapping turns?

Yes. The TurnController._TurnExecute method supports overlap turns for multi-enemy configurations. When processing a turn number, the controller first checks for and executes any shared turns across multiple enemies before running the primary turn coroutine from IBattleConfig.Turn. This allows synchronized attack patterns where multiple enemies fire bullets simultaneously while maintaining separate health pools and positions defined in the config's enemies and enemiesStartPosition arrays.

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 →