Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to the fifth module of our course. In the previous modules, we built a robust foundation for our game world, culminating in a powerful event system that can handle conditional logic and scripted cutscenes. We now have the ability to transition between different game scenes, like from a world map into a building. Today, we'll leverage that by designing the scene that lies at the heart of every classic JRPG: the battle screen.
This lesson addresses the first and most crucial outcome for this module: Design the architecture for a classic turn-based combat system, including a state machine (start, input, execution, end). We won't be writing the final code today, but we will create the architectural blueprint that will guide all our future implementation for combat.
1. Why We Need a Plan: The Perils of Unstructured Combat Logic
Imagine trying to code a battle system using only the if/else logic and flags we've discussed. You'd quickly end up with a tangled web of variables: isPlayerTurn, isEnemyTurn, isAnimating, hasPlayerChosenAction, isBattleOver, and so on. Your main update loop would become a massive, unreadable block of conditional statements trying to figure out what to do next.
This approach is brittle, hard to debug, and nearly impossible to extend. Adding a "poisoned" status that deals damage after a character's turn could break the entire flow. This is a classic software engineering problem, and the solution is a design pattern you may have encountered in managing complex application states: the Finite State Machine (FSM).
An introduction to finite state machines and the state pattern for game development
To see why this is so critical, let's watch a short segment from the video "An introduction to finite state machines" by The Shaggy Dev. It uses a platformer character as an example, but the problem of escalating complexity is universal. As you watch, think about how this applies to the flow of a battle.
Watch the section "The Problem Without State Machines" from 00:52 to 02:35. Notice how adding new abilities like 'double jump' or 'attack' exponentially increases the complexity of the conditional logic.
Just like in the video, a JRPG battle has distinct phases: it starts, someone chooses an action, the action is performed, the consequences are checked, and the cycle repeats. An FSM provides a formal, clean way to manage these phases.
2. The Finite State Machine in Game Development
A Finite State Machine is a model of computation that is always in one of a finite number of "states." The machine can only be in one state at a time, and it can transition from one state to another in response to inputs or conditions.
The core rules, as applied to our game:
- A fixed number of states: The battle can be in a
Battle Startstate, anInputstate, anAction Executionstate, etc. - Only one state is active at a time: The game isn't simultaneously waiting for player input and executing an enemy's attack.
- Each state has a single responsibility: The
Inputstate's job is to get an action from the current combatant. It doesn't do damage calculation. - Transitions are explicit: The
Inputstate transitions to theExecutionstate only after an action has been selected.
This approach turns a chaotic mess of flags into an orderly, predictable flowchart.
How to Build a JRPG: A Primer for Game Developers - Code
The article "How to Build a JRPG: A Primer for Game Developers" provides an excellent overview of using state machines to manage a JRPG's overall game modes. The same principles apply within the combat system itself.
Please read the section titled 'Managing Game State' and the subsection 'Handling Complexity With a State Machine'. Focus on how an FSM is proposed as a solution to handle transitions between different game modes like the world map and combat.
3. Architecting the Battle State Machine
The resources suggest a common and powerful object-oriented pattern for implementing an FSM, which will translate very well to JavaScript/TypeScript classes.
The core components are:
- A
StateMachineobject: This controller holds a reference to thecurrentStateand is responsible for managing transitions. Itsupdatemethod simply calls theupdatemethod of thecurrentState. - A
Statebase class or interface: This defines the "contract" for all states. It ensures every state has methods likeenter(),exit(), andupdate(). - Concrete State classes:
BattleStartState,InputState, etc. Each of these inherits from the baseStateand implements the specific logic for that phase of the battle.
An introduction to finite state machines and the state pattern for game development
Let's return to The Shaggy Dev's video. This next segment explains the object-oriented state pattern in detail. This is the blueprint for our combat system's code structure.
Watch the section "The State Pattern: Object-Oriented Implementation" from 05:55 to 10:48. Pay close attention to: The creation of a generic State interface. How each state becomes its own class (JumpState, IdleState). How the main Player class delegates logic to the current state. The enter() and exit() methods for setup and cleanup during transitions.
This pattern provides excellent separation of concerns. If you need to change how player input works, you only touch the InputState class; the rest of the system remains unaffected.
4. The States of Combat
Now, let's define the specific states for our classic turn-based system, as required by the learning outcome.
A flowchart representing the Finite State Machine for our turn-based combat system. It shows the primary states and the transitions between them.
-
Start State:
- Responsibility: Initializes the battle.
- Actions:
- Transitions the screen (e.g., screen shatter/swirl effect).
- Creates and places party and enemy sprites.
- Calculates the initial turn order (we'll explore this more in a later lesson, but for now, we can just pick a starting character).
- Transition: Once setup is complete, it transitions to the
Inputstate for the first combatant.
-
Input State (or Decision State):
- Responsibility: Get the desired action for the currently active combatant.
- Actions:
- If the combatant is player-controlled, display the command menu (Attack, Defend, Skill, Item) and wait for the player's selection.
- If the combatant is an enemy, run its AI routine to decide on an action.
- Transition: Once an action is chosen, it transitions to the
Executionstate, passing the chosen action and its target(s) along.
-
Execution State:
- Responsibility: Perform the chosen action.
- Actions:
- Play the action's animation (e.g., character sprite moves forward, sword slash animation).
- Play sound effects.
- Calculate damage/healing based on stats and formulas.
- Update the target's HP/MP and display damage pop-ups.
- Transition: After the action is fully resolved (including animations), it transitions to the
CheckEndstate.
-
CheckEnd State:
- Responsibility: Check if the battle has been won or lost.
- Actions:
- Check if all enemies have 0 HP. If so, transition to the
Victorystate. - Check if all party members have 0 HP. If so, transition to the
GameOverstate.
- Check if all enemies have 0 HP. If so, transition to the
- Transition: If the battle is not over, it determines the next combatant whose turn it is and transitions back to the
Inputstate for them.
-
Victory & GameOver States (End States):
- Victory: Displays a victory message, calculates and awards Experience Points (EXP) and loot, and then signals the main
SceneManagerto pop the battle scene and return to the map. - GameOver: Displays a game over message and transitions back to the title screen or a load menu.
- Victory: Displays a victory message, calculates and awards Experience Points (EXP) and loot, and then signals the main
Test your understanding!
You want to implement a "Defend" command. When a character defends, they take reduced damage until their next turn. In which state would you:
- Set a
isDefendingflag on the character? - Use that
isDefendingflag to modify the damage calculation?
Show answer
- You would set the
isDefendingflag in the Execution State. When the player chooses "Defend" from theInput State, theExecution Statewould receive this action. Its job would be to apply the result of that action, which in this case is setting the character's status to "defending." - You would check the
isDefendingflag, also in the Execution State. When another character attacks the defending character, theExecution Statehandles the damage calculation. It is here that it would check the target's status:if (target.isDefending) { damage /= 2; }.
This shows how a single state can be responsible for executing many different types of actions, reinforcing the clean separation of concerns.
5. Nesting State Machines
A key architectural point is that this battle FSM will live inside our main game SceneManager.
- On the world map, a random encounter is triggered.
- The
SceneManagerpushes a newBattleSceneonto its state stack. - When the
BattleSceneis created, it initializes its own internalBattleStateMachineand sets its first state toStartState. - The main game loop calls
SceneManager.update(), which in turn callsBattleScene.update(). - All the
BattleScenedoes in itsupdateis callthis.battleStateMachine.update(), delegating all the complex battle logic to the currently active battle state.
The article from "How to Build a JRPG" has a pseudocode example that perfectly illustrates this nesting.
How to Build a JRPG: A Primer for Game Developers - Code
Let's revisit the "How to Build a JRPG" article. The pseudocode here is the most important part of this lesson for understanding the overall architecture.
Scroll down to the 'Combat' section and examine the pseudocode for the BattleState class. Notice that the BattleState class (which is a state for the main game) contains its own state machine: mBattleStates. This is the nested architecture we will build.
Conclusion
Today we've laid the complete architectural blueprint for a robust and extensible turn-based combat system. By choosing to use a Finite State Machine from the outset, we've replaced a potential mountain of complex conditional logic with a clean, manageable, and professional software pattern.
Key Takeaways:
- A classic JRPG combat system is a sequence of distinct phases, making it a perfect candidate for a Finite State Machine (FSM).
- The core states of our battle FSM are Start, Input, Execution, and CheckEnd, with terminal states like Victory and GameOver.
- The implementation will use an object-oriented approach with a main
StateMachinecontroller and individual classes for each state (e.g.,InputState), each withenter,exit, andupdatemethods. - This
BattleStateMachineis self-contained and will be managed by the largerBattleScene, which is in turn managed by the game's mainSceneManager.
Preview of the next lesson:
We have the flow, but we don't have the fighters. In our next lesson, we will address the learning outcome: "Create a data structure for combatants (actors and enemies) with core stats (HP, MP, ATK, DEF, AGI)." We will design the data objects that our new battle system will manipulate, defining the very essence of our heroes and monsters.