present
In the vast universe of video games, one of the most popular game mechanics is the turn-based combat system. This philosophical approach to development can be found in many examples of games, especially in the RPG genre. The original Final Fantasy series is the best example of turn-based combat systems in games. In this guide, I'll be implementing a simple yet customizable turn-based combat system.
Content:
- The structure of the alternating combat system.
- Transition from the game world to the turn-based arena
- Save world state data
- battle data set
- transition to the battlefield
- Transition animation settings
- his move! The core of the turn-based combat system
- Appearance of the fight.
- Update the HUD using a script
- Control the flow of the battle.
- Characters appear and disappear in combat position (optional)
- na redu je igrač
- it's the enemy's turn
- final state
1. Structure of the turn-based combat system.
We will use two different scenarios to implement the turn-based combat system. The first scene will contain all items from the level our character is currently on. The second scene will be the battlefield we will move to. This happens when our character is touched by an enemy in the level. Alternatively, battles can be randomly called each time the player travels through the game world. This system is similar to the mechanics of old role-playing games, where the player enters the fight without even knowing who is attacking him.

The scenario in which we enter the battle will be determined by the collisions between characters and enemies. When a new collision is detected, the transition animation will start, a new arena scene will be loaded, and the necessary data will be read. This data will include information about the enemy as well as the player's combat abilities such as health, mana, etc.
In order to correctly position the character in the level after the fight, I will also record his last position. Both sides will trade attacks until one of them runs out of HP. When this happens, the player will be returned to the level or the end screen will appear. I will create all the functionality in addition to what I have created in the previous tutorials. seemovement from top to bottom,combat mechanicsgmap tile systemPost for more.
2. Transition from the game world to the turn-based arena
In this section, we will switch our character from the level scene to the arena scene. All of this will happen while maintaining character state information.
Save world state data
To save data between two different "scenes", we will use"Programmable Objects"concept. It is a data container implemented in Unity to hold large amounts of data independent of class instances. I write and read these objects every time we change scenes during the game. I will expand on the given example hereArticles about combat mechanics..
- Right-click on the Assets panel and create a new "C# Script"
- Name the script "CharacterStatus" and remove the "Start()" and "Update()" functions
- I'm going to make our object inherit "ScriptableObject" instead of "MonoBehaviour".
- It defines all the fields that we will use to describe the state of the character in battle.
- To do
- qualification
- maxHealth y currHealth
- macMana i currMana
- I will add a float field 'position' to store the last recorded position of the character in the level. the reason why i don't usevector 2This is because the Unity engine cannot serialize it. Serialization is beyond the scope of this guide, but for more information seeOfficial documentation of the unit.
- The last field I want to use is the character's GameObject which contains all the fight animations. They can be called upon in battle.
- To create a new object inside the engine, I'll add a line above the class declaration.
After that, the entire class should look like this:
Use UnityEngine;[CreateAssetMenu(fileName = "HealthStatusData", menuName = "StatusObjects/Health", order = 1)] public class CharacterStatus: ScriptableObject{ public string charName = "name"; public float[] position = new float[2] ; public GameObject char GameObject; public Integer level = 1; public float maxHealth = 100; public float maxMana = 100; public float health = 100; public float mana = 100; }
If you have completed all the steps above and returned to the engine, you can now create a state object. Make one for the player and a placeholder for the enemy. At this point, you can create as many different states as you want, depending on the number of enemies present in the level!

battle data set
Now that we have a scriptable object, let's define two fields in the script "StatusManager" of type "CharacterStatus". They will contain information about the status of the player and the enemy, and who is attacking our player. The enemy status field is a generic place that only contains information about our opponent at a certain time. That is, I will update this field only when an enemy touches our character in the level. We'll do it before we go into the arena to fight real enemies! To make sure that no other enemy can attack our player during a transition that has already been done, I will use a boolean value.
void OnTriggerEnter2D(Collider2D other){ if(this.playerStatus.health > 0) { if (other.tag == "Neprijatelj") { if(!isAttacked) { isAttacked = true; }Establecer datos de batalla (otros); LevelLoader. instancia.LoadLevel("BattleArena"); } } }}
Before going into battle, I first check a few conditions:
- if our character is still alive
- Entities that collide with us are enemies.
- The transition hasn't happened yet
After that I call two functions. One is responsible for populating the enemy location data and the other is responsible for loading a new scene. Then we'll implement them and start with setBattleData(), which is easy.
setBattleData(Collider2D other){ // podaci o igraču playerStatus.position[0] = this.transform.position.x; playerStatus.position[1] = this.transform.position.y; // podaci o neprijatelju CharacterStatus status = other.gameObject .GetComponent().enemyStatus;enemyStatus.charName = status.charName;enemyStatus.characterGameObject = status.characterGameObject.transform.GetChild(0).gameObject;enemystatus.health = status.health;enemystatus.maxHealth = status.maxHealth; neprijatelj status.mana = status.mana; neprijatelj status.maxMana = status.maxMana; }
The highlighted lines refer to the enemy's pre-made child's play objects. When an enemy appears in a level, their "combat presence" will be disabled. However, we will use it to generate, animate, and update enemy states during combat. In other words, each enemy in the level consists of two "characters". One defines behavior in the outside world, and the other defines behavior in combat. Add an "Animator" component to the "Battle Presence" object and define all the animation actions that the enemy will perform during the battle. Add a new script to the root of the enemy prefab with only the "CharacterStatus" field and assign it the enemy data.

transition to the battlefield
Now let's see how to move between levels. I will create a new game object and attach a script called "LevelLoader". Since we want to manage the scene in Unity, we first need to import the necessary libraries.
Utilice UnityEngine.SceneManagement;
I just want to have one instance of this object that is available no matter what scene our character is in. That is, I will implement it using an object-oriented programming (OOP) pattern calledsingle. Also, I will use the built-in function "DontDestroyOnLoad()" to ensure that the LevelLoader is only created once during the entire game session.
#region Singleton public static LevelLoader instance; void Awake(){ instance = this; DontDestroyOnLoad(this.gameObject);}#endregion
Now we are ready to prepare a series of different transition animations. Let's define the field related to the "Animator" component and the duration of the transition we want to use. The creative possibilities are almost endless, and the appearance of the transition depends on your imagination. However, each animation must be divided into two parts, a beginning and an end. The reason is that we want to load the scene in the background and create a natural flow. For this I will use "corruption'Properly timed to perform the required functions.
public Animator transition; public float conversionTime = 1f; public void LoadLevel(string nameLevel){ StartCoroutine(LoadNamedLevel(NameLevel));}IEnumerator LoadNamedLevel(string nameLevel){ // Start transition animation conversion.SetTrigger("Start"); } Execution return new WaitForSeconds(transitionTime); SceneManager.LoadScene(levelName); // End of transition animation conversion.SetTrigger("End");}
The code should be self-explanatory. First, we activate the startup animation and wait for a certain amount of time. Second, we load a new scene using the built-in "SceneManager". Finally, we run a final animation to complete the transition. Both "triggers" are parameters defined in the "animator" that we want to use for the given transition.
Transition animation settings
To define transition animations, first create a canvas and make it a child of "LevelLoader". From this point on, everything below that object can be used as part of the animation. For this tutorial, I'll simply drag a giant pixelated circular image from right to left. The initial animation will consist of frames where the image moves from the right to the middle. The final animation consists of frames that move the image from the center of the screen to the left. I named them "CircleWipe_Start" and "CircleWipe_End" accordingly.

Setting up "Animator" is very simple.
- Defines two parameters of type "trigger" to trigger two animations. These are used in the level loading script we wrote earlier.
- Set the final animation to the default state
- transition to the beginning of the animation
- In the Inspector panel, uncheck "Have Timeout" and set the Transition Duration and Transition Offset fields to "0". Use the "start" trigger as the condition for the transition to occur.
- Create similar settings for the transition from start clip to end clip, but use the "End" trigger parameter
- Drag and drop the game object from the canvas to the "Animator" field of the "LevelLoader" script

If you want to define multiple transition animations, you don't need to create these settings for each new animator!
- Create an "Animator Override Controller" in your asset source
- Specifies an animation controller with all native transitions.
- Select the new start and end animation clips that you want to replace with the old one
- In the canvas object that contains the new set of animations, select the new controller in the "Animator" component.

Next, we'll look at how to control the flow of combat in individual scenes.
3. Your action! The core of the turn-based combat system
In this chapter we will create a completely new scene where our battle will take place. I will show the stats for both sides to the player on the Heads Up Display (HUD). When the player presses the button during their turn, they will attack the enemy. In this way, we will give the enemy a chance to attack us. Battles will end in victory or defeat, depending on the health status of both players.
Appearance of the fight.
It's time to create an arena stage where our characters will collide! Start by creating a new scene and name it "BattleArena". We want our two characters to be in certain positions on the battlefield. To do this, I'll add two platform objects with shadow sprites. One is for the player and the other is for the enemies we find. We will load the character's "BattlePresence" at these points in the scene.
As with any fighting game, we had to keep track of the character's health and mana points. It is necessary to show this information numerically in some way. Depending on the situation, the player can behave differently. i created aPrefabricated houseI will use this to display the health and mana of players and enemies. They consist of plain text and images of the "fill" type. In Unity, the image allows for gradual charging during gameplay and is a perfect candidate to represent the health bar.

The health and mana HUD is ready. However, to control its state, we need to access its fields. We could reference these fields in the scripts responsible for managing during battle, but I'll make them part of the main prefab. In this way, we managed to separate the logic of calculating combat values from their visualization. In other words, it leads to a cleaner, more modular solution. Add a new script to the main prefab and name it "StatusHUD". Create public fields within the script that will reference all text and images. These will be used to display the status of our character.

Update the HUD using a script
In the script, we will write a function that takes the "state" object that we defined earlier as a parameter. I'll use this to update all the relevant HUDs in the scene we just created. In order to update our progress bar, I need to enter a value between 0 and 1. To do this, I calculate the percentage value of the current health stat compared to its maximum amount. I will then normalize them so that their values are always between 0 and 1.
public void SetStatusHUD(estado de stado de personaje) { float currentHealth = status.health * (100 / status.maxHealth); float trenutna mana = status. mana * (100 / status. max mana); statusHPBar.fillAmount = estado de salud actual / 100; statusHPValue .SetText(status.health + "/" + status.maxHealth); statusMPBar.fillAmount = stvarni broj / 100; statusMPValue.SetText(status.mana + "/" + status.maxMana);}
The HUD will likely change frequently during combat. Therefore, I will add a function that is only responsible for updating the "Health" column. I do this for efficiency, as I don't want to change every HUD element every time I need to update my personal stats. To make things more interesting, I "gradually" fill a given status bar and its corresponding text over time. For this I will again use, you guessed it,corruption!
public void SetHP(CharacterStatus status, float hp){ StartCoroutine(GraduallySetStatusBar(status, hp, false, 10, 0.05f));}IEnumerator GraduallySetStatusBar(CharacterStatus status, float cantidad, bool isIncrease, int fillTimes, float fillDelay) { porcentaje flotan te = 1 / (flotante) tiempos de llenado; if (isPovećaj) { for (int fillStep = 0; fillStep < fillTimes; fillStep++) { float _fKoličina = monto * porcentaje; float _dIznos = _fIznos / status.maxHealth; status.health += _fIznos ; statusHPBar.fillAmount += _dAmount; if (status.health <= status.maxHealth) statusHPValue.SetText(status.health + "/" + status.maxHealth); rendimiento devuelve nuevos WaitForSeconds(fillDelay); } } else { for ( int fillStep = 0; fillStep < fillTimes; fillStep++) { float _fKoličina = monto * porcentaje; float _dIznos = _fIznos / status.maxHealth; status.health -= _fIznos; statusHPBar.fillAmount -= _dAmount; if (status.health > = 0 ) statusHPValue.SetText(status.health + "/" + status.maxHealth); Rendimiento devolver nuevo WaitForSeconds(fillDelay); } }}
In order for the coroutines to work, we need to calculate the "percentage" of the given statistic that we want to increase or decrease. It mostly depends on how long it takes us to change the HUD element to a certain value. In the above code I do this in the following lines:
float % = 1 / (float) fillTimes; [...] float _fAmount = amount * percentage; float _dIznos = _fIznos / status.maxHealth;
Control the flow of the battle.
Now we will write the core of the turn-based combat system. I will focus on building a base that you can easily expand. Adjustments to existing game mechanics are recommended. Let's start by creating an empty game object called "BattleSystem". Add a new script called "Battle System Manager". In it I will define alistThe type data structure contains all possible battle states. I'll keep him out of class.
Javno nabrajanje BattleState {START, PLAYER TURN, EEMY TURN, WIN, LOSE}
Before I start coding, I need some references about this battle.
- Our character statistics (CharacterStatus)
- Corresponding HUD element (StatusHUD)
- Platforma (transformacija) na kojoj će se pojaviti.
- Character fight animation (GameObject)
- The current battle state is (BattleState)
- Did the player choose an action (boolean). This will prevent a player from choosing a specific action multiple times during their turn.
private GameObject enemy; private GameObject Player; public transformation of the enemy's combat position; public transformation of the player's battle position; public character state player state; audience Character status Enemy status; Public Status HUD Player Status HUD; Public Status HUD Enemy Status HUD; private BattleStateBattleState; private bool hasClicked = true;
Assign the correct data to all these fields by dragging and dropping the resource in the editor.

In Start(), I'll set the initial state of the battle and call a function that stages the battle. I fade into character before taking the player's turn.
void Start(){ BattleState = BattleState.START; StartCoroutine(BeginBattle());}IEnumerator BeginBattle(){ // rodovi neprijateljskih osoba na platformi = Instantiate(enemyStatus.characterGameObject, HeavenlyBattlePosition); enemigo.SetActive(true); jugador =instanciate(playerStatus.characterGameObject.transform.GetChild(0).gameObject, playerBattlePosition); player.SetActive(true); // Hacer que el sprite del personaje sea invisible al inicio enemigo.GetComponent().color = new Color(1, 1, 1, 0); player.GetComponent ().color = new Color(1, 1, 1, 0); // Sets character stats displayed by HUD playerStatusHUD.SetStatusHUD(playerStatus); Enemy Status HUD.SetStatusHUD(Enemy Status); Execution return new WaitForSeconds(1); // The character's character fade effect return StartCoroutine(FadeInOpponents()); yield return new WaitForSeconds(2); // It's the player's turn! BattleState = BattleState.PLAYERTURN; // Now let the player choose his action! return performance StartCoroutine(PlayerTurn()); }
The above code is very simple. First, we generate a character's "BattlePresence" on their platform. I make their sprites transparent temporarily so I can fade them out later. It gives the game an extra nice visual effect that can easily be implemented using coroutines. This is completely optional and in the next optional section I will show you how to do it. After that, we give the player a chance to be the first to perform the action he wants. In our case it would be an attack.
Characters appear and disappear in combat position (optional)
What the "FadeInOpponents" function does is fade out our character before a fight. To do this, I manipulate the alpha value of his sprites per second. Similar to how we update the health, I calculate the percentage by which the object's opacity should increase at each time step. Therefore, the number of steps specified as a parameter determines how quickly the character becomes fully opaque.
IEnumerator FadeInOpponents(int steps = 10){ float totalTransparencyPerStep = 1 / (float) steps; za (int i=0;i().boja; float alfa = currColor.a; alfa += transPerStep; ob.GetComponent ().color = nuevo Color(currColor.r, currColor.g, currColor.b, alfa);}
it's the player's turn
The implementation of player moves boils down to three closely related functions. First, we give the player a chance to perform an action by releasing the block imposed by the boolean "isClicked" parameter. Second, we define the logic for pressing the keys. Be careful to press only once per move. Third, we run the animations and attack logic.
IEnumerator PlayerTurn(){ // Could display some message // Say it's the player's turn now return new WaitForSeconds(1); // Unlocks the click // so the player can click the "Attack" button hasClicked = false;}
public void OnAttackButtonPress(){ // If it's not the player's turn yet, the player can't click the "Attack" button! if (battleState != BattleState.PLAYERTURN) return; // Only one action is allowed per move if(!hasClicked) { StartCoroutine(PlayerAttack()); // Prevent the user from repeatedly pressing the attack button hasClicked = true; }}IEnumerator PlayerAttack(){ // Activate attack animation on 'BattlePresence' animator // Player.GetComponent().SetTrigger("Attack"); Yield return new WaitForSeconds(2); // Reduces the enemy's health by a // flat amount of 10. Maybe you should have // more complex logic here. enemyStatusHUD.SetHP(enemyStatus, 10); if (enemyStatus.health <= 0) { // If enemy health drops to 0 // we win! BattleState = BattleState.WIN; Return StartCoroutine(EndBattle()); } else { // If the enemy's health is still above 0 at the end of the move // it's the enemy's turn! BattleState = BattleState.ENEMYTURN; execution return StartCoroutine(EnemyTurn()); }}
To summarize the functionality, create an image and add a "pointer click" event handler component. After that, drag "Combat System" into the field and select the execution function we just wrote.

it's the enemy's turn
Let's implement the enemy's behavior during the move. In a real situation, you may need to use some AI scripts to determine the action. However, for the sake of simplicity, I'll make the enemy attack every time it's their turn.
IEnumerator EnemyTurn(){ // As before, reduce the player's health by a // fixed amount of 10. // You might want to have some // more complex logic here. playerStatusHUD.SetHP(playerStatus, 10); // Play the enemy attack animation by activating the internal enemy animator.GetComponent().SetTrigger("Attack"); Yield return new WaitForSeconds(2); if (playerStatus.health <= 0) { // If player health drops to 0 // we lose the battle... BattleState = BattleState.LOST ; Execution return StartCoroutine(EndBattle()); } else { // If the player's health is still above 0 at the end of the round // It's our turn again! BattleState = BattleState.PLAYERTURN; execution return StartCoroutine(PlayerTurn()); }}
final state
Now let's complete our script by declaring the function responsible for ending the fight. We will consider two cases. The win condition is met when the enemy's health drops to 0. Similarly, when the player's health drops to 0, the failure condition is true and the game ends. All of this will be inside the "EndBattle" function block that we will write next. Again, for simplicity, I'm just going from combat to level. I will do this for both cases using the LoadLevel instance defined above.
IEnumerator EndBattle(){ // Check if we've won if (battleState == BattleState.WIN) { // Maybe you want to display some kind of message // or play a victory tune // Products here return new WaitForSeconds(1); LevelLoader.stance.LoadLevel("TestLevel"); } // if not, check if we've lost // you probably want to show some sort of // 'game over' screen to communicate game failure // to the player else if (battleState == BattleState.LOST) { // ¡ Maybe you want to show some kind of // message or play a sad tune! return return new WaitForSeconds(1); LevelLoader.instance.LoadLevel("TestLevel"); }}
That's all! You have implemented a complete turn-based combat system with smooth transitions from level to level. Future work may include adding more sounds, animations, text messages, artificial intelligence, and anything else you can think of to make combat more engaging.
in conclusion
In this guide, I show one of the many possible ways to implement an alternating combat system. I used two separate scenes: one for the level and one for the arena. To store data that represents the current state of the world, I use scriptable objects. In fact, this structure is ideal for writing and reading data between scenes. I used coroutines to create and run timed combat transition animations. I made some HUD visualizations in the battlefield scene. These are updated every time the character's state changes. The implementation of the combat flow is reduced to the execution of coroutines at the appropriate time. Rounds alternate between player and enemy. When any character's health drops to zero, the battle ends.
reference
Brackey's move fight
How to make great scene transitions in Unity by Brackeys