14 min read

In this article by Ashley Godbold author of book Mastering Unity 2D Game Development, Second Edition, we will start out by laying the main foundation for the battle system of our game. We will create the Heads Up Display (HUD) as well as design the overall logic of the battle system.

The following topics will be covered in this article:

  • Creating a state manager to handle the logic behind a turn-based battle system
  • Working with Mecanim in the code
  • Exploring RPG UI
  • Creating the game’s HUD

(For more resources related to this topic, see here.)

Setting up our battle statemanager

The most unique and important part of a turn-based battle system is the turns. Controlling the turns is incredibly important, and we will need something to handle the logic behind the actual turns for us. We’ll accomplish this by creating a battle state machine.

The battle state manager

Starting back in our BattleScene, we need to create a state machine using all of Mecanim’s handy features. Although we will still only be using a fraction of the functionality with the RPG sample, I advise you to investigate and read more about its capabilities.

Navigate to AssetsAnimationControllers and create a new Animator Controller called BattleStateMachine, and then we can begin putting together the battle state machine. The following screenshot shows you the states, transitions, and properties that we will need:

As shown in the preceding screenshot, we have created eight states to control the flow of a battle with two Boolean parameters to control its transition.

The transitions are defined as follows:

  • From Begin_Battle to Intro
  • BattleReadyset to true
  • Has Exit Timeset to false (deselected)
  • Transition Durationset to 0
  • From Intro to Player_Move
  • Has Exit Timeset totrue
  • Exit Timeset to0.9
  • Transition Durationset to2
  • From Player_Move to Player_Attack
  • PlayerReadyset totrue
  • Has Exit Timeset tofalse
  • Transition Durationset to0
  • From Player_Attack to Change_Control
  • PlayerReadyset tofalse
  • Has Exit Timeset tofalse
  • Transition Durationset to2
  • From Change_Control to Enemy_Attack
  • Has Exit Timeset totrue
  • Exit Timeset to0.9
  • Transition Durationset to2
  • From Enemy_Attack to Player_Move
  • BattleReadyset totrue
  • Has Exit Timeset tofalse
  • Transition Durationset to2
  • From Enemy_Attack to Battle_Result
  • BattleReadyset tofalse
  • Has Exit Timeset tofalse
  • Transition Timeset to2
  • From Battle_Result to Battle_End
  • Has Exit Timeset totrue
  • Exit Timeset to0.9
  • Transition Timeset to5

Summing up, what we have built is a steady flow of battle, which can be summarized as follows:

  • The battle begins and we show a little introductory clip to tell the player about the battle.
  • Once the player has control, we wait for them to finish their move.
  • We then perform the player’s attack and switch the control over to the enemy AI.
  • If there are any enemies left, they get to attack the player (if they are not too scared and have not run away).
  • If the battle continues, we switch back to the player, otherwise we show the battle result.
  • We show the result for five seconds (or until the player hits a key), and then finish the battle and return the player to the world together with whatever loot and experience gained.

This is just a simple flow, which can be extended as much as you want, and as we continue, you will see all the points where you could expand it.

With our animator state machine created, we now just need to attach it to our battle manager so that it will be available when the battle runs; the following are the ensuing steps to do this:

Open up BattleScene.

Select the BattleManager game object in the project Hierarchy and add an Animator component to it.

Now drag the BattleStateMachine animator controller we just created into the Controller property of the Animator component.

The preceding steps attached our new battle state machine to our battle engine. Now, we just need to be able to reference the BattleStateMachine Mecanim state machine from theBattleManager script. To do so, open up the BattleManager script in AssetsScripts and add the following variable to the top of the class:

private Animator battleStateManager;

Then, to capture the configuredAnimator in our BattleManager script, we add the following to an Awake function place before the Start function:

voidAwake(){

battleStateManager=GetComponent<Animator>();

if(battleStateManager==null){

Debug.LogError("NobattleStateMachineAnimatorfound.");

}

 

}

We have to assign it this way because all the functionality to integrate the Animator Controller is built into the Animator component. We cannot simply attach the controller directly to the BattleManager script and use it.

Now that it’s all wired up, let’s start using it.

Getting to the state manager in the code

Now that we have our state manager running in Mecanim, we just need to be able to access it from the code. However, at first glance, there is a barrier to achieving this. The reason being that the Mecanim system uses hashes (integer ID keys for objects) not strings to identify states within its engine (still not clear why, but for performance reasons probably). To access the states in Mecanim, Unity provides a hashing algorithm to help you, which is fine for one-off checks but a bit of an overhead when you need per-frame access.

You can check to see if a state’s name is a specific string using the following:

GetCurrentAnimatorStateInfo(0).IsName(“Thing you’re checking”)

But there is no way to store the names of the current state, to a variable.

A simple solution to this is to generate and cache all the state hashes when we start and then use the cache to talk to the Mecanim engine.

First, let’s remove the placeholder code, for the old enum state machine.So, remove the following code from the top of the BattleManager script:

enum BattlePhase

{

  PlayerAttack,

  EnemyAttack

}

private BattlePhase phase;

Also, remove the following line from the Start method:

phase = BattlePhase.PlayerAttack;

There is still a reference in the Update method for our buttons, but we will update that shortly; feel free to comment it out now if you wish, but don’t delete it.

Now, to begin working with our new state machine, we need a replica of the available states we have defined in our Mecanim state machine. For this, we just need an enumeration using the same names (you can create this either as a new C# script or simply place it in the BattleManager class) as follows:

publicenumBattleState

{

Begin_Battle,

Intro,

Player_Move,

Player_Attack,

Change_Control,

Enemy_Attack,

Battle_Result,

Battle_End

}

It may seem strange to have a duplicate of your states in the state machine and in the code; however, at the time of writing, it is necessary. Mecanim does not expose the names of the states outside of the engine other than through using hashes. You can either use this approach and make it dynamic, or extract the state hashes and store them in a dictionary for use.

Mecanim makes the managing of state machines very simple under the hood and is extremely powerful, much better than trawling through code every time you want to update the state machine.

Next, we need a location to cache the hashes the state machine needs and a property to keep the current state so that we don’t constantly query the engine for a hash. So, add a new using statement to the beginning of the BattleManager class as follows:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

Then, add the following variables to the top of the BattleManager class:

private Dictionary<int, BattleState> battleStateHash = new 
Dictionary<int, BattleState>();

private BattleState currentBattleState;

Finally, we just need to integrate the animator state machine we have created. So, create a new GetAnimationStates method in the BattleManager class as follows:

void GetAnimationStates()

{

  foreach (BattleState state in (BattleState[])System.Enum.
    GetValues(typeof(BattleState)))

  {

    battleStateHash.Add(Animator.StringToHash
      (state.ToString()), state);

  }

}

This simply generates a hash for the corresponding animation state in Mecanim and stores the resultant hashes in a dictionary that we can use without having to calculate them at runtime when we need to talk to the state machine.

Sadly, there is no way at runtime to gather the information from Mecanim as this information is only available in the editor.

You could gather the hashes from the animator and store them in a file to avoid this, but it won’t save you much.

To complete this, we just need to call the new method in the Start function of the BattleManager script by adding the following:

GetAnimationStates();

Now that we have our states, we can use them in our running game to control both the logic that is applied and the GUI elements that are drawn to the screen.

Now add the Update function to the BattleManager class as follows:

voidUpdate()

{

  currentBattleState = battleStateHash[battleStateManager.
    GetCurrentAnimatorStateInfo(0).shortNameHash];

 

  switch (currentBattleState)

  {

    case BattleState.Intro:

      break;

    case BattleState.Player_Move:

      break;

    case BattleState.Player_Attack:

      break;

    case BattleState.Change_Control:

      break;

    case BattleState.Enemy_Attack:

      break;

    case BattleState.Battle_Result:

      break;

    case BattleState.Battle_End:

      break;

    default:

      break;

  }

}

The preceding code gets the current state from the animator state machine once per frame and then sets up a choice (switch statement) for what can happen based on the current state. (Remember, it is the state machine that decides which state follows which in the Mecanim engine, not nasty nested if statements everywhere in code.)

Now we are going to update the functionality that turns our GUI button on and off. Update the line of code in the Update method we wrote as follows:

if(phase==BattlePhase.PlayerAttack){

so that it now reads:

if(currentBattleState==BattleState.Player_Move){

This will make it so that the buttons are now only visible when it is time for the player to perform his/her move. With these in place, we are ready to start adding in some battle logic.

Starting the battle

As it stands, the state machine is waiting at the Begin_Battle state for us to kick things off. Obviously, we want to do this when we are ready and all the pieces on the board are in place.

When the current Battle scene we added, starts, we load up the player and randomly spawn in a number of enemies into the fray using a co-routine function called SpawnEnemies. So, only when all the dragons are ready and waiting to be chopped down do we want to kick things off.

To tell the state machine to start the battle, we simple add the following line just after the end of the forloop in the SpawnEnemies IEnumerator co-routine function:

battleStateManager.SetBool("BattleReady", true);

Now when everything is in place, the battle will finally begin.

Introductory animation

When the battle starts, we are going to display a little battle introductory image that states who the player is going to be fighting against. We’ll have it slide into the scene and then slide out.

You can do all sorts of interesting stuff with this introductory animation, like animating the individual images, but I’ll leave that up to you to play with. Can’t have all the fun now, can I?

Start by creating a new Canvas and renaming it IntroCanvas so that we can distinguish it from the canvas that will hold our buttons. At this point, since we are adding a second canvas into the scene, we should probably rename ours to something that is easier for you to identify.

It’s a matter of preference, but I like to use different canvases for different UI elements. For example, one for the HUD, one for pause menus, one for animations, and so on.

You can put them all on a single canvas and use Panels and CanvasGroup components to distinguish between them; it’s really up to you.

As a child of the new IntroCanvas, create a Panel with the properties shown in the following screenshot. Notice that the Imageoblect’s Color property is set to black with the alpha set to about half:

Now add as a child of the Panel two UI Images and a UI Text. Name the first image PlayerImage and set its properties as shown in the following screenshot. Be sure to set Preserve Aspect to true:

Name the second image EnemyImage and set the properties as shown in the following screenshot:

For the text, set the properties as shown in the following screenshot:

Your Panel should now appear as mine did in the image at the beginning of this section.

Now let’s give this Panel its animation. With the Panel selected, select the Animation tab. Now hit the Create button. Save the animation as IntroSlideAnimation in the Assets/Animation/Clipsfolder.

At the 0:00 frame, set the Panel’s X position to 600, as shown in the following screenshot:

Now, at the 0:45 frame, set the Panel’s X position to 0. Place the playhead at the 1:20 frame and set the Panel’s X position to 0, there as well, by selecting Add Key, as shown in the following screenshot:

Create the last frame at 2:00 by setting the Panel’s X position to -600.

When the Panel slides in, it does this annoying bounce thing instead of staying put. We need to fix this by adjusting the animation curve. Select the Curves tab:

When you select the Curves tab, you should see something like the following:

The reason for the bounce is the wiggle that occurs between the two center keyframes. To fix this, right-click on the two center points on the curve represented by red dots and select Flat,as shown in the following screenshot:

After you do so, the curve should be constant (flat) in the center, as shown in the following screenshot:

The last thing we need to do to connect this to our BattleStateMananger isto adjust the properties of the Panel’s Animator.

With the Panel selected, select the Animator tab. You should see something like the following:

Right now, the animation immediately plays when the scene is entered. However, since we want this to tie in with our BattleStateManager and only begin playing in the Intro state, we do not want this to be the default animation.

Create an empty state within the Animator and set it as the default state. Name this state OutOfFrame. Now make a Trigger Parameter called Intro. Set the transition between the two states so that it has the following properties:

The last things we want to do before we move on is make it so this animation does not loop, rename this new Animator, and place our Animator in the correct subfolder. In the project view, select IntroSlideAnimation from the Assets/Animation/Clips folder and deselect Loop Time. Rename the Panel Animator to VsAnimator and move it to the Assets/Animation/Controllersfolder.

Currently, the Panel is appearing right in the middle of the screen at all times, so go ahead and set the Panel’s X Position to600, to get it out of the way.

Now we can access this in our BattleStateManager script.

Currently, the state machine pauses at the Intro state for a few seconds; let’s have our Panel animation pop in.

Add the following variable declarations to our BattleStateManager script:

public GameObjectintroPanel;

Animator introPanelAnim;

And add the following to the Awake function:

introPanel Anim=introPanel.GetComponent<Animator>();

Now add the following to the case line of the Intro state in the Updatefunction:

case BattleState.Intro:

introPanelAnim.SetTrigger("Intro");

break;

For this to work, we have to drag and drop the Panel into the Intro Panel slot in the BattleManager Inspector.

As the battle is now in progress and the control is being passed to the player, we need some interaction from the user. Currently, the player can run away, but that’s not at all interesting. We want our player to be able to fight! So, let’s design a graphic user interface that will allow her to attack those adorable, but super mean, dragons.

Summary

Getting the battle right based on the style of your game is very important as it is where the player will spend the majority of their time. Keep the player engaged and try to make each battle different in some way, as receptiveness is a tricky problem to solve and you don’t want to bore the player.

Think about different attacks your player can perform that possibly strengthen as the player strengthens.

In this article, you covered the following:

  • Setting up the logic of our turn-based battle system
  • Working with state machines in the code
  • Different RPG UI overlays
  • Setting up the HUD of our game so that our player can do more than just run away

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here