December 08, 2020

[Programming Notes] Turn-Based Combat System

This week, I implemented the core systems of the new turn-based combat system for Daughter of Dreams. This is one of the most technically complicated pieces of code I've written, though it may not seem like it. The challenging part for me was making it as simple as it is. I believe what I have will be powerful and flexible enough to expand easily to support the entire combat system (and probably in-game cutscenes as well, though GML 2.3 sequences may still be a better option). Abilities are modular, and can be assigned to any entity (player characters and enemies), and the combat engine can transition between states fluidly, as well as undo actions.

What It Looks Like

So far, the combat prototype has two main operations. A character can be selected from any number of playable characters, and they can be given an order to move according to basic grid-based movement. These are merely the simple actions I implemented to test the system. As the game expands, I will add new actions that will be supported by the foundational code that makes up the combat system.

Two playable characters with grid-based movement.

System Overview

The combat system is currently comprised of two primary components.
- A finite state queue of functions which return true or false.
- An index of actions assigned to combat entities (playable characters and enemies).

The logic for the finite state function queue is as follows:
1) Run the current function when the update method is called. (Usually in the step event).
2) If the function returns true, progress the queue to the next function.

Next, an action is defined globally, and can be assigned by reference ID to any entity.
- The action includes all the data required to display the action on the UI.
- The action execute method can assign required values for the entities.
- The action can add functions to the combat function queue to define the logic of the action.

Class definitions shows the essential attributes and operations of each component.

System Implementation

With these components in place, the logic for starting a new combat scenario is as simple as adding each step of the combat cycle to the combat function queue. The logic of each step is handled internally, moving to the next state once that function returns true.

Each step (combat_start, combat_player, etcetera) can divert from the primary cycle by inserting more functions in the combat function queue at its own position. For example, the player phase will allow the player to choose actions for their characters. When an action is chosen, the associated functions are added to the combat function queue.

The final combat cycle can be represented with this flowchart. The primary combat cycle is the Start > Player Phase > Enemy Phase > End/Restart. Each player or enemy action diverts the primary cycle by inserting other phases into the combat function queue. Furthermore, actions could be canceled at certain steps, by instead inserting the previous phase. (For example: with Choose Attack, on "confirm" input, insert Choose Target, but on "cancel" input, insert Player Phase again).

System Pros and Cons

This method is powerful because it is easy to transition from each state to any other state, just by appending the next associated function. It handles the turn-based aspect of the combat with the simple logic of the function queue. Each state is handled in order, one at a time. Actions are modular, so they can be assigned to and used by any entity, instead of programming every ability for each entity.

The downside of this system is that at the moment, the function queue cannot accept parameters. Every function used for combat abilities must return true or false, and be designed to run every time the function queue update method is called. If additional variables are needed (such as timers, or damage values), they must be declared outside of the scope of the function. It is possible I will expand the function queue to use parameters as well, but it will complicate the system.

Additionally, while actions are modular, they also require more code this way than they would if each was more specialized. The move action as it stands uses four separate functions (initialization, player input, drawing, and execution). However I believe the flexibility outweighs this complication.

Stray Thoughts

In the future, I hope to regularly post programming notes to cover the more interesting systems that I develop for Daughter of Dreams. I am learning a lot, and I hope if I share what I've been doing that others might find it useful too. Also, if anyone knows of useful articles or posts covering similar topics, please let me know and I will include them in the resources section.


No comments:

Post a Comment