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:
- EnterEnemyTurn sets
isMyTurn = falseand runs_TurnExecute. _TurnExecutefirst processes any overlap turns (shared turn numbers for multi-enemy encounters).- It then awaits the battle config's
Turncoroutine, passing abulletPoolfor spawning. - 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 callingPlayerCommonMove,PlayerContinuouslyMove,PlayerWithGravity, etc.ChangePlayerColor()– animates colour/shape changes and updatesplayerColor.
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
BulletControldata (sprite, collider sizes, colour). - Updates its collider size each frame unless
followModeisNoFollow. - Detects collisions with the player heart using
OnTriggerStay2D. - Calls
HitPlayerInBattlewhen 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:
- Loads the battle config (fallback to
DemoBattleif none assigned). - Reads language packs for turn text, act/mercy options, and enemy names.
- Instantiates references to
battlePlayerController,selectUIController, and battle-specific cameras. - 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
IBattleConfiginterface, 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 viaOnTriggerStay2Dand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →