Changing the Initiative system in the 2.5D Turn-based course

Hello all, it’s been some time since I asked for help on here, but I’ve run into a snag while working on my project. Now my goal is to change the initiative system to a bit more dynamic than comparing the initiative stat of the party versus the initiative stat of the enemy party and flipping between the two teams each turn to make an initiative system more akin to the timer system of some of the Final Fantasy games (The original FF4 through FF7 to be precise as seen below)


where each party member/enemy has a timer bar that fills up over time and when the bar is full the fastest unit may take their turn then their timer is reset at the end of their respective turns and that timer is linked to the initiative stat that will determine how quickly the bar fills over time.

This will allow for a turn-based system where different attacks and spells can either slow down the party/enemy timers during combat.

Now the closest tutorial I’ve found was a video series here. However, the series is quite old (around 7 years) so it might be difficult to translate into the more modern 2.5D series.

So, my main question is how should I approach this? I’ve added a timer bar to the BattleVisual.cs script,

[SerializeField] private Slider progressbar; 
private float currentCooldown = 0f; //<- timer bar fill
private float maxCooldown = 5f; //<- max time to fill

void Awake()
{
   progressbar.maxValue = maxCooldown;
   progressbar.value = currentCooldown;
}
private void Update() 
{
   UpdateProgressBar();
}
public void UpdateProgressBar()
{
    //if(!battleSystem.isPaused) <- added public bool isPaused to BattleSystem script
     //{
            currentCooldown = currentCooldown += /* insert unit initiative stat * */ Time.deltaTime ;
            progressbar.value = currentCooldown;

            if(currentCooldown >= maxCooldown)
            {
                Debug.Log("Unit goes first!");
                //battleSystem.isPaused = true;
                
            }
    //}
}
public void ResetTimer()
{
      progressbar.value = 0;
}

but I don’t know how to get the unit’s initiative stat or how to approach the kind of system envisioned. I thought about making a separate “currentBattler” list in our BattleSystem.cs script and then adding units to said list from the battle visual, checking if the unit is IsPlayer or not, the player then chooses an action then the unit is removed from the “currentBattler” list and reset the unit’s timer.

Am I on the right track with this logic? Or is there a more efficient way to go about this?

Thank you for your time.

Do you use the same Unity version?

I am currently using Unity Version 2023.2.20f1

Make it the same version as the lecture.
Make it the latest version later.

I haven’t done this course but I have thought of a system like this for one of my games before. I never implemented it, but there are a few issues you need to think about like when all the battlers are in cooldown and no-one is ready to attack your player is just going to sit there and look at a screen with nothing to do. You may want to speed up all the timers until one becomes ready. Give me some time to think about it and I’ll get back to you. I need to check what the course is doing and what can be done to achieve this


In the course the battle order is determined as soon as the battle starts. This is no longer valid because this battle order is now dynamic and changes as the battle progresses, eg. you cast a ‘Slow’ spell that reduces the speed of the target’s timer.
The game also loops through all the battlers and executes them in sequence. This is also not valid anymore because of the dynamic nature of the initiative. The BattleRoutine now needs to change to accomodate for the changed initiative. I think we can get away with just adding another state

So, let’s break this into bits;

  1. We need the battlers to only be attacking when they’re ready, so we could perhaps add a new state called Cooldown
  2. We need the BattleRoutine to skip over battlers that are in the cooldown state
  3. Set the state when an enemy has completed their attack
  4. We need a way for the battlers to advance their cooldown, i.e. countdown their timers

1. Add a new state

We can add a new state to BattelEntities.Action

public enum Action { Attack, Run, Cooldown } // <- add cooldown

easy.

2. Ignore battlers that is cooling down

Let the BattleRoutine skip over battlers that are in the Cooldown state. We just need to add a new check in the loop in BattleRoutine

switch (allBattlers[i].BattleAction)
{
    case BattleEntities.Action.Attack:
        // do the attack
        yield return StartCoroutine(AttackRoutine(i));
        break;
    case BattleEntities.Action.Run:
        yield return StartCoroutine(RunRoutine());
        break;
    case BattleEntities.Action.Cooldown:
        // Skip battlers that are in the cooldown state
        break;
    default:
        Debug.Log("Error - incorrect battle action");
        break;
}

3. Set state after attack

After the battler attacked, we need to set the state to cooldown so that the system will skip the battler when it’s not ready yet. Set the state at the bottom of the AttackRoutine

private IEnumerator AttackRoutine(int i)
{

    // players turn
    if (allBattlers[i].IsPlayer == true)
    {
        // Player turn is in here, leave it here
    }

    //enemies turn
    if (i < allBattlers.Count && allBattlers[i].IsPlayer == false)
    {
        // Enemy's turn is in here, leave it here
    }

    // Adjust the battler's state
    allBattlers[i].BattleAction = BattleEntities.Action.Cooldown;
    // TODO: Start the cooldown
}

I haven’t checked (it would take the whole weekend to get through the whole course) but it appears ‘Run’ takes up a turn, so you’d need to do the same in the RunRoutine.
This brings up an issue; At the end of the loop, it appears the player can choose what to do next, but if the player is in the cooldown state they should not be able to do anything. You need to decide how to handle that because you don’t want the player to remove themselves from the cooldown state but you also need to trigger the next loop…

4. Running the cooldown

I’m not sure what the best way is here, because I haven’t watched the full course. From my brief look, it appears the whole combat is run off the BattleEntities class. One way to handle cooldown would be to store the cooldown values in here and run a separate coroutine that will reduce the cooldown if the battler is in the cooldown state.
You’d add a cooldown value; public float CurrentCooldown; and add a method to start the battler’s cooldown

public void StartCooldown()
{
    BattleAction = Action.Cooldown;
    CurrentCooldown = CalculateCooldown();
}

We can then update the AttackRoutine (and RunRoutine) above to be

private IEnumerator AttackRoutine(int i)
{

    // players turn
    if (allBattlers[i].IsPlayer == true)
    {
        // Player turn is in here, leave it here
    }

    //enemies turn
    if (i < allBattlers.Count && allBattlers[i].IsPlayer == false)
    {
        // Enemy's turn is in here, leave it here
    }

    // Adjust the battler's state  - REMOVE
    //allBattlers[i].BattleAction = BattleEntities.Action.Cooldown; - REMOVE
    // TODO: Start the cooldown - REMOVE
    // Start the battler's cooldown
    allBattlers[i].StartCooldown();
}

The battler will now go to the cooldown state, but we have a function that doesn’t exist yet. This is where you need to determine what the cooldown is. You need to make another function in BattleEntities called CalculateCooldown() where you can calculate what the cooldown period must be.

private float CalculateCooldown()
{
    return 2f; // temp 2 second cooldown
}

Now this is not all, though. We also need to count down the cooldown. You can just do this in the BattleRoutine but the BattleRoutine does not loop per frame. There are times where multiple frames will pass during each turn. I’m going to call this ok and just do it here.
In the BattleRoutine, when the battler is in cooldown, we’ll reduce the cooldown timer

switch (allBattlers[i].BattleAction)
{
    case BattleEntities.Action.Attack:
        // do the attack
        yield return StartCoroutine(AttackRoutine(i));
        break;
    case BattleEntities.Action.Run:
        yield return StartCoroutine(RunRoutine());
        break;
    case BattleEntities.Action.Cooldown:
        allBattlers[i].ReduceCooldown(Time.deltaTime);
        break;
    default:
        Debug.Log("Error - incorrect battle action");
        break;
}

And in BattleEntities we reduce the cooldown and reset the state when the cooldown is over

public void ReduceCooldown(float deltaTime)
{
    CurrentCooldown -= deltaTime;
    if (CurrentCooldown <= 0f)
    {
        BattleAction = Action.Attack;
    }
}

Now all you need to do is update the BattleVisuals to look at CurrentCooldown and update it

I have no idea if this is what you wanted (and I have no idea if this will even work) but it may be a start

1 Like

Unfortunately, my computer has crashed and burned and converted itself into a paperweight last night, because I actually have a simpler solution for this.

FFVII, and Final Fantasy Tactics happen to be my favorite turn based setups, and exactly because each character has a (fairly predictable) turn order, rather than side to side.

In my approach, each character has two variables, speed (higher is better) and NextTurn, which is simply a float value used for comparison.

You also need to determine how much weight to give to speed. You’ll have to play with this constant.

At the beginning of each battle, all of the characters calculate their own NextTurn, which is rather simple:

nextTurn = Weight/speed * (Random.Range(.9f, 1.1f); //simulates initiative rolls

Then, it’s time to determine who goes first, based on their NextTurn value.

For this, you can simply sort through the living battlers and select the one with the lowest NextTurn.

When that character’s turn finishes, calculate the nextTurn

nextTurn+=Weight/speed;

Continue doing this until one side or the other is out of characters.

2 Likes

Thank you @Brian_Trotter for your response. I used the method you prescribed and everything works in the battle scene (every unit takes its own turn) I just wanted to get it peer-reviewed to ensure I understood everything.

In the BattleSystem.cs I added to the BattleEntities class:

public float Speed = 10f; //<- place holder, add to SetEntityValues() like other stats

public float Weight = 2f; //<- place holder, keep static

public float nextTurn;

public void InitiativeRoll()
{
     nextTurn = Weight/Speed * (Random.Range(.9f, 1.1f));
}
public void CalculateNextTurn()
{
     nextTurn += Weight/Speed;
}

then I went and changed the DetermineBattleOrder method

private void DetermineBattleOrder()
{
   //allBattlers.Sort((bi1, bi2) => -bi1.Initiative.CompareTo(bi2.Initiative)); // sorts list by initiative in ascending order (original code)
   for (int i = 0; i < allBattlers.Count; i++)
   {
      allBattlers[i].InitiativeRoll();

      allBattlers.Sort((bi1, bi2) => bi1.nextTurn.CompareTo(bi2.nextTurn));
   }
}

After I added the AttackRoutine and RunRoutine respectfully

private IEnumerator AttackRoutine(int i)
{
     if (allBattlers[i].IsPlayer == true)
     {
         //player action logic
     }
     if (i < allBattlers.Count && allBattlers[i].IsPlayer == false)
     {
         //enemy action logic
     }
     
      allBattlers[i].CalculateNextTurn();
      DetermineBattleOrder();
}

private IEnumerator RunRoutine(int i) //<- add int to pass in from BattleRoutine()
{
   if(state == BattleState.Battle)
   {
      if(Random.Range(1,101) >= RUN_CHANCE)
      {
         //keep the same
      }
      else
      {
          bottomPopUpText.text = RUN_FAILED_MESSAGE;
          allBattlers[i].CalculateNextTurn();
          DetermineBattleOrder();
          yield return new WaitForSeconds(TURN_DURATION);
      }
    }
}

I hope I have interpreted your instructions correctly. If I have how would I go about visualizing the initiative system in the BattleVisual.cs? Would I assign a slider value follow the nextTurn variable or should I make an UI icon hover the player/enemy to SetActive() to show whose turn it is?

Thanks again!

That looks pretty good.

As each character takes it’s turn, I like to put some sort of affordance. It could be an image in the upper left showing the current character’s turn, or perhaps even temporarily setting the scale on the current characters turn.

It’ll be at least 3 days before I’m in a position to demonstrate this with some code (It’s the hard drive, and it’s failed spectacularly).

1 Like

Okay, I’ve had my hard drive konk out on me around this time of the year about two years back, so I definitely feel your pain. Hopefully, it’s not an expensive repair job, so good luck!

Hello all, and thank you to those who offered assistance, but after some modifications to the BattleSystem.cs I got the results I sought while keeping the vast majority of the original structure intact, as seen in the images below. (It is recommended to save the original BattleSystem.cs because this will be an invasive tutorial)


New Battle Scene (John w/Speed stat of 10 and Kira w/Speed stat of 9, Bandit Speed stat 1-4 for reference)

To get this result I shall go through step-by-step so that others may do the same in the future.

First things first we will need to prepare our BattleVisual.cs (since this is the easiest part) we just need to add a serializable Silder component, a reference to the BattleSystem.cs, and make a UpdateProgressbar() method to create the visuals. For simplicity’s sake, I copied and pasted the health bar in our battle visual prefab and made the fill white.

public class BattleVisual : MonoBehaviour
{
   [SerializeField] private Slider progressbar;
private BattleSystem battleSystem;
   
void Awake()
  }
     battleSystem = FindFirstObjectByType<BattleSystem>();
     progressbar.maxValue = battleSystem.maxCooldown;
     progressbar.value = 0;
 }
   public void UpdateProgressBar(float currentCooldown) //<- Update in BattleSystem.cs
   {
        progressbar.value = currentCooldown;
   }
}

Then we shall go to our BattleSystem.cs and make some additions to our BattleEntities class. Well shall add a reference to the BattleSystem, make a Speed stat (int), and a Cooldown (float). And remember to set the Speed stat like you would the Strength via EnemyInfo.cs and PartyMemberInfo.cs.

[System.Serializable]
public class BattleEntities
{
   public BattleSystem BattleSystem;
   public int Speed;
   public float CurrentCooldown;
   
   public void CalculateInitiative(float delta)
   {
        if (!BattleSystem.isPaused && CurrHP > 0)
        {
            CurrentCooldown += (10 - (10*(Speed / 100))) * delta / 5; //<- formula may vary as needed
            if (CurrentCooldown >= BattleSystem.maxCooldown)
            {
                BattleSystem.currentTurn.Add(this);
                BattleSystem.isPaused = true;
            }
            UpdateTimer();
        }
    }
   public void ResetInitiative()
   {
        CurrentCooldown = 0f;
   }
   /*
   public void SetBoostedInitiative(float speedBoost) <- bonus code for future implementation of speed-boosting items/abilities
   {
        CurrentCooldown += speedBoost;
        UpdateTimer()
    }
  */
   public void UpdateTimer() //<- BattleVisual
    {
        BattleVisual.UpdateProgressBar(CurrentCooldown);
    }
}

Now we’re almost to the crux of the new system, but we have some more prep to do. In our CreatePartyEntites() and CreateEnemyEntites() we need to add our BattleSystem reference to our tempEntity object.

public void CreateEnemyEntites() and CreatePartyEntites()
{
   {
     ...
        //Keep all code above the same
        tempEntity.BattleVisual = tempBattleVisual;
        tempEntity.BattleSystem = this; //<- add here

        allBattlers.Add(tempEntity);
     ...
   }
}

Next we will add a new list to hold the entity that will take their respective turn, a maxCooldown (float), and a isPaused bool at the top of our BattleSystem.cs

private List<BattleEntities> currentTurn = new List<BattleEntities>();
public float maxCooldown = 100f; //<- adjust as needed
public bool isPaused = false;

Okay with that out of the way, we will be going past to point of return as we will be changing the state machine in the BattleSystem.cs

[SerializeField]
private enum BattleState
{
     Wait,
     Idle,
     TakeAction,
     CheckStatus,
     Selection,
     Victory,
     Defeat,
     Retreat
}

Be sure to delete the ShowBattleMenu() and DetermineBattleOrder() from the Start() method since we won’t be using those anymore. Now we add an Update() method to hold our BattleState machine. Then edit our BattleRoutine, AttackRoutine, and RunRoutine to reflect our new statemachine.

void Update()
{
    for (int i = 0; i < allBattlers.Count; i++)
    {
        allBattlers[i].CalculateInitiative(Time.deltaTime);
    }

    switch (battleState)
    {
       case State.Wait:
           if (currentTurn.Count > 0)
            {
               battleState = State.Idle;
           }
           break;
        case State.Idle:
            StartAction();
            break;
        case State.TakeAction:
        //Wait till Action is selected
        break;
        case State.CheckStatus:
            RemoveDeadBattlers();
            bottomPopUp.SetActive(false);
            battleState = State.Wait;
            break;
        case State.Selection:
        //Empty space for retreat behavior
            break;
        case State.Victory:
            break;
        case State.Defeat:
            break;
        case State.Retreat:
            break;
    }
}

private void StartAction() //<- determines if Current Turn is player
{
    for (int i = 0; i < currentTurn.Count; i++)
    {
        if (currentTurn[i].IsPlayer && battleState == State.Idle)
        {
            ShowBattleMenu();
            battleState = State.TakeAction;
        }
        else if (!currentTurn[i].IsPlayer)
        {
            StartCoroutine(BattleRoutine());
        }
    }
 }

public IEnumerator BattleRoutine()
 {
    battleState = State.TakeAction;
    bottomPopUp.SetActive(true);
        
     for (int i = 0; i < currentTurn.Count; i++)
     {
    if (currentTurn[i].IsPlayer && battleState == State.TakeAction)
    {
        switch (currentTurn[i].BattleAction)
       {
            case BattleEntities.Action.Attack:
               yield return StartCoroutine(AttackRoutine(i));
               break;

               case BattleEntities.Action.Run:
                   yield return StartCoroutine(RunRoutine(i));
                    break;

               default:
                    Debug.Log("Error - null battle action!");
                   break;
            }
        }
        else if (!currentTurn[i].IsPlayer)
        {
            switch (currentTurn[i].BattleAction)
            {
                  case BattleEntities.Action.Attack:
                    yield return StartCoroutine(AttackRoutine(i));
                    break;

                 case BattleEntities.Action.Run:
                    yield return StartCoroutine(RunRoutine(i));
                    break;

                default:
                    Debug.Log("Error - null battle action!");
                    break;
            }
        }
    }

    yield return null;
}

private IEnumerator AttackRoutine(int i)
 {
     battleMenu.SetActive(false);

     if (currentTurn[i].IsPlayer == true)
     {
         BattleEntities currAttacker = currentTurn[i];
         if (allBattlers[currAttacker.Target].CurrHP <= 0)
         {
             currAttacker.SetTarget(GetRandomEnemy());
         }
         BattleEntities currTarget = allBattlers[currAttacker.Target];
         AttackAction(currAttacker, currTarget);
            
         yield return new WaitForSeconds(TURN_DURATION);

         if (currTarget.CurrHP <= 0)
         {
             bottomPopUpText.text = string.Format("{0} defeated {1}!", currAttacker.Name, currTarget.Name);
             yield return new WaitForSeconds(TURN_DURATION);
             enemyBattlers.Remove(currTarget);

             if (enemyBattlers.Count <= 0)
             {
                 battleState = State.Victory;
                 bottomPopUpText.text = VICTORY_MESSAGE;
                 yield return new WaitForSeconds(TURN_DURATION);
                    
                 SceneManager.LoadScene(OVERWORLD_SCENE);
             }
         }
     }

     if (i < allBattlers.Count && currentTurn[i].IsPlayer == false)
     {
         BattleEntities currAttacker = currentTurn[i];
         currAttacker.SetTarget(GetRandomPartyMember());
         BattleEntities currTarget = allBattlers[currAttacker.Target];
         AttackAction(currAttacker, currTarget);

         yield return new WaitForSeconds(TURN_DURATION);
         if (currTarget.CurrHP <= 0)
         {
             bottomPopUpText.text = string.Format("{0} defeated {1}!", currAttacker.Name, currTarget.Name);
             yield return new WaitForSeconds(TURN_DURATION);
             playerBattlers.Remove(currTarget);

             if (playerBattlers.Count <= 0)
             {
                 battleState = State.Defeat;
                 bottomPopUpText.text = DEFEAT_MESSAGE;
                 yield return new WaitForSeconds(TURN_DURATION);
             }
         }
     }
        
     EndAction(i);
 }

 private IEnumerator RunRoutine(int i)
 {
     if (battleState == State.Idle)
     {
         if (Random.Range(1, 101) >= RUN_CHANCE)
         {
             bottomPopUpText.text = RUN_SUCCESS_MESSAGE;
             battleState = State.Retreat;
             allBattlers.Clear();
             yield return new WaitForSeconds(TURN_DURATION);
             SceneManager.LoadScene(OVERWORLD_SCENE);
         }
         else
         {
             bottomPopUpText.text = RUN_FAILED_MESSAGE;
             EndAction(i);
             yield return new WaitForSeconds(TURN_DURATION);
         }
     }
 }

private void EndAction(int I) <- Reset Initaitive 
{
     isPaused = false;
     currentTurn[i].ResetInitiative();
    currentTurn[i].UpdateTimer();
    currentTurn.RemoveAt(0);
    battleState = State.CheckStatus;
}

Finally, we need to change our SelectEnemy() and SelectRunAction() for our buttons to match our new BattleSystem.

public void SelectEnemy(int currentEnemy)
{
     BattleEntities currentPlayerEntity = currentTurn[0];
     currentPlayerEntity.SetTarget(allBattlers.IndexOf(enemyBattlers[currentEnemy]));

     currentPlayerEntity.BattleAction = BattleEntities.Action.Attack;

     StartCoroutine(BattleRoutine());

     enemySelectMenu.SetActive(false);
}

public void SelectRunAction()
{
     battleState = State.Selection;
     BattleEntities currentPlayerEntity = currentTurn[0];

     currentPlayerEntity.BattleAction = BattleEntities.Action.Run;
     StartCoroutine(BattleRoutine());
     enemySelectMenu.SetActive(false);
}

And that’s it for our modifications to the BattleSystem.cs, no other changes are necessary. The only thing left is determining a good speed formula, which requires some finagling to find a good balance of how slow or fast you want the initiative bars to go in your game.

Hope this helps y’all on your GameDev journey,
Thank you for reading.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms