Turn based battle and transition from a game world Unity

 by • Reading Time: 15 minutes

Introduction

In the vast universe of video games one of most popular game mechanics is a turn-based battle system. This philosophical development approach can be found in many examples of games, especially within RPG genre. The original Final Fantasy franchise is a prime example of how turn-based battle system can look like inside a game. In this tutorial I’m going to implement simple, yet customizable turn based battle system.

Table of contents:

  1. The architecture of a turn based battle system
  2. Transition from a game world to a turn based battle arena
  3. Your move! The core of a turn based battle system

1. The architecture of a turn based battle system

We are going to use two different scenes to implement the turn based battle system. The first scene will contain all elements of the level our character currently roams. The second scene is going to be our battle arena that we will transition to. This will happen when our character is touched by an enemy in the level. Alternatively, the battles can be invoked at random whenever player travels the game world. That kind of system would resemble older RPG’s mechanics where player enters the battle without even knowing who’s attacking him.

Final Fantasy original turn based battle system

The scenario in which we enter the battle is going to be dictated by collision between character and enemy. When the new collision is detected a transition animation is started, new battle arena scene loaded and necessary data read. The data will consist of information on enemy as well as player’s combat capabilities such as health, magic points and so on. To correctly position character in the level after the battle, I will also record his last location. Both parties will exchange attacks until one of them runs out of hit points (HP). When that happens the player shall be transitioned back to the level or be presented with ending screen. I will build the entire functionality upon a foundation established in the previous tutorials. Please refer to top-down movement, fighting mechanics and tilemaps system posts for more.

2. Transition from a game world to a turn based battle arena

In this section we are going to transition our character from a level scene to a battle arena scene. All this will happen while the information on character statuses are maintained.

Preserving the world state data

In order to preserve data between two different ‘scenes’ we’re going to take the advantage of a ‘Scriptable Object’ concept. It is a data container implemented in Unity to save large amounts of data independently of class instances. I will write to and read from those objects whenever we switch the scenes during gameplay. Here I’m going to expand on example presented in article on fighting mechanics.

  • right-click inside your ‘Assets’ resources panel and create a new ‘C# Script’
  • name the script a ‘CharacterStatus’ and delete ‘Start()’ and ‘Update()’ functions from it
  • instead of ‘MonoBehaviour’ I’ll make our object to inherit from ‘ScriptableObject’
  • define all the fields we are going to use to describe character status during battle
    • name
    • level
    • maxHealth and currHealth
    • macMana and currMana
  • I’m going to add ‘position’ float array to store last recorded location of character in the level. The reason why I’m not using Vector2 here is because it is not serializable by Unity engine. Serialization is beyond the scope of this tutorial, but if you would like to learn more about it then please refer to official Unity documentation.
  • the last field I’m going to use is the GameObject of a character containing all battle animations. I’ll be able to reference them during the battle.
  • in order to create a new object inside engine I will add a line above the class declaration.

After that, the entire class should look like that:

using 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 characterGameObject;
    public int level = 1;
    public float maxHealth = 100;
    public float maxMana = 100;
    public float health = 100;
    public float mana = 100;

}

If you have followed all the steps above then back in the engine you can now create status objects. Create one for the player, one for enemy placeholder. At this point you can create as many different statuses as many enemies are present inside the level!

Create a Health Status Scriptable Object

Setting battle data

Now that we have the scriptable object, lets define two fields in ‘StatusManager’ script of ‘CharacterStatus’ type. They will hold information on the player and enemy status, who is attacking our player. The enemy status field is a generic placeholder that will only hold information of our antagonist at a given time. That is to say, I will update this field only when enemy touches our character in a level. We are going to do this just before transitioning to battle arena to fight the correct enemy! To ensure that no other enemy is able to attack our player during the already executed transition I’ll use a boolean value.

void OnTriggerEnter2D(Collider2D other)
{
    if(this.playerStatus.health > 0)
    {
        if (other.tag == "Enemy")
        {
            if(!isAttacked)
            {
                isAttacked = true;
                setBattleData(other);                 
                LevelLoader.instance.LoadLevel("BattleArena");
            }            
        }
    }
}

Before entering a battle I’m checking few conditions first:

  • if our character is still alive
  • that the entity we clashed with is an enemy
  • that no transition is already taking place

After that, I’m invoking two functions. One is responsible for filling in the enemy placeholder data and the other for loading a new scene. We’re going to implement them next and start with setBattleData(), which is straightforward.

private void setBattleData(Collider2D other)
{
    // Player Data 
    playerStatus.position[0] = this.transform.position.x;
    playerStatus.position[1] = this.transform.position.y;

    // Enemy Data
    CharacterStatus status = other.gameObject.GetComponent<EnemyStatus>().enemyStatus;
    enemyStatus.charName = status.charName;
    enemyStatus.characterGameObject = status.characterGameObject.transform.GetChild(0).gameObject;  
    enemyStatus.health = status.health;
    enemyStatus.maxHealth = status.maxHealth;
    enemyStatus.mana = status.mana;
    enemyStatus.maxMana = status.maxMana;
}

The highlighted line refers to enemy’s prefab child game object. When enemy is present in the level his ‘Battle Presence’ will be disabled. However, we will use it to spawn, animate and update the enemy status during the battle. In other words, every enemy in the level consists of two ‘personas’. One defines behaviour in outer world and the other during a battle. Add ‘Animator’ component to ‘Battle Presence’ object and define all animation actions your enemy will execute during battle. In the root of enemy prefab add a new script with just a single ‘CharacterStatus’ field and assign your enemy data to it.

Creating 'BattlePresence' of an enemy

Transition to battle arena

Let’s now look into how we are going to actually transition between levels. I’m going to create a new game object and attach a script called ‘LevelLoader’ to it. Since we want to manage the scenes in Unity we have to import necessary library first.

using UnityEngine.SceneManagement;

I want to have only one instance of this object that is going to be available regardless of the scene our character is in. That is to say, I’m going to implement it using Object Oriented Programming (OOP) pattern called Singleton. In addition, I’ll use a built-in function ‘DontDestroyOnLoad()’ to make sure that LevelLoader is going to be created only once for entire game session duration.

#region Singleton
public static LevelLoader instance;

void Awake()
{
    instance = this;
    DontDestroyOnLoad(this.gameObject);
}
#endregion

We are now ready to prepare a range of different transition animations. Let’s define a field referencing an ‘Animator’ component and the duration of the transition we want to use. The creative possibilities are nearly endless and it’s up to your imagination how the transition will look. However, every animation needs to be divided into two parts, namely beginning and ending. The reason for that is the fact that in-between we want to load the scene in the background and create a natural flow. To that end, I’m going to complement the entire mechanism with ‘Coroutine‘ to properly time the execution necessary functions.

public Animator transition;
public float transitionTime = 1f;

public void LoadLevel(string levelName)
{
    StartCoroutine(LoadNamedLevel(levelName));
}

IEnumerator LoadNamedLevel(string levelName)
{
    // Start transition animation
    transition.SetTrigger("Start");

    yield return new WaitForSeconds(transitionTime);

    SceneManager.LoadScene(levelName);

    // End transition animation
    transition.SetTrigger("End");
}

The code should be self-explanatory. First, we trigger the starting animation and wait for a specified amount of time. Second, we are loading the new scene using built-in ‘SceneManager’. Lastly, we execute the ending animation to finish the transition. Both ‘triggers’ are parameters defined in the ‘Animator’ of a given transition we want to use.

Transition animation setup

To define the transition animation start by creating a canvas and making it a child object of ‘LevelLoader’. From this point onward anything under this object can be used as part of the animation. For the sake of this tutorial I’m going to simply drag a giant pixelated circle image from right to left. The starting animation will contain frames where the image travels from right to the centre. The ending animation consist of frames moving the image from the centre to left of the screen. I called them ‘CircleWipe_Start’ and ‘CircleWipe_End’ accordingly.

Starting transition animation

The ‘Animator’ setup is fairly simple.

  • define two parameters of ‘trigger’ type that will be used to start both animations. These are used inside the level loader script we wrote earlier.
  • set the ending animation as a default state
  • make a transition to starting animation
  • in the ‘Inspector’ panel untick the ‘Has Exit Time’ and set ‘Transition Duration(s)’ and ‘Transition Offset’ fields to ‘0’. Use the ‘Start’ trigger as a condition of the transition to take place.
  • make the analogous setup for the transition from starting to ending clip but using the ‘End’ trigger parameter
  • drag & drop the canvas game object into ‘Animator’ field of ‘LevelLoader’ script
Animator setup for the transition

If you’d like to define more transition animations you don’t have to create this setup for each new animator!

  • create an ‘Animator Override Controller’ in your assets resources
  • specify the animator controller with all original transitions
  • select the new starting and ending animation clips, which you wish to swap the old ones with
  • in the canvas object holding the new set of animations, select the the new controller in the ‘Animator’ component
Animator Override Controller setup for transition animations

Next, we’ll look into how to we can control the flow of a battle in a separate scene.

3. Your move! The core of a turn based battle system

In this chapter we’re going to create a completely new scene in which our battle is going to take place. I’ll display the statistics of both parties to the player in a form with heads-up display (HUD’s). The player will attack the enemy when he presses a button at his turn. We then will give the enemy a chance to attack us. The battle shall end with a win or defeat depending on health stats of both parties.

The look and feel of a battle

It is time to work on the battle arena scene where our characters will clash! Start by creating a new scene and call it ‘BattleArena’. We want our two characters to stand in specific positions on the battle field. To that end, I’m going to add two platform game objects with shadow sprites. One will be for the player and one for the enemy we encounter. We will load the ‘BattlePresence’ of our characters at these locations in the scene.

As in any game battle, we have to keep track of our character’s health and magic points. It’s essential to have some kind of numerical display of this information. With this player can take different actions depending on the situation. I have created a prefab that I’ll use to display both health points and mana points of the player and enemy. These consist of simple texts and images of ‘Filled’ type. In Unity the images allow for their gradual fill during gameplay and are perfect candidates to represent a health bar.

Filled type of image to represent health

The health and mana points HUDs are ready. However, in order to control their states we need to have access to their fields. We could reference these fields in a script responsible for managing the battle flow, but I’ll make them part of a parent prefab. That way we’ll be able to separate the logic of calculating the battle values from their display. In other words, this will result in a more modular and cleaner solution. Add a new script to the parent prefab and name it ‘StatusHUD’. Inside a script create public fields that will reference all texts and images. These will be used to display status of our characters.

Creating battle Status HUDDisplay

Updating HUDs with a script

Inside a script we are going to write a function that takes our ‘status’ object we have defined earlier as an argument. I’ll use it to update all relevant HUD’s in the scene that we just created. In order to update our progress bars I need to provide a value in a range between 0 and 1. To do this I’ll calculate the percentage values of current health stats in relation to their maximum amounts. I’ll then normalize them so that their values are always between 0 and 1.

public void SetStatusHUD(CharacterStatus status)
{
        float currentHealth = status.health * (100 / status.maxHealth);
        float currentMana   = status.mana * (100 / status.maxMana);

        statusHPBar.fillAmount = currentHealth / 100;
        statusHPValue.SetText(status.health + "/" + status.maxHealth);

        statusMPBar.fillAmount = currentMana / 100;
        statusMPValue.SetText(status.mana + "/" + status.maxMana);
}

The HUD’s most likely are going to be changed frequently during the battle. Because of that I’ll add one more function that is responsible for updating the ‘health’ bar only. I’m doing this for efficiency as I don’t want to change all HUD elements every time individual stats need to be updated. To make things a little bit more interesting, I’ll ‘gradually’ fill a given status bar and its corresponding text over time. To this end I’ll once again use, you guessed it, coroutines!

public void SetHP(CharacterStatus status, float hp)
{
    StartCoroutine(GraduallySetStatusBar(status, hp, false, 10, 0.05f));
}

IEnumerator GraduallySetStatusBar(CharacterStatus status, float amount, bool isIncrease, int fillTimes, float fillDelay){
    float percentage = 1 / (float) fillTimes;

    if (isIncrease)
    {
        for (int fillStep = 0; fillStep < fillTimes; fillStep++)
        {
            float _fAmount = amount * percentage;
            float _dAmount = _fAmount / status.maxHealth;
            status.health += _fAmount;
            statusHPBar.fillAmount += _dAmount;
            if (status.health <= status.maxHealth)
                statusHPValue.SetText(status.health + "/" + status.maxHealth);
            yield return new WaitForSeconds(fillDelay);
        }
    }
    else
    {
        for (int fillStep = 0; fillStep < fillTimes; fillStep++)
        {
            float _fAmount = amount * percentage;
            float _dAmount = _fAmount / status.maxHealth;
            status.health -= _fAmount;
            statusHPBar.fillAmount -= _dAmount;
            if (status.health >= 0)
                statusHPValue.SetText(status.health + "/" + status.maxHealth);

            yield return new WaitForSeconds(fillDelay);
        }
    }
}

For the coroutine to work we have to calculate a ‘percentage of a percentage’ of a given stat we want to either increase or decrease. This mainly depends on the ‘number of steps’ we need to take over time in order to change the HUD elements to a given value. In the code above I’m doing this in the following lines:

float percentage = 1 / (float) fillTimes;

[...]

float _fAmount = amount * percentage;
float _dAmount = _fAmount / status.maxHealth;

Controlling the battle flow

We are now going to write the very core of turn-based battle system. I’m going to focus on establishing a foundation upon which you can easily expand on. Giving a twist to existing game mechanics is highly encouraged. Let’s start by creating an empty game object called ‘BattleSystem’. Add a new script to it called ‘Battle System Manager’. Inside of it I’m going to define an enum type data structure holding all possible states of a battle. I’ll place it outside the class.

public enum BattleState { START, PLAYERTURN, ENEMYTURN, WIN, LOST }

Before I start coding I’m going to need few references about the battle.

  • our characters stats (CharacterStatus)
  • the corresponding HUD’s elements (StatusHUD)
  • platforms at which they are going to be spawned on (Transform)
  • characters battle animations (GameObject)
  • the current state battle is in (BattleState)
  • whether player has selected an action (boolean). This will prevent player to select a given action repeatedly during his turn.
private GameObject enemy;
private GameObject player;

public Transform enemyBattlePosition;
public Transform playerBattlePosition;

public CharacterStatus playerStatus;
public CharacterStatus enemyStatus;

public StatusHUD playerStatusHUD;
public StatusHUD enemyStatusHUD;

private BattleState battleState;

private bool hasClicked = true;

Assign all those fields with correct data by dragging and dropping the assets in the editor.

Assigning battle system's fields with references

In The Start() I’m going to set the initial battle state and call a function that sets the battle in gradual way. I will fade in the characters before switching to player turn.

void Start()
{
    battleState = BattleState.START;
    StartCoroutine(BeginBattle());
}

IEnumerator BeginBattle()
{
    // spawn characters on the platforms
    enemy  = Instantiate(enemyStatus.characterGameObject, enemyBattlePosition); enemy.SetActive(true);
    player = Instantiate(playerStatus.characterGameObject.transform.GetChild(0).gameObject, playerBattlePosition); player.SetActive(true);
        
    // make the characters sprites invisible in the beginning
    enemy.GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0);
    player.GetComponent<SpriteRenderer>().color = new Color(1, 1, 1, 0);

    // set the characters stats in HUD displays
    playerStatusHUD.SetStatusHUD(playerStatus);
    enemyStatusHUD.SetStatusHUD(enemyStatus);

    yield return new WaitForSeconds(1);

    // fade in our characters sprites
    yield return StartCoroutine(FadeInOpponents());

    yield return new WaitForSeconds(2);

    // player turn!
    battleState = BattleState.PLAYERTURN;
    
    // let player select his action now!    
    yield return StartCoroutine(PlayerTurn());
}

The code above is pretty straightforward. First we spawn the ‘BattlePresence’ of our characters on their platforms. I’m making their sprites temporarily transparent so that I can fade them in later on. It gives the game that extra nice visual that can be easily achieved with a coroutine. This is entirely optional and in the next optional section I show how you can do that. After that, we give a player a chance to execute his desired action first. In our case it’s going to be an attack action.

Fade in characters in their battle positions (optional)

The ‘FadeInOpponents’ function job is to gradually fade in our characters prior to battle. To do this, I’m manipulating the alpha value of their sprites over a span of few seconds. Similarly to how we are updating the health bars, I’m calculating the percentage by which I need to increase the opacity of a sprite at each time step. Therefore, the number of steps provided as an argument determines how quickly the characters sprites will be fully opaque.

IEnumerator FadeInOpponents(int steps = 10)
{
    float totalTransparencyPerStep = 1 / (float) steps;

    for (int i=0;i<steps;i++)
    {
        setSpriteOpacity(player, totalTransparencyPerStep);
        setSpriteOpacity(enemy,  totalTransparencyPerStep);
        yield return new WaitForSeconds(0.05f);
    }
}

private void setSpriteOpacity(GameObject ob, float transPerStep)
{
    Color currColor = ob.GetComponent<SpriteRenderer>().color;
    float alpha = currColor.a;
    alpha += transPerStep;
    ob.GetComponent<SpriteRenderer>().color = new Color(currColor.r, currColor.g, currColor.b, alpha);
}

Player’s turn

The implementation of a player’s turn is encapsulated in three tightly coupled functions. Firstly, we give the player a chance to act by releasing a blockade imposed by ‘isClicked’ boolean. Secondly, we define the logic of a button press. We make sure that it can be pressed only once per turn. Thirdly, we execute the animation and logic of the attack.

IEnumerator PlayerTurn()
{
    // probably display some message 
    // stating it's player's turn here
    yield return new WaitForSeconds(1); 

    // release the blockade on clicking 
    // so that player can click on 'attack' button    
    hasClicked = false;
}
public void OnAttackButtonPress()
{
    // don't allow player to click on 'attack'
    // button if it's not his turn!
    if (battleState != BattleState.PLAYERTURN)
        return;

    // allow only a single action per turn
    if(!hasClicked) {
        StartCoroutine(PlayerAttack());
        
        // block user from repeatedly 
        // pressing attack button  
        hasClicked = true;
    }
}

IEnumerator PlayerAttack()
{
    // trigger the execution of attack animation
    // in 'BattlePresence' animator
    player.GetComponent<Animator>().SetTrigger("Attack");

    yield return new WaitForSeconds(2);
    
    // decrease enemy health by a fixed
    // amount of 10. You probably want to have some
    // more complex logic here.
    enemyStatusHUD.SetHP(enemyStatus, 10);

    if (enemyStatus.health <= 0)
    {
        // if the enemy health drops to 0 
        // we won!
        battleState = BattleState.WIN;
        yield return StartCoroutine(EndBattle());
    }
    else
    {
        // if the enemy health is still
        // above 0 when the turn finishes
        // it's enemy's turn!
        battleState = BattleState.ENEMYTURN;
        yield return StartCoroutine(EnemyTurn());
    }

}

To sum up the functionality, create an image and add ‘Pointer Click’ event trigger component. After that, drag the ‘Battle System’ into the field and select the execution function we just wrote.

Pointer Click event trigger

Enemy’s turn

Let’s implement enemy’s behaviour during his turn. In a real case scenario you probably would want to use some AI script to determine the action. However, to keep things simple I’m going to make our enemy execute attack every time it’s his turn.

IEnumerator EnemyTurn()
{
    // as before, decrease playerhealth by a fixed
    // amount of 10. You probably want to have some
    // more complex logic here.
    playerStatusHUD.SetHP(playerStatus, 10);
 
    // play attack animation by triggering
    // it inside the enemy animator
    enemy.GetComponent<Animator>().SetTrigger("Attack");

    yield return new WaitForSeconds(2);

    if (playerStatus.health <= 0)
    {
        // if the player health drops to 0 
        // we have lost the battle...
        battleState = BattleState.LOST;
        yield return StartCoroutine(EndBattle());
    }
    else
    {
        // if the player health is still
        // above 0 when the turn finishes
        // it's our turn again!
        battleState = BattleState.PLAYERTURN;
        yield return StartCoroutine(PlayerTurn());
    }
}

Ending conditions

Let’s now finish up our script by declaring a function that will be responsible for ending the battle. There are two case scenarios that we are going to take into consideration. The winning condition is fulfilled when the enemy’s health drops to 0. Analogically, when player’s health drops to 0 the losing condition is met and the game is over. All this shall be within the block of ‘EndBattle’ function that we are going to write next. Again, for the sake of simplicity I’m simply going to go transition from a battle back to the level. I’ll do this for both cases using the previously defined LoadLevel instance.

IEnumerator EndBattle()
{
    // check if we won
    if (battleState == BattleState.WIN)
    {
        // you may wish to display some kind
        // of message or play a victory fanfare
        // here
        yield return new WaitForSeconds(1); 
        LevelLoader.instance.LoadLevel("TestLevel");
    }
    // otherwise check if we lost
    // You probably want to display some kind of
    // 'Game Over' screen to communicate to the 
    // player that the game is lost
    else if (battleState == BattleState.LOST)
    {
       // you may wish to display some kind
       // of message or play a sad tune here!
       yield return new WaitForSeconds(1); 

       LevelLoader.instance.LoadLevel("TestLevel");
    }
}

That’s it! You have implemented a fully fledged turn based battle system with a proper transition from a level. The future work may involve adding more sounds, animations, text messages, AI and whatever you can think of to make battles even more engaging!

Conclusion

In this tutorial I showed one of many possible ways of implementing a turn-based battle system. I’ve used two separate scenes: one for the level and one for the battle arena. In order to preserve data representing the current state of the world I’ve took the advantage of scriptable objects. Indeed, this construct is perfect for writing and reading the data between the scenes. I’ve created and timely executed the battle transition animations using the coroutines. I made a few HUD’s displays in battle arena scene. These were updated every time characters statuses changed. The implementation of a battle flow was narrowed down to the execution of couroutines at appropriate times. The turns were interchangeably taken by both a player and enemy. The battle finishes the moment when either character’s health drops to zero.

References

Turn based combat by Brackeys
How to make AWESOME Scene Transitions in Unity by Brackeys