Evan's Dev Blog 1/11 - The Effect System
Last week, I gave a general overview of the steps I took to make the framework for a turn-based battle similar to those found in games such as Pokémon and Dragon Quest. One of the biggest aspects of RPGs are the crazy variety of moves available, and it is this aspect that any good battle system, especially one that thrives on unpredictability should execute as best as possible.
While it would be simple to write out exceptions for every single attack, this will get unwieldy very quick. The jam game that "Random Encounter" takes inspiration from handled specific spell cases in this manner, and it got very out of hand near the end of our 48 hour time limit. For example, to handle a move that would ignore temporary invincibility, we hardcoded an exception in the damage calculation to check if the attacker's move was the specific move that countered the invincibility.
When I started work on "Random Encounter" my goal was to design a system which would allow me to create an effect on the complexity level of the move Perish Song from Pokémon without ever having to touch anything in code. (Perish Song is a move that causes all Pokémon on the field to faint automatically after 3 turns pass.) To start with, I defined a base Effect class. This class would inherit from ScriptableObject, allowing effects to be created in editor. Effects contain UnityEvents indicating checks for if the Effect can activate, what should be done when the Effect activates or deactivates, and for lingering Effects, what conditions must be met for the Effect to stay active.
Once the Effect class had been created, I modified damage calculation to allow for Spells to have a chance to invoke an arbitrary number of Effects when the Spell hits. It was at this point I ran into the first major roadbock: Effects vs. Properties. Using Pokémon as an example again, there are moves that apply effects to the user or target if the move hits (such as Perish Song), but there are also moves that have special properties that either may or may not apply depending on the battle conditions. For example, the move Revenge increases in power if the target has attacked the user. I figured that Properties are Effects that apply only during the damage roll and then disappear. It was at this point I could define several events in the Effect class relating to the different points in the turn a move effect could apply: the beginning and ending of a turn, and when the player selects a move before damage is dealt. I also modified the Spell Class to have a list of Effects indicating the properties of the attack
However, I then needed a way to keep track of different instances of a given Effect, as the ScriptableObject architecture does not support this. I wrote an EffectInstance class which contains references to the Spell that generated this Effect, the Spell's user and target, and the Effect this instance is tied to. EffectInstances contain functions that call an Effect's events. For example, at the end of a turn, the OnTurnEnd function is called on all active EffectInstances, which will invoke the TurnEnd event on each instance's related Effect. This degree of separation is necessary in the event that more than one entity in the battle has the same effect.
The Effect class defines a number of functions that can influence the user or the target. In editor, these functions can be assigned to the events on an Effect to build out its function. As an example, let's take a look at one of the more complex Effects in the game right now: the Attack accuracy debuff. Each time the player uses their standard attack command, the move decreases in accuracy by 0.8x for each turn the command has been used. This debuff is removed when the attack misses.
First, the attack itself has a 100% chance to inflict an Effect called Attack Effect. Let's take a look at it:
The Effect has no checks for success, meaning it will always activate. In order for this effect to remain active, the user's last used attack must have been the Attack itself, and that last attack must have been successful.
When this Effect activates, it will apply an instance of itself to the user. This may not always be required for Effects, but if it needs to be tracked for later use, it should always be applied. Note that Effects can be set to stack, though this effect does not stack, meaning the instance is not applied if an instance is already applied. This Effect also increases the user's MP by 0.1 times the damage dealt for a minimum of 1 and a maximum of 50. This unfortunately has to be handled through a parsed string due to limitations on Unity Events, but I will look for a better solution in the future.
At the end of each turn, the game checks if each Effect should remain active and when this Effect is no longer active, its instance should be removed from the user.
Lastly, when a move is selected, this Effect applies a property to modify the damage roll. Let's take a look:
The property is a non-stackable property that applies only if the user's selected move is equal to the standard attack. When this property activates, it applies an accuracy modifier equal to 0.8 times the number of turns the EffectInstance has been active for. When the property deactivates (after the damage roll) the accuracy modifier will be removed. Note that the accuracy removal function is overloaded and can search for a modifier by name, but we don't need to use that version, as the base function searches for modifiers applied by this Effect.
Can we recreate Perish Song with this system?
Create a move called Perish Song that invokes an Effect called Perish Song Effect.
The Effect is activated 100% of the time.
Once activated, the Effect applies an instance of itself to the user and target.
The Effect remains active if the number of turns active is less than 3.
When the Effect deactivates, kill the entity the Effect is attached to.
I'm sorry for the lack of nice visuals this week. Next week's entry should be more visually pleasing. In the meantime, feel free to take a look at some snippets from this project on my github.
See you next week!