Battle System Architecture
Like I said yesterday, I implemented the BLIND status effect and implemented a simple placeholder for CHARM.
For BLIND, I needed a way for attacks to miss, so I just added a flat 1/16 chance of attacks missing. I have no idea if I'll keep this base miss chance but it's kind of funny. When you are BLIND this chance increases to 3/4.
By the way, there's no display currently that shows which status effects are active. I'll have to add that later.
For CHARM, I just made it so both the player and enemy will attack themselves when CHARMED. I think the battle system code is structured in a way that makes it relatively easy to swap this out later for something more complicated, like a menu that lets you select what the enemy does.
It would be cool if eventually, this blog can serve as a guiding light for others who want to make an RPG but don't know where to get started on all the systems you have to implement. So I wanted to describe how my battle system works right now.
The battle system has the following key variables:
- List of battlers in the battle.
- Queue of actions that the battlers will take during the turn.
- Current battler being processed, which is a number (0 for the first battler in the list, 1 for the second, etc.)
- Current action being processed, which is also a number (0 for the first action in the queue, 1 for the second, etc.)
The basic structure is for the system to ask each battler to submit an action to the action queue. Once all battlers have submitted their actions, the system executes the submitted actions in the order they were submitted. Once all actions have been executed, the turn ends. At the end of a turn, the battle system empties the action queue, and resets the current battler and current action back to 0.
Does it sound simple? The problem is that video games have animations. If they didn't, then this kind of system would be much easier to program. You could simply do the steps in the order listed above, and whenever you require input from the player, you could freeze the entire game until they input their action.
But because video games have animations, it's like this: while the player is selecting their action, the game needs to continue running at 60 frames per second and updating (for example) the idle animations of all enemies. So you can't just "stop the game and wait for input". You have to write the code in a way where it will check for the player input and process it but then continue doing all the other stuff it needs to do in that frame.
And let's consider this: if the player attacks an enemy, maybe you want it to play a little animation of a sword swing and have a damage number pop up. This will happen during the phase of the battle system logic where "the system executes the submitted actions in the order they are submitted". So it's actually not that simple to just say "do action 0, then action 1". You have to write the code like this: "every frame, advance the animation for action 0, and check if the animation has finished yet, and once it finishes, move on to action 1".
So let's be more specific. Every frame, this is roughly what the battle system does. (I'm leaving out some minor details.)
- If there are still battlers to process, ask the current battler to submit an action to the queue.
- The current battler responds to the ask with either "Here's my action" or "I'm still deciding".
- Enemies will typically just submit their action instantly.
- Player-controlled characters do three things when they receive an ask: they check player input, then display a menu, and then finally respond to the ask. Checking input is used to control the state of menu (which the player uses to select actions). Every frame, character will respond with "I'm still deciding" up until the point that the player actually chooses their action from the menu, and then they will finally respond with the chosen action.
- If the current battler responds with an action, the action is added to the action queue.
Then, move on to the next battler, until there are no more battlers to process.
- If all battlers are processed, we proceed to the next step.
- Otherwise, STOP!! The battle system does not execute any more code this frame.
- If we get here, it's time to process the actions in the action queue. Each action stores, at minimum, three pieces of information:
- The battler who is performing the action.
- The target of the action, which is another battler. (This could be extended later to a list of battlers for multi-target attacks.)
- The effect of the action, which is a function that takes the battler and target as input, and does the action.
- If there are still actions to process, ask the current action to perform its effect.
- The current action responds to the ask with either "Done" or "Not done yet".
- The reason an action might respond with "Not done yet" is because of animations. For example, an attack action, rather than just dealing damage immediately, might play a "sword swing" animation, then deal damage, creating a damage number effect, and then pause briefly so the player has time to read the damage number before the next action.
- If the action responded with "Done", move on to the next action in the queue, until there are no more actions to process.
- If all actions are processed, we proceed to the next step.
- Otherwise, STOP!! The battle system does not execute any more code this frame.
- We also check after processing each action if the action ended the battle (the last enemy or last player character was defeated). If so, STOP everything and switch over to end-of-battle logic.
- If we get here, the turn is over! We have processed all actions for all battlers. Run end-of-turn logic: empty action queue, reset current battler and current action.
Well, so far this system seems to be reasonably flexible. Here's some examples of how I implemented status effects:
- For STUN, before asking the battler to submit an action, the system checks if the battler is STUNNED. If so, the system submits a "skip turn" action on the battler's behalf and moves on to the next battler.
- For POISON, during the end-of-turn logic, the system checks if any battlers are POISONED. If so, it skips the rest of the end-of-turn logic, and adds "poison damage" actions which hurt each POISONED battler to the action queue. After all these new actions are processed, it runs the end-of-turn logic normally.
Going back to CHARM, suppose I wanted to do my ambitious idea where you have full control over CHARMED enemies, and can select their attacks from a menu. How I would do this is, if the enemy is CHARMED, instead of the usual "ask for an action" function, the system would call a "CHARMED ask" function that has different behaviour. I could make it so the "CHARMED ask" function displays a menu and waits for input, just like the one for player characters.
One thing I think is lacking in this system is control over turn order. I suppose it's just a matter of sorting the action queue in a certain way before processing it, but there's currently no mechanism for that. I may need that eventually, but I don't really know how turn order will work in this game yet.
I didn't expect to be writing posts that are this long. I thought my posts would be shorter....
- ← Previous
Status - Next →
Aesthetics