Selecting battle targets in a grid-based game

 by • Reading Time: 12 minutes

Introduction

In this tutorial I’m going to develop a system that allows the player selecting different battle targets. It is a combination of interaction system and turn-based battle system. In many ways it extends the functionality of a battle system because we are equipping the player with a tool to decide which target to attack or heal in the next turn. This makes it more advanced and enforces the player to think about his next move. The inspiration for developing it came from the old school tactic games such as Shining Force II (Sega Genesis) and Vandal Hearts (PS1).

Selecting battle targets in a Shining Force 2 game

Table of contents:

  1. The concept behind the “Selection Mode”
  2. The core of the battle system – the “Battle Manager”
  3. Expanding the “Menu Button” script for selecting battle targets
  4. Entering the “Selection Mode” to select battle targets

1. The concept behind the “Selection Mode”

I’ll start by explaining the concept behind the feature we’re trying to build. When the player enters a turn-based battle he is going to be presented with a set of available options. Upon selecting an option the script needs to switch to a mode enabling the player choosing the target. Once the player confirms his target, the character carries out a specified action. This can be perceived as a 2-stage process in which we first select the action and then select the target. Of course not all actions may require specifying the target. It is up to developer to define the game’s behaviour under such cases.

When the player transitions from one mode to another, we have to assign different actions to the buttons. For example, when we are in the “menu mode” then the arrow buttons will be used to navigate up and down through the options. However, in case of “selection mode” the player will be able to choose his targets standing next to him. This means that the right and left arrow need to be activated as well. The same goes for the “action button”, which we need to “confirm” our choices in a different manner depending on the “mode” we’re in.

2. The core of the battle system – the “Battle Manager”

There needs to be an order in which specific scripts will execute their functions. To keep things relatively simple I’ll focus on implementing an option to attack enemies that are within character’s range. The option itself will be one of the many displayed in a compact scrollable menu. This will form a foundation upon which you can expand on to develop your own interesting battle mechanic. I’ll start by creating a “BattleManager” script that will contain the logic controlling the flow of a battle. I’ll initialise it using the singleton pattern since we are going to need only one instance of it.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

public class BattleManager : MonoBehaviour
{
    #region Singleton
    public static BattleManager instance;

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(this);
        }
        else
        {
            Destroy(this.gameObject);
        }
    }
    #endregion Singleton
}

Line 5 contain all possible states and we will use them to control the flow of battles. At this point we need to create few fields and reference game objects that are relevant to our functions. However, let’s first quickly add another enum that will hold all possible actions we can choose during our turn. These will be ‘ATTACK’, ‘MAGIC’ and ‘ITEM’ respectfully. I need to define this right after the ‘BattleState’ enum and before the class declaration. I’ll use the additional ‘PANEL’ constant whenever I want to open another menu panel when selecting a given option (e.g. items panel).

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum BattleMenuOption { PANEL, ATTACK, MAGIC, ITEM } 
public enum BattleState  { START, PLAYERTURN, ENEMYTURN, WIN, LOST }

public class BattleManager : MonoBehaviour
{
...
}

The essential fields and references

For selecting battle targets I’ll need a list of enemies our brave hero encountered on his journey, the details of our character and boolean variables controlling player’s selection.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum BattleMenuOption { PANEL, ATTACK, MAGIC, ITEM } 
public enum BattleState  { START, PLAYERTURN, ENEMYTURN, WIN, LOST }

public class BattleManager : MonoBehaviour
{

    public bool      hasClicked = false;
    public bool      isSelectionMode = false, onSelectionModeEnabled = false, keyDown = false;          

    BattleState battleState;
    BattleMenuOption   lastBattleMenuOption; 
       
    public GameObject player;
    public GameObject enemy, lastChoiceEnemy;      
    public List<GameObject> enemiesList;            

    ContactFilter2D    contactFilter = new ContactFilter2D();
    List<RaycastHit2D> results       = new List<RaycastHit2D>();
    public LayerMask   enemiesLayer;

    Singleton

}

I’ll now explain the meaning and purpose of all the declared fields:

  • hasClicked is going to be used to determine whether the player has confirmed the choice at every step during the process
  • isSelectionMode will check if we are now in “Selection Mode” and need to select the target of our action
  • onSelectionModeEnabled is going to be used to initialize certain variables right after the selection mode is activated.
  • keyDown, just like in case of scrollable menu, will be used to ensure proper selection behaviour.
  • BattleState and lastBattleMenuOption will hold the information on the current battle state and player’s selection to execute mapped action functions at his turn.
  • player and enemy will be used to reference both our character and the enemy we have selected. The lastChoiceEnemy will hold the most recent selected enemy and the purpose of this variable will become clearer later on.
  • enemiesList will contain references of all adversaries of our hero in a given battle.
  • contactFilter, results and enemiesLayer are going to be used to perform a raycast in order to check if an enemy is within our range in a given direction.

Depending on the structure of your game, you may wish to place different components across multiple scripts. For instance, you may have a script somewhere that its sole responsibility is capturing player’s input and communicate it to other scripts. However, for the sake of this tutorial I’m going to place all the logic inside the BattleManager. I’ll do so to illustrate how various aspects can be achieved. This by no means implies you have to stick to this structure. Quite the contrary, I’m encouraging careful delegation of different responsibilities to specific classes.

The “delegate” listener functions

In order to execute the correct methods when user confirms his selection, I’ll have to define few delegates. I already explained how they can be used in the previous article. In a nutshell, they can be treated as callback functions triggered by certain events occurring at runtime. In our case, we need a callback for when the player selects an option from the menu and when he confirms his target.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum BattleMenuOption { PANEL, ATTACK, MAGIC, ITEM } 
public enum BattleState  { START, PLAYERTURN, ENEMYTURN, WIN, LOST }

public class BattleManager : MonoBehaviour
{

    public bool      hasClicked = false;
    public bool      isSelectionMode = false, onSelectionModeEnabled = false, keyDown = false;          

    BattleState battleState;
    BattleMenuOption   lastBattleMenuOption; 
       
    public GameObject player;
    public GameObject enemy, lastChoiceEnemy;      
    public List<GameObject> enemiesList;            

    ContactFilter2D    contactFilter = new ContactFilter2D();
    List<RaycastHit2D> results       = new List<RaycastHit2D>();
    public LayerMask   enemiesLayer;

    public delegate void OnBattleMenuSelectionCallback();
    public OnBattleMenuSelectionCallback onBattleMenuSelectionCallback;
    public delegate void OnBattleSelectionModeConfirmCallback();
    public OnBattleSelectionModeConfirmCallback onBattleSelectionModeConfirmCallback; 

    Singleton

}
      

It is worth keeping in mind that there can be some downsides of using delegates if we have hundreds or even thousands of options in our menu. However, since we are going to maintain only a limited set of available options this shouldn’t impact the performance in our case.

Initialization of the script

We have declared all the fields but we still need to initialize them in the Start() method. I will initialize the enemies list and set up a filter for raycast to detect only enemies using the layer mask. Furthermore, I’ll subscribe a method to onBattleSelectionModeConfirmCallback delegate that will be called every time player is inside the ‘Selection Mode’ and confirms his target.

void Start()
{
    enemiesList = new List<GameObject>();
    contactFilter.SetLayerMask(enemiesLayer);                                               
    contactFilter.useLayerMask = true;

    onBattleSelectionModeConfirmCallback += ConfirmSelectionModeChoice;   
}

The ConfirmSelectionModeChoice is simply a function that does a bit of a clean up after we have selected our target.

void ConfirmSelectionModeChoice()
{
    if(isSelectionMode)
    {
        onSelectionModeEnabled = false;
        isSelectionMode = false;
        hasClicked = true;
    }
}

I’m going to expand this function later on but right now let’s move on to capturing the input coming from the player!

Capturing player’s input to make strategic battle choices

I’m assuming you are building a game for a touchscreen mobile phone and that you already have buttons capturing the player’s input. Instead of using the directional arrow buttons to move the character around we now want to make different options selections. Thus when we enter a battle the purpose of these buttons will change. This time we are going to use their input to make our choices. We can easily control that with a single boolean variable telling our script whether we are inside a battle or not.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

...

public class BattleManager : MonoBehaviour
{

   ...

   public int VerticalMovement, HorizontalMovement;
   
   void Update() {

        // we are not in battle so we can move our character around levels!
        if (!isInBattle)
        {
            if (isPressUp) movement.y = 1;
            if (isPressDown) movement.y = -1;
            if (isPressLeft) movement.x = -1;
            if (isPressRight) movement.x = 1;
            if (!isPressUp &amp;&amp; !isPressDown &amp;&amp; !isPressLeft &amp;&amp; !isPressRight) movement = Vector2.zero;
        } 
        // we are inside a battle so we need to make options selections inside our menus!
        else if(isInBattle) 
        {
            if (isPressUp)   VerticalMovement = 1; 
            if (isPressDown) VerticalMovement = -1;
            if (isPressRight) HorizontalMovement = 1;
            if (isPressLeft) HorizontalMovement = -1;
            if (!isPressUp &amp;&amp; !isPressDown &amp;&amp; !isPressLeft &amp;&amp; !isPressRight)
            {
                VerticalMovement = 0;
                HorizontalMovement = 0;
            }
        }
   }
   
}

The code should be self-explanatory. I capture the input from the player and assign different values to the corresponding fields. How you enter the battle is entirely up to you. It can occur when the character touches the enemy, after certain amount of time passes or purely at random. However, once that happens make sure to set isInBattle bool to true to capture the player’s input correctly. Furthermore, make sure to add all of your adversaries references to enemiesList to register them for a battle!

The functionality of a “confirm” button

I already wrote a functionality of an action button. Now, I’ll use it to define the logic of a “confirm” button. To that end, I need to modify its code a little to accommodate different battle states and modes.

public void onPressAction()
{
    if(isInBattle)
    {
        if (!BattleManager.instance.hasClicked &amp;&amp; !BattleManager.instance.isSelectionMode)
            BattleManager.instance.onBattleMenuSelectionCallback.Invoke();              
        else if (!BattleManager.instance.hasClicked &amp;&amp; BattleManager.instance.isSelectionMode)
            BattleManager.instance.onBattleSelectionModeConfirmCallback.Invoke();

    }
    else if (!isInBattle)
    {
        ...
    }    
}

Every time the player enters the battle and presses the “action/submit” button it will check hasClicked and isSelectionMode boolean variables we specified earlier. Depending on their values it will invoke a specific callback method that we also declared in previous subsection. I’ve already touch-based the function serving the onBattleSelectionModeConfirmCallback. At this point you may wish to expand on the logic of the that method. To keep it short, I’ll check the value of the lastBattleMenuOption and call the appropriate method.

void ConfirmSelectionModeChoice()
{
    if(isSelectionMode)
    {
        onSelectionModeEnabled = false;
        isSelectionMode = false;
        hasClicked = true;

        if (lastBattleMenuOption == BattleMenuOption.ATTACK)
        {
            // Perform Attack, play animation etc.
            Attack();
        } 
    }
}

However, I still need to write a function that will serve the onBattleMenuSelectionCallback event. For that I’ll have to modify the MenuButton script contents.

3. Expanding the “Menu Button” script for selecting battle targets

If you have followed my tutorial on creating scrollable menus you should already have the MenuButtonController and MenuButton scripts. We need to extend the latter to trigger the “confirm” action every time the player confirms his menu selection. In other words, when onBattleMenuSelectionCallback callback is invoked.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MenuButton : MonoBehaviour
{

    ...

    static bool isPressedAction = false;

    // for setting up specific battle option
    public BattleMenuOption battleMenuOption;
    
    void Start()
    {
        BattleManager.instance.onBattleMenuSelectionCallback += TriggerAction;
    }

    void TriggerAction()
    {
        isPressedAction = true;
    }

    void Update()
    {
        ...
    }

}

The battleMenuOption is a field that holds the information about the action assigned to a specific menu option on our list.

Assignment of battle menu option for selecting battle targets

The TriggerAction() function is going check whether the player actually confirmed his selection. One very important thing to note in the above listing is that the isPressedAction is declared static. The reason for this is that I assigned the MenuButton script to every single position on our menu list. That means we want to make sure it belongs to our class type rather than every individual object instance. By doing so I’m ensuring that only a single option is being selected at one time and not all possible options at once!

Confirming the menu option selection

All that’s left in the MenuButton script is to modify the Update() method so that it incorporates the latest changes.

void Update()
{
    if(menuButtonController.index == thisIndex)
    {
        animator.SetBool("selected", true);
        if(isPressedAction)
        {
            animator.SetBool("pressed", true);

            // if other panel needs to be opened upon selecting this option
            if(menuPanelToOpen != null)
            {
                menuButtonController.gameObject.SetActive(false);
                menuPanelToOpen.SetActive(true);
            } 
            else
            {
        
        BattleManager.instance.ExecuteOption(battleMenuOption);
            }

            isPressedAction = false;
        } 
        else if(animator.GetBool("pressed"))
        {
            animator.SetBool("pressed", false);
            //animatorFunctions.disableOnce = true;
        }
    } else
    {
        animator.SetBool("selected", false);
    }
}

Please take note of line 6 and 22 to see how the isPressedAction boolean now controls the “confirmation” of the player’s selection. Line 19 contains a reference to a function back in our BattleManager that is to be triggered when selection is confirmed. I’ll write it next!

4. Entering the “Selection Mode” to select battle targets

Let’s now write a function that will be responsible for carrying out the action. Inside a BattleManager script create a function that determines if it’s a player’s turn and sets the global fields accordingly.

public void ExecuteOption(BattleMenuOption battleMenuOption)
{
    if (battleState != BattleState.PLAYERTURN)
        return;

    if (!hasClicked)
    {
        HUDManager.SetBattleMenu(false);
        lastBattleMenuOption = battleMenuOption;
        isSelectionMode = true;
    }
}

On line 8 I’m turning on the ‘battle menu’ panel by invoking appropriate function in the HUDManager script. It’s a relatively simple method that only requires a reference to the Panel that needs to be activated.

using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class HUDManager : MonoBehaviour
{
    ...
    
    public void SetBattleMenu(bool isActive)
    {
 BattleScreen.transform.GetChild(0).gameObject.SetActive(isActive);
    }
}

Please refer to my previous article to find out more about HUDManager script. Now that we have confirmed our choice we should enter the “Selection Mode”. To do this, I’ll write a block of code inside the Update() method of a BattleManager script. In order to make the code more maintainable and clean, I’ll help myself with few helper methods.

void Update()
{
    ...

    if (isInBattle)
    {
        if (isSelectionMode)
        {
            // on initializing "Selection Mode"
            if(!onSelectionModeEnabled)
            {
                // Make the cursor visible
                player.GetComponent<PlayerMovement>().lastDirectionPoint.transform.GetChild(0).gameObject.SetActive(true);

                // Face the first enemy on the list
                enemy = enemiesList[0];  
                FaceEnemy(enemy);

                onSelectionModeEnabled = true;
            }

            UpdateSelectionChoice();

            UpdateCursorLocation(enemy);
        }
    }

}

Few lines in the above code require explanation.

  • Line 13: here I’m making the “cursor” visible by enabling a child component of the lastDirectionPoint. I’ve used it when I wrote a script for interacting with different objects in the scene.
  • Lines 16 and 17: here I’m getting the very first registered enemy in the battle and assigning him to the currently selected one. After that, I’m facing the character towards him with the FaceEnemy() helper method.
  • Line 22: on this line I update the target selection.
  • Line 23: here I’m updating the position of the lastDirectionPoint and the cursor that is currently set to be its child component.

I still need to write those individual helper methods to wrap up the “Selection Mode” functionality.

The helper methods supporting the process of selecting battle targets

Let’s now write the helper functions we have used in the Update() method. I’l start with FaceEnemy() since it is quite straightforward.

void FaceEnemy(GameObject aEnemy)
{
    lastChoiceEnemy = aEnemy;
    Vector3 enemyDirection = (aEnemy.transform.position - player.transform.position).normalized;
    player.GetComponent<Animator>().SetFloat("Last_Horizontal", enemyDirection.x);
    player.GetComponent<Animator>().SetFloat("Last_Vertical", enemyDirection.y);
}

This function is responsible for facing the character into the direction of the currently selected enemy. Firstly I’m calculating the directional vector so that it points towards the enemy. Secondly, I’m using its x and y values to set the Last_Horizontal and Last_Vertical parameters of the player’s Animator component. You can read more on those parameters in my first article. The lastChoiceEnemy is to hold the most recent “valid” enemy reference so I’m assigning the currently selected enemy to it. This is going to prevent a situation in which the enemy variable is assigned with null value when the player selects a direction in which no enemy stands.

Let’s now move on to a helper method responsible for updating the cursor position. As in case of the above method, this one is also relatively simple. Its main task is to update the position of the cursor once the new enemy is selected from the list.

void UpdateCursorLocation(GameObject aEnemy)
{
    if(aEnemy != null)
    {
        Vector3 lastDirectionPoint = player.GetComponent<PlayerMovement>().lastDirectionPoint.position;
        if (Vector3.Distance(lastDirectionPoint, aEnemy.transform.position) > .001f)
        {
            player.GetComponent<PlayerMovement>().lastDirectionPoint.position = Vector3.MoveTowards(lastDirectionPoint, aEnemy.transform.position, 8 * Time.deltaTime);
        }
    }
}

Once again, lines 5 and 8 contain a reference to the lastDirectionPoint I used quite extensively when I wrote a script for detecting ‘interactable’ objects inside a level. One more helper method remains. I’ll use it to detect an enemy in a direction specified by the player. However, it is also important to run a check if the detected enemy exists inside our enemiesList list. This will ensure that we are dealing with an enemy we’re currently battling.

The main helper method for selecting battle targets

When the player is in the “Selection Mode”, every time he pushes a directional arrow button a ray is cast in the corresponding direction. In order to determine which direction player wants to check I’m using the VerticalMovement (for up and down) and HorizontalMovement (for left and right) integers I’ve declared earlier. The GetRayCastResult is yet another helper method that will perform a raycast and return an enemy reference that was detected in a given direction.

void UpdateSelectionChoice()
{

    if (playerController.VerticalMovement != 0)
    {
        if (!keyDown)
        {
            // Down
            if (playerController.VerticalMovement < 0)
            {
                enemy = GetRayCastResult(Vector3.down);
            }
            // Up
            else if (playerController.VerticalMovement > 0)
            {
                enemy = GetRayCastResult(Vector3.up);
            }

            keyDown = true;
        }
    }
    else if (playerController.HorizontalMovement != 0)
    {
        if (!keyDown)
        {
            // Left
            if (playerController.HorizontalMovement < 0)
            {
                enemy = GetRayCastResult(Vector3.left);
            }
            // Right
            else if (playerController.HorizontalMovement > 0)
            {
                enemy = GetRayCastResult(Vector3.right);
            }

            keyDown = true;
        }
    }
    else
    {
        keyDown = false;
    }

    if (enemy == null) enemy = lastChoiceEnemy;

    FaceEnemy(enemy);

}

On line 45 I’m stating that if no enemy is found in a direction pointed by the player I want to use the last “valid” detected enemy as the current one. The GetRayCastResult is yet another helper method, which performs a ray cast to detect enemy standing next to the player in a given direction. It also takes care of checking the detected enemy against records held in enemiesList. If the ray intersects an enemy that was previously registered in the battle then its reference is returned.

GameObject GetRayCastResult(Vector3 direction)
{
    GameObject _enemy = null;
    Vector3 rayOriginPosition = player.transform.position + new Vector3(0, 0.5f, 0);
    Physics2D.Raycast(rayOriginPosition, direction, contactFilter, results, 1f);

    if (results.Count > 0)
    {
        foreach (RaycastHit2D result in results)
        {
            if (result.collider.tag.Equals("Enemy")) {
                _enemy = result.collider.gameObject;
                break;
            }
        }
    }

    if(_enemy != null)
    {
        _enemy = enemiesList.Find(obj => obj.name == _enemy.name);      
    }

    return _enemy;
}

If you have managed to reach this point then congratulations! You have now implemented a core of a system allowing the player selecting battle targets. This is how it may look like:

Conclusion

In this tutorial I have expanded on the scrollable menu I’ve presented in a previous article. I added a new feature in a form of “selection mode” that allows player to choose target of the action. By doing so I introduced a 2-stage process that resulted in a more advanced gaming mechanics. I created a “Battle Manager” script which controls the flow a battle and tracks changes to the state of its participants. Furthermore, I’ve added delegate functions to listen to events occurring during the encounters. I went for this type of solution in order to compose an expandable system for selecting battle targets.