1
\$\begingroup\$

I'm attempting to create a turn based battle system in Unity that is made up of dynamic events. Attacks may involve a lot of movement, animation, and often player input during the attack. The current idea is to have these "events" encompass the player's decision phase, actual attacks, in-battle cutscenes, and so on. I would like the order of these to be completely flexible as well to allow for modifying turn orders if needed and inserting cutscenes into the flow of the battle.

My current solution is made up of instantiated BattleEvent classes, with the current active BattleEvent being updated every frame in the battle manager. The battle manger can also change to a new event as needed.

Here's the bare bones of the event class:

public abstract class BattleEvent
{
    protected BattleManager _battle;

    public bool isCompleted = false;

    public BattleEvent(BattleManager battleManager)
    {
        _battle = battleManager;
    }

    public abstract void UpdateEvent();

    public abstract void StartEvent();
    
    public abstract void EndEvent();
}

However, I'm still unsure of the best way to manage queuing up events, ordering them, and maybe splitting up events into smaller parts, such as if an attack has the player move to a position first, it would be nice to have a reusable "sub" event that can handle that part.

Currently I am considering just adding a queue to the battle manager that allows me to order up events and insert them where they might be needed. Events themselves would handle queuing up the next event. I could also add a queue of sub events to the events themselves, but I am unsure if this is a bad idea for reasons I might not be aware of. I guess my question is, what is the most logical way to be structuring this for scalability, or am I headed in the wrong direction entirely?

\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

One architectural pattern that is very useful for turn-based combat systems is the "stack machine".

This is a pattern that is similar to a state machine, in that you have states with enter, update and leave logic. But the difference is that you don't just have one active state. You have a list of states (the "stack") with the upmost state in the list being the active one. State transitions are done by pushing a new state on top of the list, or popping the current top state off, so the previous state becomes active again.

Let's take combat, for example. The base state of combat is when the player chooses an attack. The state stack has just one entry:

pick_attack

When the Update() event occurs, you execute the logic of the pick_attack state, which probably means to show a menu and listen to player input.

Now when the player picked an attack, you first want to play the attack animation and then resolve the attack damage by applying damage to the enemies. So you push those two states onto the stack in reverse order. The stack looks like this:

play_attack_animation
resolve_damage
pick_attack

The active state is now the play_attack_animation state. Which means the Update() method does no longer execute the logic for listening to player input. It executes the logic that manages the animation.

But now while the state is being updated, there is an event in the attack animation. The player is supposed to do a quick-time event that affects how much damage their attack does. So we push another state:

quick_time_event
play_attack_animation
resolve_damage
pick_attack

The play attack animation state is no longer active. Which means it no longer updates the animation. It's basically frozen while the QTE event is active.

When the QTE state is finished, it pops itself from the stack, so the play_attack_animation state continues where it left off:

play_attack_animation
resolve_damage
pick_attack

When the attack animation is over, it also pops itself from the stack:

resolve_damage
pick_attack

The resolve damage event subtracts the HP from the enemies and when it's over it notices that two of them died. Which means that their death animations need to be played before we can go back to the pick_attack state. So as it removes itself, it pushes those states onto the stack:

play_death_animation
play_death_animation
pick_attack

So these two death animations take over. When the second death animation finished playing, it removes itself making room for the first:

play_death_animation
pick_attack

And then the first one removes itself so we are back to where we started with the player picking another attack:

pick_attack
\$\endgroup\$
2
  • \$\begingroup\$ Thank you, this was incredibly helpful. Using this kind of structure, is there a good way to manage things that need to happen at the same time? For example, if two enemies need to play a death animation at the same time. Could I simply add a list of states to update during one queue slot or is there a better way? \$\endgroup\$
    – blue
    May 22, 2023 at 17:09
  • \$\begingroup\$ @blue In that case I would refactor the "Play Animation" state to a "Play Animations" state (plural) that manages the animations of multiple objects at the same time and pops itself when the last one finished. I could also imagine a class "CompositeState" that includes multiple regular states. But that makes a couple things more complicated. For example, what happens when those sub-states try to push new states or try to pop themselves. \$\endgroup\$
    – Philipp
    May 23, 2023 at 8:45

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .