Build a 2.5D RPG Battle System in Unity
Build a turn-based RPG battle system with ScriptableObjects and state machines.
Introduction
30 Second Summary
Think about the last time you played a turn-based RPG. You line up your party, pick an attack, and watch the damage numbers fly.
In this project, you will build a 2.5D turn-based RPG battle system in Unity. Two heroes will face off against two enemies on a 3D battle stage, with action commands, damage calculations, and enemy AI all driven by code you write.
What You'll Build
A playable 2.5D turn-based RPG battle scene where a party of heroes battles enemies through action command buttons, powered by a data-driven character system and an enum-based state machine.
By the end of this project, you'll have:
- A data-driven character system using CharacterData ScriptableObjects with 4 unique character assets that can be extended without changing code.
- A BattleManager with an enum-based state machine controlling the full battle flow across player turns, enemy turns, and win/loss conditions.
- A complete battle UI with action command buttons, HP displays, and a message log built with uGUI Canvas and TextMeshPro.
- Secret Mission: A Magic system with MP that gives heroes a powerful spell attack costing mana points.
Are there any prerequisites?
This project assumes you already have Unity 6.4 installed via Unity Hub and VS Code installed on your machine. Step 1 will walk you through verifying your tools and setting up the project.
Set Up the Project and Tools
Every RPG battle system needs a solid foundation before the first line of combat code. Your turn-based battle scene will need scripts, character data, and a 3D stage, all organized in one Unity project.
In this step, you will create a new 3D project, connect VS Code as your script editor with full C# support, and set up the folder structure that keeps everything organized.
In this step, get ready to:
- Create a new Unity project with the 3D template.
- Set up VS Code as the external script editor.
- Build the project folder structure for your RPG.
Create a new Unity project
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Unity Hub and press Enter to open it.
- Click New project in the top-right corner.
- Select the 3D (Built-in Render Pipeline) template.
- In the Project name field, type TurnBasedRPG.
- Choose a save location you can find easily, such as your Desktop.
- Click Create project and wait for the Unity Editor to load.
The Unity Editor opens with a default scene called SampleScene. This is where you will build your battle stage.
What is the Built-in Render Pipeline?
The Built-in Render Pipeline is Unity's standard rendering setup. It provides everything you need for 3D scenes without the extra complexity of the Universal or High Definition render pipelines.
Unity Hub not opening?
- Make sure Unity Hub is installed. It comes bundled with your Unity installation.
- Try restarting your computer and searching for Unity Hub again.
Help me troubleshoot why Unity Hub isn't opening on my machine.
Verify VS Code integration
Unity needs an external code editor for writing C# scripts. VS Code with the Unity extension gives you IntelliSense, which auto-completes your code and catches errors as you type.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
- Search for Unity in the search bar.
- Find the extension named Unity by Microsoft and click Install.
What does this extension install?
Installing the Unity extension automatically installs C# Dev Kit as a dependency. Together they provide IntelliSense, debugging, and project support for Unity C# scripts.
Now connect VS Code to the Unity Editor so it opens automatically when you double-click a script.
- Switch back to the Unity Editor.
- Click Edit in the top menu bar.
- Select Preferences.
- In the Preferences window, click External Tools in the left sidebar.
- Set the External Script Editor dropdown to Visual Studio Code.
Now confirm the Unity-side package that connects to VS Code is up to date.
- Click Window in the top menu bar.
- Select Package Manager.
- In the Package Manager window, find the Visual Studio Editor package.
- Check the version number displayed next to it.
✔️ I see version 2.0.20 or higher
You are all set. Close the Package Manager window.
ⓧ I see an older version
- Click the Update button next to the package name to install the latest version.
- Wait for the update to complete, then close the Package Manager.
Finally, confirm that double-clicking a script opens VS Code with IntelliSense active.
- In the Unity Project window, right-click inside the Assets folder.
- Select Create, then C# Script.
- Name it TestScript.
- Double-click TestScript to open it in VS Code.
VS Code should open the file with syntax highlighting and code suggestions active. You can delete TestScript.cs after confirming.
VS Code not opening from Unity?
- Make sure you set the External Script Editor to Visual Studio Code in Edit > Preferences > External Tools.
- If VS Code opens but shows errors, confirm the Unity extension by Microsoft is installed in the Extensions panel.
- Make sure the Visual Studio Editor package in Unity's Package Manager is version 2.0.20 or higher.
Help me troubleshoot why VS Code is not opening when I double-click a script in Unity.
Create the project folder structure
A clean folder structure keeps your scripts, data assets, and scenes organized. This matters as your RPG grows with more characters, abilities, and battle scenes.
- In the Unity Project window, right-click inside the Assets folder.
- Select Create, then Folder.
- Name the folder Scripts.
- Repeat the process to create a folder named ScriptableObjects inside Assets.
- Create one more folder named Scenes inside Assets.
- Right-click inside the ScriptableObjects folder and create a subfolder named Heroes.
- Create another subfolder inside ScriptableObjects named Enemies.
Your folder structure in the Project window should now look like this:
- Assets/Scripts for your C# scripts.
- Assets/ScriptableObjects/Heroes for hero character data.
- Assets/ScriptableObjects/Enemies for enemy character data.
- Assets/Scenes for your battle scenes.
Why separate folders for Heroes and Enemies?
You will create ScriptableObjects for each character in your RPG. Keeping hero data and enemy data in separate folders makes it easy to find and manage characters as you add more to the game.
Folders not appearing?
- Make sure you are right-clicking inside the Assets folder in the Project window, not in the Hierarchy or Scene view.
- If you accidentally created a folder in the wrong place, drag it into the correct parent folder.
Help me fix my project folder structure in Unity.
Your project is set up and your tools are connected. Next up, you will create the data system that defines every character in your RPG.
Create Character Data with ScriptableObjects
Your Unity project is set up and VS Code is connected. Now it's time to define the characters who will battle it out in your RPG.
Every RPG character needs stats like health, attack, and defense. In this step, you'll use ScriptableObjects to store character data as reusable assets you can edit directly in the Unity Inspector without touching any code.
In this step, get ready to:
- Write the CharacterData ScriptableObject script.
- Create hero data assets for a Warrior and a Mage.
- Create enemy data assets for a Goblin and a Dark Knight.
Write the CharacterData ScriptableObject script
ScriptableObjects are data containers that live as assets in your project. You create one script to define the shape of the data, then create as many assets from it as you need.
What is a ScriptableObject?
A ScriptableObject is a Unity class designed to hold data outside of any specific scene or GameObject. Think of it like a character sheet in a tabletop RPG.
You fill in the stats once, and any part of the game can read from it. Want to add a new character? Just create a new asset from the same template. No code changes needed.
- In the Project window, right-click inside the Scripts folder.
- Select Create, then C# Script.
- Name the new script CharacterData.
- Double-click CharacterData.cs to open it in VS Code.
- Select all the default code and replace it with the following ScriptableObject class:
using UnityEngine;
// CreateAssetMenu adds a menu item to create instances of this data asset
[CreateAssetMenu(fileName = "NewCharacterData", menuName = "RPG/Character Data")]
public class CharacterData : ScriptableObject
{
// Character identity
public string characterName;
// Combat stats
public int maxHP;
public int attack;
public int defense;
public int speed;
// Determines if this character is controlled by enemy AI
public bool isEnemy;
// Visual tint applied to the character model at runtime
public Color characterColor = Color.white;
}
What does this code do?
- CreateAssetMenu registers a menu item in Unity's Create menu so you can create character data assets without writing code.
- menuName sets the menu path to Create > RPG > Character Data.
- The public fields (maxHP, attack, defense, etc.) define the stats every character needs for combat.
- characterColor defaults to white so characters always have a visible tint, even before you configure them.
- Save CharacterData.cs in VS Code (Cmd+S on macOS or Ctrl+S on Windows).
- Switch back to the Unity Editor and wait a moment for the script to compile.
- Check the Console window at the bottom of the editor. You should see no red error messages.
Seeing compilation errors?
- Make sure the file is named CharacterData.cs. The filename must match the class name exactly, including capitalization.
- Confirm the script is inside the Assets/Scripts folder. Scripts placed outside Assets will not compile.
- Check that you replaced all the default code, including the using statement at the top.
Still stuck? Ask for help: Why is my CharacterData script not compiling in Unity?
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete CharacterData.cs file. Compare it against yours and fix any differences.
using UnityEngine;
[CreateAssetMenu(fileName = "NewCharacterData", menuName = "RPG/Character Data")]
public class CharacterData : ScriptableObject
{
public string characterName;
public int maxHP;
public int attack;
public int defense;
public int speed;
public bool isEnemy;
public Color characterColor = Color.white;
}
Create hero ScriptableObject assets
Now that the script has compiled, Unity recognizes a new asset type you can create from the menu. Time to build your first two heroes.
- In the Project window, navigate to Assets/ScriptableObjects/Heroes.
- Right-click inside the folder.
- Select Create, then RPG, then Character Data.
- Name the new asset Warrior.
- Click on the Warrior asset to select it, then set these values in the Inspector:
Now create the second hero using the same process.
- Right-click inside the Heroes folder again.
- Select Create, then RPG, then Character Data.
- Name it Mage.
- Click on the Mage asset and set these values in the Inspector:
You should now see two assets in the Heroes folder: Warrior and Mage. Clicking either one shows its stats in the Inspector.
Why use data assets instead of hardcoded values?
With ScriptableObjects, a game designer can tweak every character's stats directly in the Inspector. No code changes, no recompilation.
This is how professional RPGs handle character data. You define the structure once in code, then create as many characters as you want from the editor.
Create enemy ScriptableObject assets
Your heroes need opponents. You'll create two enemy characters using the exact same menu and workflow.
- In the Project window, navigate to Assets/ScriptableObjects/Enemies.
- Right-click inside the folder.
- Select Create, then RPG, then Character Data.
- Name the first asset Goblin.
- Click on the Goblin asset and set these values in the Inspector:
Now create the second enemy.
- Right-click inside the Enemies folder again.
- Select Create, then RPG, then Character Data.
- Name it DarkKnight.
- Click on the DarkKnight asset and set these values in the Inspector:
You should now see Goblin and DarkKnight in the Enemies folder. Click each one to verify the stats are correct in the Inspector.
Don't see the RPG menu item?
- Make sure CharacterData.cs compiled without errors. The Create > RPG > Character Data menu item only appears after a successful compilation.
- Check the Console window for any red error messages. Fix them, save, and try again.
Need a hand? Why is the RPG menu item missing from my Unity Create menu?
Notice the pattern?
You wrote one script and created four completely different characters from it. Each asset has unique stats, but they all share the same data structure. Want to add a Healer or a Dragon later? Just create another asset from the same menu.
Your character data is ready to go. You have two heroes and two enemies, each with their own stats and colors. Next up, you'll bring these characters to life by placing them in a 2.5D battle scene.
Build the 2.5D Battle Scene
Your character data system is complete. Four ScriptableObject assets are sitting in the Project window, each loaded with unique stats.
Now it is time to bring those characters into the actual game world. In this step, you will write a runtime script that bridges your data into gameplay, place character capsules on a 3D battle arena, and set up the classic angled RPG camera perspective.
In this step, get ready to:
- Write the BattleCharacter runtime script that connects ScriptableObject data to gameplay.
- Build a battle arena with hero and enemy capsules facing each other.
- Set up the 2.5D camera angle for a classic RPG perspective.
Create character GameObjects with BattleCharacter components
Your ScriptableObject assets hold character stats, but they cannot live on GameObjects in the scene on their own. You need a MonoBehaviour script to bridge that data into gameplay.
The BattleCharacter script will reference a CharacterData asset, track runtime state like current HP, handle damage calculation, and apply each character's color to its visual representation.
- In the Project window, right-click the Scripts folder.
- Select Create > C# Script.
- Name the script BattleCharacter and press Enter.
- Double-click BattleCharacter.cs to open it in VS Code.
- Select all the default code and replace it with the class declaration, fields, and the Initialize() method below:
using UnityEngine;
public class BattleCharacter : MonoBehaviour
{
// Reference to the ScriptableObject data asset
public CharacterData data;
// Tracks HP during battle (starts at maxHP)
public int currentHP;
// Called at battle start to set up this character
public void Initialize()
{
currentHP = data.maxHP;
// Apply the character's color to the capsule
Renderer renderer = GetComponentInChildren<Renderer>();
if (renderer != null)
{
renderer.material.color = data.characterColor;
}
}
}
What does this code do?
The data field holds a reference to the CharacterData ScriptableObject you created earlier. This is how the script reads stats like maxHP, attack, and defense at runtime.
Initialize() sets currentHP to full health and then tints the capsule's material to match characterColor from the data asset. This is how you will tell heroes and enemies apart visually.
- Save BattleCharacter.cs in VS Code.
- Switch back to Unity and wait a moment for the script to compile. Check the Console window at the bottom for any red error messages.
Seeing a compilation error?
- Make sure the file is named exactly BattleCharacter.cs. In C#, the filename must match the class name.
- Confirm that CharacterData.cs is still in the Scripts folder and compiles without errors. BattleCharacter depends on it.
- Why is my BattleCharacter script not compiling in Unity?
- Return to BattleCharacter.cs in VS Code.
- Inside the BattleCharacter class, add these two methods below the closing brace of Initialize():
// Calculate and apply damage from an attacker
public int TakeDamage(int incomingAttack)
{
// Damage = attacker's power minus this character's defense (minimum 1)
int damage = Mathf.Max(1, incomingAttack - data.defense);
currentHP = Mathf.Max(0, currentHP - damage);
return damage;
}
// Check whether this character is still standing
public bool IsAlive()
{
return currentHP > 0;
}
What does this code do?
TakeDamage() subtracts the target's defense from the incoming attack value, ensuring the result is at least 1. This means every hit deals some damage, even against heavily armored characters.
IsAlive() returns true if the character's HP is above 0. The BattleManager will use this later to skip defeated characters and check for victory or defeat.
- Save BattleCharacter.cs.
- Switch back to Unity and confirm no compilation errors appear in the Console.
New errors after adding the methods?
- Make sure both methods are inside the class braces. They should sit after Initialize() but before the final closing } of the class.
- Check that you did not accidentally delete the closing brace of Initialize() when pasting the new code.
- Help me fix a compilation error in my BattleCharacter script after adding TakeDamage and IsAlive.
✔️ Awesome, I've got everything!
Great. Double check you have saved BattleCharacter.cs and that Unity shows no errors in the Console.
ⓧ I'd like to double check the full code
using UnityEngine;
public class BattleCharacter : MonoBehaviour
{
public CharacterData data;
public int currentHP;
public void Initialize()
{
currentHP = data.maxHP;
Renderer renderer = GetComponentInChildren<Renderer>();
if (renderer != null)
{
renderer.material.color = data.characterColor;
}
}
public int TakeDamage(int incomingAttack)
{
int damage = Mathf.Max(1, incomingAttack - data.defense);
currentHP = Mathf.Max(0, currentHP - damage);
return damage;
}
public bool IsAlive()
{
return currentHP > 0;
}
}
With the script ready, you can now create the GameObjects that will represent each character in the scene.
- In Unity, click GameObject in the top menu bar.
- Select 3D Object, then Capsule.
- A new Capsule appears in the Hierarchy window with its name highlighted. Type Warrior and press Enter to rename it.
- Repeat the process three more times to create capsules named Mage, Goblin, and DarkKnight.
Rename an existing GameObject
If you accidentally pressed Enter before typing the name, select the capsule in the Hierarchy and press F2 to re-enter rename mode.
Now attach the BattleCharacter component to each capsule and link it to the matching ScriptableObject asset.
- Select the Warrior capsule in the Hierarchy.
- In the Inspector panel on the right, scroll down and click Add Component.
- Type BattleCharacter in the search box and select it from the results.
- In the Project window, expand Assets > ScriptableObjects > Heroes.
- Drag the Warrior asset into the Data field on the BattleCharacter component in the Inspector.
- Select the Mage capsule in the Hierarchy.
- Click Add Component, search for BattleCharacter, and add it.
- Drag the Mage asset from Assets > ScriptableObjects > Heroes into the Data field.
- Select the Goblin capsule in the Hierarchy.
- Click Add Component, search for BattleCharacter, and add it.
- Drag the Goblin asset from Assets > ScriptableObjects > Enemies into the Data field.
- Select the DarkKnight capsule in the Hierarchy.
- Click Add Component, search for BattleCharacter, and add it.
- Drag the DarkKnight asset from Assets > ScriptableObjects > Enemies into the Data field.
You should now see four capsules in the Hierarchy, each with a BattleCharacter component pointing to its matching data asset.
BattleCharacter not showing in Add Component?
- The script must compile without errors before it appears in the Component browser. Check the Console for red error messages and fix them first.
- Verify that the filename is exactly BattleCharacter.cs and the class name inside the file is BattleCharacter. They must match exactly.
- Why can't I find BattleCharacter in the Add Component search?
Position characters on the battle stage
Right now the capsules are all stacked on top of each other at the center of the scene. You need a ground plane and proper positions so the heroes line up on the left and enemies on the right.
- Click GameObject in the top menu bar.
- Select 3D Object, then Plane.
- Select the Plane in the Hierarchy.
- In the Inspector, find the Transform component at the top.
- Set the Scale to X: 3, Y: 1, Z: 2 to create a wide battle arena.
Now position each character capsule. Heroes go on the left side, enemies on the right.
- Select the Warrior capsule in the Hierarchy.
- In the Transform component, set Position to X: -3, Y: 1, Z: 0.
- Select the Mage capsule.
- Set Position to X: -3, Y: 1, Z: -2.
- Select the Goblin capsule.
- Set Position to X: 3, Y: 1, Z: 0.
- Select the DarkKnight capsule.
- Set Position to X: 3, Y: 1, Z: -2.
Why these positions?
The negative X values place heroes on the left side of the arena. The positive X values place enemies on the right. The Z offsets stagger characters so they do not overlap.
Setting Y to 1 raises each capsule so it sits on top of the ground plane rather than clipping through it.
In the Scene view, you should now see four capsules arranged on the ground plane. Two on the left, two on the right.
Capsules not visible or floating?
- If capsules are sunk into the ground, check that the Y position is set to 1, not 0.
- If the Scene view looks empty, press F with a capsule selected to focus the camera on it.
- My capsules are not positioned correctly on the battle stage.
Set up the 2.5D camera angle and lighting
The default camera looks straight ahead, but classic RPG battles use an elevated, angled-down perspective. This creates the 2.5D look where 3D objects are viewed from a fixed dramatic angle.
- Select Main Camera in the Hierarchy.
- In the Transform component, set Position to X: 0, Y: 8, Z: -8.
- Set Rotation to X: 35, Y: 0, Z: 0.
What creates the 2.5D perspective?
Raising the camera to Y: 8 and pulling it back to Z: -8 lifts the viewpoint above and behind the arena. The 35-degree X rotation angles it downward, creating the classic RPG battle perspective where you look down at the action from above.
Next, adjust the Directional Light for more dramatic shadows across the battle stage.
- Select Directional Light in the Hierarchy.
- In the Transform component, set Rotation to X: 50, Y: -30, Z: 0.
Time to see it all come together. Preview the 2.5D camera angle in action.
- Press the Play button at the top-center of the Unity editor (or press Ctrl+P on Windows or Cmd+P on macOS).
- Look at the Game view. You should see four white capsules arranged on the ground plane from the angled overhead perspective. Heroes sit on the left, enemies on the right.
- Press Play again to exit Play mode.
Why are the capsules all white?
The character colors from your ScriptableObject assets are applied by the Initialize() method, which has not been called yet. Once you build the BattleManager in the next step, it will call Initialize() on every character at battle start, and each capsule will turn its assigned color.
- Save the scene by pressing Ctrl+S (Windows) or Cmd+S (macOS).
Game view looks wrong or shows nothing?
- If you see a blue sky and nothing else, the camera is pointing in the wrong direction. Double-check that the Main Camera Rotation X is set to 35, not -35.
- If capsules appear tiny, verify the camera Position Y is 8 and Z is -8. A Z value of 8 (positive) would push the camera behind the scene.
- Make sure you are looking at the Game tab (not the Scene tab) to see the camera preview.
- My 2.5D camera angle does not look right in the Game view.
Your battle stage is ready. Four characters are standing in formation, waiting for orders. Next up, you will write the BattleManager that controls the entire flow of combat.
Code the Battle System
Your battle scene has characters standing on a stage, but they have no idea what to do. There is no system telling them whose turn it is, what happens when someone attacks, or how to determine a winner.
In this step, you will build the brain of the entire battle. The BattleManager uses an enum-based state machine to control every phase of combat, from setup to victory or defeat.
In this step, get ready to:
- Write the BattleManager state machine that controls turn flow.
- Add player action methods for Attack and Defend.
- Add enemy AI and win/loss conditions.
Write the BattleManager state machine
A state machine keeps track of what phase the battle is in at any moment. Instead of juggling boolean flags like isPlayerTurn and isBattleOver, an enum restricts the battle to exactly one state at a time.
What is a state machine?
A state machine is a pattern where a system can only be in one defined state at a time (like Start, PlayerTurn, EnemyTurn, Won, or Lost). Each state determines what actions are allowed and what transitions are possible next.
This prevents impossible combinations like the player and enemy acting simultaneously. The battle always knows exactly what phase it is in.
- In the Scripts folder, right-click and select Create, then C# Script.
- Name the script BattleManager.
- Double-click BattleManager.cs to open it in VS Code, then replace the entire contents with this code:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class BattleManager : MonoBehaviour
{
// All possible battle states
public enum BattleState { Start, PlayerTurn, EnemyTurn, Won, Lost }
public BattleState state;
public List<BattleCharacter> playerParty;
public List<BattleCharacter> enemyParty;
public BattleUI battleUI;
// Tracks which player character is currently acting
private BattleCharacter currentActor;
private int currentPlayerIndex;
void Start()
{
state = BattleState.Start;
StartCoroutine(SetupBattle());
}
// Initialize all characters and show the opening message
IEnumerator SetupBattle()
{
foreach (var character in playerParty)
character.Initialize();
foreach (var character in enemyParty)
character.Initialize();
battleUI.SetMessage("Enemies appear! Prepare for battle!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(2f);
state = BattleState.PlayerTurn;
BeginPlayerPhase();
}
// Find the first alive player and enable action buttons
void BeginPlayerPhase()
{
currentPlayerIndex = FindNextAliveIndex(playerParty, 0);
if (currentPlayerIndex == -1)
{
state = BattleState.Lost;
battleUI.SetMessage("You have been defeated...");
battleUI.DisableActions();
return;
}
currentActor = playerParty[currentPlayerIndex];
battleUI.SetMessage(currentActor.data.characterName + "'s turn! Choose an action.");
battleUI.EnableActions();
}
// Move to the next alive player, or switch to enemy turn
void AdvancePlayerPhase()
{
int nextIndex = FindNextAliveIndex(playerParty, currentPlayerIndex + 1);
if (nextIndex == -1)
{
state = BattleState.EnemyTurn;
StartCoroutine(ExecuteEnemyTurn());
}
else
{
currentPlayerIndex = nextIndex;
currentActor = playerParty[currentPlayerIndex];
battleUI.SetMessage(currentActor.data.characterName + "'s turn! Choose an action.");
battleUI.EnableActions();
}
}
// Search for the next alive character starting from a given index
int FindNextAliveIndex(List<BattleCharacter> party, int startIndex)
{
for (int i = startIndex; i < party.Count; i++)
{
if (party[i].IsAlive()) return i;
}
return -1;
}
// Pick a random alive character from a party list
BattleCharacter GetRandomAliveCharacter(List<BattleCharacter> party)
{
List<BattleCharacter> alive = new List<BattleCharacter>();
foreach (var c in party)
{
if (c.IsAlive()) alive.Add(c);
}
if (alive.Count == 0) return null;
return alive[Random.Range(0, alive.Count)];
}
}
What does this code do?
- BattleState is an enum that locks the battle into one of five states: Start, PlayerTurn, EnemyTurn, Won, or Lost. Only one state is active at a time.
- SetupBattle() is a coroutine that initializes every character's HP and color, displays a message, pauses for 2 seconds, then transitions to PlayerTurn.
- BeginPlayerPhase() finds the first alive hero and enables the action buttons for them. AdvancePlayerPhase() moves to the next alive hero, or switches to the enemy turn if all heroes have acted.
- FindNextAliveIndex() and GetRandomAliveCharacter() are helper methods that other methods will call to find living characters in a party.
- Save BattleManager.cs.
You won't see any changes in Unity yet. The state machine foundation is ready, but it needs player actions and enemy AI before the battle can run.
Add player action methods
The state machine knows how to cycle through turns, but the player has no way to act. Each hero needs two choices during their turn: attacking an enemy, or defending to recover HP.
- In BattleManager.cs, find the closing brace of the GetRandomAliveCharacter() method. Add the following methods below it, still inside the class braces:
// Called when the player clicks the Attack button
public void OnAttackButton()
{
if (state != BattleState.PlayerTurn) return;
battleUI.DisableActions();
StartCoroutine(ExecutePlayerAttack());
}
// Pick a random alive enemy and deal damage based on the actor's attack
IEnumerator ExecutePlayerAttack()
{
BattleCharacter target = GetRandomAliveCharacter(enemyParty);
if (target == null) yield break;
int damage = target.TakeDamage(currentActor.data.attack);
battleUI.SetMessage(currentActor.data.characterName + " attacks " + target.data.characterName + " for " + damage + " damage!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(1.5f);
if (!target.IsAlive())
{
battleUI.SetMessage(target.data.characterName + " is defeated!");
target.gameObject.SetActive(false);
yield return new WaitForSeconds(1f);
}
// Check if all enemies are down
if (!AnyAlive(enemyParty))
{
state = BattleState.Won;
battleUI.SetMessage("Victory! All enemies defeated!");
battleUI.DisableActions();
yield break;
}
AdvancePlayerPhase();
}
// Called when the player clicks the Defend button
public void OnDefendButton()
{
if (state != BattleState.PlayerTurn) return;
battleUI.DisableActions();
StartCoroutine(ExecutePlayerDefend());
}
// Heal the current actor for 15% of their max HP
IEnumerator ExecutePlayerDefend()
{
int healAmount = Mathf.RoundToInt(currentActor.data.maxHP * 0.15f);
currentActor.currentHP = Mathf.Min(currentActor.data.maxHP, currentActor.currentHP + healAmount);
battleUI.SetMessage(currentActor.data.characterName + " defends and recovers " + healAmount + " HP!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(1.5f);
AdvancePlayerPhase();
}
What do these methods do?
- OnAttackButton() and OnDefendButton() are public methods that Unity's UI buttons will call. They first check that the state is PlayerTurn to prevent double-clicks.
- ExecutePlayerAttack() picks a random alive enemy and calls TakeDamage() using the hero's attack stat. If the target dies, the enemy capsule is deactivated with SetActive(false) so it disappears from the scene.
- ExecutePlayerDefend() heals the current hero for 15% of their max HP, capped at their maximum. This gives the player a strategic choice between dealing damage and staying alive.
- Save BattleManager.cs.
No visible changes yet. The player actions need enemy responses and a UI to function.
Add enemy AI and win/loss conditions
After all heroes have acted, the battle transitions to the enemy turn. Each living enemy picks a random hero to attack, then the cycle returns to the player.
- In BattleManager.cs, find the closing brace of the ExecutePlayerDefend() method. Add the following methods below it, still inside the class braces:
// Loop through all alive enemies, each attacking a random player
IEnumerator ExecuteEnemyTurn()
{
foreach (var enemy in enemyParty)
{
if (!enemy.IsAlive()) continue;
BattleCharacter target = GetRandomAliveCharacter(playerParty);
if (target == null) break;
int damage = target.TakeDamage(enemy.data.attack);
battleUI.SetMessage(enemy.data.characterName + " attacks " + target.data.characterName + " for " + damage + " damage!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(1.5f);
if (!target.IsAlive())
{
battleUI.SetMessage(target.data.characterName + " has fallen!");
yield return new WaitForSeconds(1f);
}
}
// Check if all players are down
if (!AnyAlive(playerParty))
{
state = BattleState.Lost;
battleUI.SetMessage("You have been defeated...");
battleUI.DisableActions();
yield break;
}
// Cycle back to the player turn
state = BattleState.PlayerTurn;
BeginPlayerPhase();
}
// Returns true if any character in the party is still alive
bool AnyAlive(List<BattleCharacter> party)
{
foreach (var c in party)
{
if (c.IsAlive()) return true;
}
return false;
}
What does this code do?
- ExecuteEnemyTurn() loops through every alive enemy. Each one picks a random alive hero, deals damage using the enemy's attack stat, then pauses so the player can read the message.
- After all enemies have attacked, the method checks if any players survived. If not, the battle ends in defeat. If heroes remain, the state cycles back to PlayerTurn.
- AnyAlive() is a helper that returns true if at least one character in the given party still has HP above 0.
- Save BattleManager.cs.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete BattleManager.cs file. Make sure yours matches exactly:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class BattleManager : MonoBehaviour
{
public enum BattleState { Start, PlayerTurn, EnemyTurn, Won, Lost }
public BattleState state;
public List<BattleCharacter> playerParty;
public List<BattleCharacter> enemyParty;
public BattleUI battleUI;
private BattleCharacter currentActor;
private int currentPlayerIndex;
void Start()
{
state = BattleState.Start;
StartCoroutine(SetupBattle());
}
IEnumerator SetupBattle()
{
foreach (var character in playerParty)
character.Initialize();
foreach (var character in enemyParty)
character.Initialize();
battleUI.SetMessage("Enemies appear! Prepare for battle!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(2f);
state = BattleState.PlayerTurn;
BeginPlayerPhase();
}
void BeginPlayerPhase()
{
currentPlayerIndex = FindNextAliveIndex(playerParty, 0);
if (currentPlayerIndex == -1)
{
state = BattleState.Lost;
battleUI.SetMessage("You have been defeated...");
battleUI.DisableActions();
return;
}
currentActor = playerParty[currentPlayerIndex];
battleUI.SetMessage(currentActor.data.characterName + "'s turn! Choose an action.");
battleUI.EnableActions();
}
void AdvancePlayerPhase()
{
int nextIndex = FindNextAliveIndex(playerParty, currentPlayerIndex + 1);
if (nextIndex == -1)
{
state = BattleState.EnemyTurn;
StartCoroutine(ExecuteEnemyTurn());
}
else
{
currentPlayerIndex = nextIndex;
currentActor = playerParty[currentPlayerIndex];
battleUI.SetMessage(currentActor.data.characterName + "'s turn! Choose an action.");
battleUI.EnableActions();
}
}
int FindNextAliveIndex(List<BattleCharacter> party, int startIndex)
{
for (int i = startIndex; i < party.Count; i++)
{
if (party[i].IsAlive()) return i;
}
return -1;
}
BattleCharacter GetRandomAliveCharacter(List<BattleCharacter> party)
{
List<BattleCharacter> alive = new List<BattleCharacter>();
foreach (var c in party)
{
if (c.IsAlive()) alive.Add(c);
}
if (alive.Count == 0) return null;
return alive[Random.Range(0, alive.Count)];
}
public void OnAttackButton()
{
if (state != BattleState.PlayerTurn) return;
battleUI.DisableActions();
StartCoroutine(ExecutePlayerAttack());
}
IEnumerator ExecutePlayerAttack()
{
BattleCharacter target = GetRandomAliveCharacter(enemyParty);
if (target == null) yield break;
int damage = target.TakeDamage(currentActor.data.attack);
battleUI.SetMessage(currentActor.data.characterName + " attacks " + target.data.characterName + " for " + damage + " damage!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(1.5f);
if (!target.IsAlive())
{
battleUI.SetMessage(target.data.characterName + " is defeated!");
target.gameObject.SetActive(false);
yield return new WaitForSeconds(1f);
}
if (!AnyAlive(enemyParty))
{
state = BattleState.Won;
battleUI.SetMessage("Victory! All enemies defeated!");
battleUI.DisableActions();
yield break;
}
AdvancePlayerPhase();
}
public void OnDefendButton()
{
if (state != BattleState.PlayerTurn) return;
battleUI.DisableActions();
StartCoroutine(ExecutePlayerDefend());
}
IEnumerator ExecutePlayerDefend()
{
int healAmount = Mathf.RoundToInt(currentActor.data.maxHP * 0.15f);
currentActor.currentHP = Mathf.Min(currentActor.data.maxHP, currentActor.currentHP + healAmount);
battleUI.SetMessage(currentActor.data.characterName + " defends and recovers " + healAmount + " HP!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(1.5f);
AdvancePlayerPhase();
}
IEnumerator ExecuteEnemyTurn()
{
foreach (var enemy in enemyParty)
{
if (!enemy.IsAlive()) continue;
BattleCharacter target = GetRandomAliveCharacter(playerParty);
if (target == null) break;
int damage = target.TakeDamage(enemy.data.attack);
battleUI.SetMessage(enemy.data.characterName + " attacks " + target.data.characterName + " for " + damage + " damage!");
battleUI.UpdateAllHP(playerParty, enemyParty);
yield return new WaitForSeconds(1.5f);
if (!target.IsAlive())
{
battleUI.SetMessage(target.data.characterName + " has fallen!");
yield return new WaitForSeconds(1f);
}
}
if (!AnyAlive(playerParty))
{
state = BattleState.Lost;
battleUI.SetMessage("You have been defeated...");
battleUI.DisableActions();
yield break;
}
state = BattleState.PlayerTurn;
BeginPlayerPhase();
}
bool AnyAlive(List<BattleCharacter> party)
{
foreach (var c in party)
{
if (c.IsAlive()) return true;
}
return false;
}
}
Seeing compile errors in Unity? That is expected.
BattleManager.cs references a BattleUI class that does not exist yet. Unity's console will show errors about the unknown type.
These errors will disappear in the next step when you create BattleUI.cs. For now, just make sure your code matches the file above and move on.
Now create the GameObject that will run this script in the scene.
- In the Unity Hierarchy, right-click in an empty area and select Create Empty.
- Name the new GameObject BattleManager.
Why a separate empty GameObject?
The BattleManager is a system that coordinates the battle. It does not represent a visible object in the world. An empty GameObject gives it a place to live in the scene without any mesh, collider, or visual component.
- You should see BattleManager listed in the Hierarchy alongside your capsules and ground plane.
When can I add the BattleManager component?
Because BattleUI.cs does not exist yet, Unity cannot compile BattleManager.cs. You will not be able to add the BattleManager component to this GameObject until the next step.
Once you create BattleUI.cs in Step 5, all compile errors will resolve. You will attach the component and wire up the Inspector fields at that point.
Don't see BattleManager in the Hierarchy?
- Make sure you right-clicked in an empty space in the Hierarchy panel, not inside the Project window. The Hierarchy shows scene objects, while the Project window shows files.
- If you accidentally created a child of another object, drag BattleManager to the root level of the Hierarchy so it sits at the same level as the capsules and ground plane.
Help me create the BattleManager empty GameObject in my Unity scene.
The battle brain is written. Next up, you will create the UI with action buttons and HP displays, then wire everything together for a fully playable battle.
Create the Battle UI and Connect Everything
Your BattleManager is coded and ready to run a full battle. It can initialize characters, cycle through turns, and check for victory or defeat.
The problem is that the player has no way to see what is happening or choose actions. In this step, you will build the battle UI with action buttons, HP displays, and a message log. Then you will wire everything together so the full battle is playable.
In this step, get ready to:
- Build the Canvas UI layout with message text, action buttons, and HP displays.
- Write the BattleUI script that controls all UI elements.
- Wire every Inspector reference and playtest the complete battle.
Build the Canvas UI layout
Unity's uGUI system uses a Canvas as the root container for all in-game UI elements. Every text label, button, and HP display you create will be a child of this Canvas.
- In the Unity menu bar, click GameObject.
- Select UI, then Canvas.
What is the EventSystem?
Unity automatically creates an EventSystem alongside the Canvas. This handles all input detection for UI elements like button clicks. Leave it in the scene.
Now you will add the battle message text that displays combat updates at the top of the screen.
- In the Hierarchy, right-click on the Canvas.
- Select UI, then Text - TextMeshPro.
Seeing an import prompt?
The first time you add a TextMeshPro element, Unity prompts you to import TMP Essential Resources. Click Import TMP Essentials and wait for the import to finish. This only happens once.
- Rename the new text object to MessageText in the Hierarchy.
- In the Inspector, set the Rect Transform anchor preset to top-center by clicking the anchor widget and selecting the top-center option.
- In the TextMeshPro - Text (UI) component, set the text to Battle Start!.
Next, create the two action buttons the player will use to choose Attack or Defend each turn.
- Right-click on the Canvas in the Hierarchy.
- Select UI, then Button - TextMeshPro.
- Rename this button to AttackButton.
- Expand AttackButton in the Hierarchy and click the child text object.
- In the Inspector, change the text to Attack.
- Set the Rect Transform anchor preset on AttackButton to bottom-center.
- Create a second Button - TextMeshPro as a child of the Canvas.
- Rename it to DefendButton and set its child text to Defend.
- Set the Rect Transform anchor preset on DefendButton to bottom-center.
- Position AttackButton and DefendButton side by side at the bottom of the screen by adjusting their Pos X values (e.g. -100 and 100).
Finally, create the four HP display texts so the player can see each character's health during battle.
- Right-click the Canvas and select UI > Text - TextMeshPro.
- Rename it to PlayerHP1 and anchor it to the bottom-left of the screen.
- Repeat to create PlayerHP2, also anchored to the bottom-left. Offset its Pos Y so it sits below PlayerHP1.
- Create EnemyHP1 and anchor it to the top-right of the screen.
- Create EnemyHP2 and anchor it to the top-right, offset below EnemyHP1.
Switch to the Game tab in the editor. You should see the Battle Start! message at the top, two buttons at the bottom-center, and HP text labels in the bottom-left and top-right corners.
UI elements not visible in the Game view?
Make sure every UI element is a child of the Canvas in the Hierarchy. Text and buttons created outside the Canvas will not render on screen.
If text appears but is tiny, select the text element and increase the Font Size in the Inspector.
Help me troubleshoot my Canvas UI layout in Unity.
Write the BattleUI script
The Canvas elements are in place, but they need a script to control them. BattleUI.cs will hold references to every UI element and expose methods that BattleManager already calls: SetMessage(), EnableActions(), DisableActions(), and UpdateAllHP().
- In the Project window, right-click inside the Scripts folder.
- Select Create > C# Script and name it BattleUI.
- Double-click BattleUI.cs to open it in VS Code.
- Replace the entire contents of the file with the following class declaration and fields:
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using TMPro;
public class BattleUI : MonoBehaviour
{
// References to UI elements set in the Inspector
public TextMeshProUGUI messageText;
public Button attackButton;
public Button defendButton;
public TextMeshProUGUI[] playerHPTexts;
public TextMeshProUGUI[] enemyHPTexts;
}
What does this code do?
The using TMPro directive gives access to the TextMeshProUGUI class, which is what TextMeshPro text elements use on a Canvas.
The using UnityEngine.UI directive gives access to the Button class. Each public field will be connected to a UI element in the Inspector later.
You won't see a change yet. The next chunk adds the methods that make the UI respond to battle events.
- Add the following methods inside the BattleUI class, below the field declarations:
// Display a message in the battle log
public void SetMessage(string message)
{
messageText.text = message;
}
// Turn action buttons on so the player can click them
public void EnableActions()
{
attackButton.interactable = true;
defendButton.interactable = true;
}
// Turn action buttons off while actions resolve
public void DisableActions()
{
attackButton.interactable = false;
defendButton.interactable = false;
}
// Refresh every character's HP display text
public void UpdateAllHP(List<BattleCharacter> players, List<BattleCharacter> enemies)
{
for (int i = 0; i < playerHPTexts.Length && i < players.Count; i++)
{
if (players[i].IsAlive())
playerHPTexts[i].text = players[i].data.characterName + "\nHP: " + players[i].currentHP + "/" + players[i].data.maxHP;
else
playerHPTexts[i].text = players[i].data.characterName + "\nDEFEATED";
}
for (int i = 0; i < enemyHPTexts.Length && i < enemies.Count; i++)
{
if (enemies[i].IsAlive())
enemyHPTexts[i].text = enemies[i].data.characterName + "\nHP: " + enemies[i].currentHP + "/" + enemies[i].data.maxHP;
else
enemyHPTexts[i].text = enemies[i].data.characterName + "\nDEFEATED";
}
}
What does this code do?
- SetMessage() updates the top-of-screen text so the player sees combat events like damage dealt or turn changes.
- EnableActions() and DisableActions() toggle Button.interactable. This prevents the player from clicking buttons while an attack is animating.
- UpdateAllHP() loops through every character and writes their name plus current/max HP into the text labels. Fallen characters show "DEFEATED" instead.
- Save BattleUI.cs.
- Return to Unity and wait for the script to compile. Check the Console window at the bottom of the editor for errors.
- You should see no errors in the Console. The script compiles cleanly.
Seeing a compile error?
If you see an error mentioning TMPro, make sure you imported TMP Essential Resources when prompted earlier. You can trigger it again by adding any Text - TextMeshPro UI element.
If the error references BattleCharacter, confirm that BattleCharacter.cs exists in the Scripts folder and compiles on its own.
Help me fix a compile error in BattleUI.cs.
- Now add the BattleUI component to the Canvas. Select the Canvas in the Hierarchy.
- In the Inspector, click Add Component.
- Search for BattleUI and select it.
- You should see the BattleUI component appear in the Inspector with empty fields for messageText, attackButton, defendButton, and the HP text arrays.
✔️ Awesome, I've got everything!
Great. Double check you have saved BattleUI.cs and that the BattleUI component is attached to the Canvas.
ⓧ I'd like to double check the full code
Here is the complete BattleUI.cs file. Make sure your code matches this exactly.
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using TMPro;
public class BattleUI : MonoBehaviour
{
public TextMeshProUGUI messageText;
public Button attackButton;
public Button defendButton;
public TextMeshProUGUI[] playerHPTexts;
public TextMeshProUGUI[] enemyHPTexts;
public void SetMessage(string message)
{
messageText.text = message;
}
public void EnableActions()
{
attackButton.interactable = true;
defendButton.interactable = true;
}
public void DisableActions()
{
attackButton.interactable = false;
defendButton.interactable = false;
}
public void UpdateAllHP(List<BattleCharacter> players, List<BattleCharacter> enemies)
{
for (int i = 0; i < playerHPTexts.Length && i < players.Count; i++)
{
if (players[i].IsAlive())
playerHPTexts[i].text = players[i].data.characterName + "\nHP: " + players[i].currentHP + "/" + players[i].data.maxHP;
else
playerHPTexts[i].text = players[i].data.characterName + "\nDEFEATED";
}
for (int i = 0; i < enemyHPTexts.Length && i < enemies.Count; i++)
{
if (enemies[i].IsAlive())
enemyHPTexts[i].text = enemies[i].data.characterName + "\nHP: " + enemies[i].currentHP + "/" + enemies[i].data.maxHP;
else
enemyHPTexts[i].text = enemies[i].data.characterName + "\nDEFEATED";
}
}
}
Wire everything up and playtest
All the scripts are written and all the UI elements exist. The last piece is connecting every reference in the Inspector so the scripts know which GameObjects to talk to.
Start by wiring the BattleUI component on the Canvas.
- Select the Canvas in the Hierarchy.
- In the BattleUI component in the Inspector, drag MessageText from the Hierarchy into the Message Text field.
- Drag AttackButton into the Attack Button field.
- Drag DefendButton into the Defend Button field.
- Expand the Player HP Texts array. Set the size to 2.
- Drag PlayerHP1 into Element 0 and PlayerHP2 into Element 1.
- Expand the Enemy HP Texts array. Set the size to 2.
- Drag EnemyHP1 into Element 0 and EnemyHP2 into Element 1.
Now wire the BattleManager so it knows about the party members and the UI.
- Select the BattleManager GameObject in the Hierarchy.
- In the Inspector, expand the Player Party list. Set the size to 2.
- Drag the Warrior capsule into Element 0 and the Mage capsule into Element 1.
- Expand the Enemy Party list. Set the size to 2.
- Drag the Goblin capsule into Element 0 and the DarkKnight capsule into Element 1.
- Drag the Canvas from the Hierarchy into the Battle UI field on the BattleManager component.
Why drag the Canvas into Battle UI?
The battleUI field on BattleManager expects a BattleUI component reference. Because you attached the BattleUI component to the Canvas, dragging the Canvas in automatically resolves to that component.
The last wiring step connects the button clicks to the BattleManager methods.
- Select AttackButton in the Hierarchy.
- In the Inspector, scroll down to the On Click () event section on the Button component.
- Click the + button to add a new event entry.
- Drag the BattleManager GameObject from the Hierarchy into the object field of the new event entry.
- Click the function dropdown (it reads No Function) and select BattleManager > OnAttackButton.
- Now select DefendButton in the Hierarchy.
- Repeat the same process: click + on On Click (), drag the BattleManager into the object field, and select BattleManager > OnDefendButton.
What are On Click () events?
Unity's uGUI buttons use serialized events that you can configure directly in the Inspector without writing any connection code. When the player clicks the button at runtime, Unity automatically calls the method you assigned.
Everything is wired. Time to playtest the full battle.
- Save the scene by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Press the Play button at the top of the Unity editor.
The battle should begin with the message "Enemies appear! Prepare for battle!" followed by the first hero's turn prompt. HP displays should show each character's name and health values.
- Click Attack or Defend for each hero when their turn comes up.
- Watch the HP values update after each action.
- Observe defeated enemies disappear from the scene.
- Keep playing until the battle ends with either a "Victory!" or "You have been defeated..." message.
Battle not starting or buttons not responding?
If nothing happens when you press Play, check the Console for errors. A NullReferenceException usually means a field is not wired in the Inspector. Double-check that every field on the BattleUI and BattleManager components has a reference assigned.
If buttons appear but clicking them does nothing, verify the OnClick() events. Select each button and confirm that the function dropdown shows the correct method (OnAttackButton or OnDefendButton).
If HP text is not updating, make sure the Player HP Texts and Enemy HP Texts arrays on the Canvas BattleUI component have their size set to 2 with the correct text elements in each slot.
Help me debug my battle system in Unity.
Secret mission
Add a Magic System with MP
What if your heroes could cast spells? Right now every hero has the same two options: Attack or Defend. In this secret mission, you will add a Magic system with MP, giving each hero a powerful spell attack that costs mana points.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely in the Unity Editor on your local machine. There are no cloud resources, paid assets, or external services to worry about, so there are no ongoing costs.
Decide whether to keep your project files, close Unity to free up memory, or delete everything entirely.
Resources you used:
- TurnBasedRPG Unity project folder (all C# scripts, ScriptableObject assets, and scene files).
Keep everything running
No action needed. Choose this if you plan to keep building on your RPG battle system.
- Your TurnBasedRPG project is saved locally and ready to open any time from Unity Hub.
- You can extend the project by creating more ScriptableObject assets for new characters, adding abilities, or building an overworld scene.
Pause - I'll come back to this later
Close Unity to free up memory while keeping all your project files on disk.
- Save your scene in Unity by pressing Ctrl+S (Windows) or Cmd+S (macOS).
- Close the Unity Editor. Your project files remain on your machine.
- When you are ready to return, open Unity Hub and select the TurnBasedRPG project from the list.
Delete - I don't want to use this again
Remove the project entirely from your machine. This cannot be undone.
- Close the Unity Editor if it is still open.
- Open Unity Hub. In the Projects list, right-click TurnBasedRPG and select Remove project from list. This removes it from Unity Hub but does not delete the files.
- Delete the TurnBasedRPG folder from your file system. Open Finder (macOS) or File Explorer (Windows), navigate to the save location you chose when creating the project, and delete the entire TurnBasedRPG folder.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully playable turn-based RPG battle system in Unity, complete with data-driven characters, a state machine controlling the battle flow, and a full combat UI.
You've learned how to:
- Built a 2.5D battle scene with positioned hero and enemy characters viewed from a classic angled RPG camera.
- Created a data-driven character system using ScriptableObjects that lets you add new characters entirely from the Inspector without changing code.
- Implemented an enum-based state machine in BattleManager that controls the full battle flow across player turns, enemy AI, and win/loss conditions.
- Secret Mission: Extended the battle system with a Magic/MP resource, adding a third action type and mana management for each hero.
Ready to quiz yourself?