Using Metaprogramming for a RPG Battle Engine
Ever wonder what happens behind the scenes when you click on an action on your toolbar in Forumwarz?
The action might perform an attack. It might heal you. It might even do both! Some attacks are stronger when you’re weaker, others spawn buffs that protect (or harm you) for multiple turns.
How does the game know what to do?
Putting Effects in the Database
When I was first prototyping Forumwarz, I stored the information for each action as rows in a database table. An Action would have multiple Effects.
An Effect would have a target, an attribute and a value.
Here’s a (simplied) example of an attack:
Target: Enemy, Attribute: Pwnage, Value: 6
Target: Player, Attribute: Douchebaggery, Value: 1
This worked pretty well at first. However, I quickly found myself wanting to put in exceptions to the structure. What if you only want an effect to apply when a certain condition is met?
For example, Life After Death brings you back to life if you die in the next few turns after you’ve used it.
You could add a new column for that, like “only_on_death” as a boolean, but that’s kind of lame. Are you going to add a new column every time an action does anything interesting?
And what if you want your attack to do some cool stuff in the future? Let’s say it’s stronger against certain things, weaker against others.
Forumwarz is coded in Ruby, so I decided to make use of its scripting capabilities.
I came up with a framework where each action in the game had its own class. The class would be instantiated with a reference to the player performing it, and the enemy they were currently attacking. Then it would call an execute method which would do whatever the action was meant to do:
action = AsciiArtAttack.new(character, enemy)
The implementation for AsciiArtAttack would look something like this:
class AsciiArtAttack < Action
@enemy.update_attribute :life, @enemy.life - 6
@character.update_attribute :douchebaggery, @character.douchebaggery - 1
The beauty of this approach is you’re free to use any Ruby code you want in the execute method. You can check any conditions you want, you can calculate anything you want, really, the sky is the limit!
It’s also faster to execute, since it doesn’t involve querying the database each time you perform an action. It just runs through the Ruby code!
One obvious disadvantage is that you can’t change the balance of the game without access to its Ruby code. Another is that you cannot “query” your attacks without a parser that can read Ruby code which is a non-trivial problem. So if you wanted, say, a list of all the attacks that effect Ego, your best tool is probably a text editor’s search tool.
Although the above code is how the Forumwarz battle engine works under the hood, I rarely write the code that way. As it turns out, exceptions like “Only execute this action when you’re dead” are quite exceptional. Most of the attacks in the game can be written as the simple list of their effects.
To make my life easier, and to avoid repeating myself, I have used the metaprogramming capabilities of Ruby to build the classes for the actions.
Instead of implementing an execute method, I use class methods to build it for me. This is similar to how putting in Validation class methods in ActiveRecord will write the validations for you.
Here’s the actual code for the Ascii Art Attack that’s running on Forumwarz right now:
class EventHandlers::Abilities::Troll::Ascii < EventHandlers::ActionBase
decrease :enemy, :pwnage, :value => 7..9
decrease :player, :douchebaggery, :value => 1
inactivate_action :turns => 1
minimum :douchebaggery, :value => 1
As you can see it does a little more than the above example did. It inactivates itself on your toolbar for 1 turn. It also enforces a minimum of 1 douchebaggery before it runs.
However, I think it’s quite a bit more readable. You can just look at it and understand what it’s doing.
This is how most of the actions in the game are written. If I need more flexibility, I just leave those class methods out and write an execute method instead. Pretty neat eh?
A secondary advantage is that the class methods can be implemented differently depending on what you’re doing with them. Those same methods are used to generate the descriptions of an action, for example. Instead of calling “execute” I can call “describe” and get back the text for the tooltips you see in battle.
I won’t go into how the metaprogramming works since it’s outside the scope of this article (and also, other people can explain it much better than I can), but it’s really worth looking into if you’re coding something similar to this.
Subscribe to comments with RSS.
Comments are closed.