25 7 / 2011

Embracing meta programming: An RPG character stat system

In this post we’ll explore some Ruby meta programming to create a character stat system. It serves as an introduction to Ruby meta programming for developers who only used static object-oriented languages like Java and PHP before. We are going to assume that you know a bit about the syntax. If not, it’ll be an interesting challenge for you to follow the code :)

A stat system is responsible for keeping track of various stats in role playing games. To get a better idea, these stats are usually character properties like armor value, dexterity, magic power etc. Basically every character property you can express using a number can be a stat. It is also responsible for providing ways to define the interplay between various stats. For example, you might want an increase to constitution to also increase the character’s maximum health. We have a look at a way to accomplish this task in Ruby. The system I’m describing here is also used in my game.

Creating a stat system was a big discussion over at galaxynews some days ago. Most developers of browser based games use static object-oriented languages like PHP and Java, so meta programming is a foreign concept for them. Whenever I try to explain the advantages of ruby and meta programming in general, I’m greeted with doubt and disbelief. “Nobody needs meta programming”, “I’ve never seen something like this before, so it can’t be good”, “I can do that in PHP too with eval and a lot of strings” are some common reactions. Intolerant noobs.

This post is targeted at PHP and Java programmers with no clue about meta programming. I’m hoping that it is able to show them what they’re missing out on. Some concepts are explained in greater detail, so the experienced ruby programmer might get bored.

Kicking it off with a module

In case you are coming from PHP or Java you know what classes are and how inheritance works. All common object-oriented languages these days support these concepts. However, if you have never worked in ruby before, it’s time to introduce a new concept, that is not quite as common. This concept is called a mixin. Ruby supports this concept through modules. Once you start using mixins, you do not want to go back to a language that does not support them. Trust me :) You might now ask: “What is a mixin?”

To understand what a mixin is, we first need to agree on a definition of what an object actually is. Here is my preferred definition: “An object is state plus behavior”. For the remainder of this post you have no choice but to agree. You are allowed to fall back to your old definition once we’re done :)

So lets have a look at a simple implementation of a character class. This can be done in any object oriented language:

The source for this class would look like this:

Ruby:

class Character
  attr_accessor :name
  attr_accessor :location
  def talk; end
  def walk; end
  def max_health; 100; end
  def constitution; 50; end
end

PHP:

class Character {
  public $name;
  public $location;
  public function talk() {  }
  public function walk() {  }
  public function max_health() { return 100; }
  public function constitution() { return 50; }
}

Nothing crazy going on so far.

According to the definition mentioned earlier, an instance (equivalent to object) is composed of two parts - state and behavior. The state of the instance is defined by its instance variables, while the behavior is defined by the referenced class. Now we go ahead and implement a module. A module describes behavior. You can think of it as a collection of methods. A module can be included by a class. Actually, a module can be shared between and included by multiple classes at the same time.

module Stats
  def max_health
    100
  end
  def constitution
    50
  end
end
class Character
  include Stats
  attr_accessor :name
  attr_accessor :location
  def talk; end
  def walk; end
end

Here we have extracted max_health and constitution into a module.

OK, what’s going on here? It’s not hard to understand once you have wrapped your head around the concept of mixins. In essence, mixins are another way to share behavior. The Character class includes the Stats module. Whenever we call a method on an instance of Character, that cannot be found in the referenced class, Ruby will look if the method can be found in one of the included modules. This is not the same as inheritance: Multiple modules can be included at the same time.

And here is the real kicker if you come from languages like Java: In Ruby, modules (as well as classes, actually) are just objects themselves. They can be instantiated, modified and included at runtime. So module SomeModule; end is more or less equivalent to SomeModule = Module.new.

Refining the module

Think about this for a moment. In Ruby, you create classes and modules at runtime. You can modify them at runtime. We are going to utilize this in a moment. But for now, let’s have a look at the Stats module we have implemented. We can assume that Characters are not the only class that uses Stats. We might also have Monsters, Buildings, or Pets each with their own Stats. They all might include the Stats module. Our Stats module has two functions: one to return the maximum amount of health, and one that returns the constitution. The hardcoded return values do not make a lot of sense. So we now are going to introduce a hash that stores the stat values.

module Stats
  def max_health
    @stats_hash['max_health']
  end
  def constitution
    @stats_hash['constitution']
  end
end

This looks much better. Each instance of a class that includes the Stat module can now have different values stored in its stat hash. For example, whenever a character gets constructed, it could iterate over all equipped items and populate the stat hash:

class Character
  include Stats
  {...}
  def initialize
    @stats_hash['max_health'] = get_max_health_from_items 
    @stats_hash['constitution'] = get_constitution_from_items
  end
end

However, there is a huge ugly problem with this architecture: The stat hash is an implementation detail of the Stats module. Including classes should not need to know about the stat hash and access it directly. So we will now write our first bit of meta code that will enable us to read and write the stat values a bit more elegantly.

module Stats
  def self.assignable *stats
    stats.each do |stat|
      define_method "#{stat}=" do |value|
        @stats_hash[stat]= value
      end
      define_method "#{stat}" do
        @stats_hash[stat] || 0
      end
    end
  end

  assignable 'max_health', 'constitution'
end

The character constructer needs to be modified too:

class Character
  include Stats
  {...}
  def initialize
    self.max_health = get_max_health_from_items_and_other_stuff 
    self.constitution = get_constitution_from_items_and_other_stuff
  end
end

Can you still follow? We have implemented a method called assignable. This is a method that defines methods. We are calling this method with two parameters ‘max_health’ and ‘constitution’ and in return 4 methods are constructed: ‘max_health’, ‘max_health=’, ‘constitution’ and ‘constitution=’. You might wonder what the ‘=’ is supposed to mean. It is part of the method name. In Ruby, a method call may be disguised as an assignment. So self.max_health = 20 is just a call to the method max_health= with the parameter 20.

Crazy stuff? We’re just getting started :) All will make sense in the end.

Dynamic stats

Our Stats module has no way to express connections between stats. You see, at the moment, we use the Stats.assignable method to define our stats that we can then freely assign. But this is not always what we want. As a matter of fact, we can determine two different types of stats: Assignable stats and calculated stats. Let’s say, for example, that it never was our intention to be able to assign the max_health stat directly, but instead have it calculated based on other stats. Lets say, the maximum amount of health should always be two times the constitution. Nothing easier than this:

module Stats
  def self.assignable *stats
    stats.each do |stat|
      define_method "#{stat}=" do |value|
        @stats_hash[stat]= value
      end
      define_method "#{stat}" do
        @stats_hash[stat] || 0
      end
    end
  end

  assignable 'constitution' # the paramter 'max_health' is removed

  def max_health
    constitution * 2
  end
end

We have removed the assignable max_health stat and implemented the method directly. Let’s have a look how it works:

c = Character.new
c.constitution = 100
puts c.max_health
# => 200

Looks good. But I see another problem coming up: We have implemented this dynamic stat directly in the Stats module, which is shared between all classes that include it. Other classes might want to use an other factor than two to calculate the max_health stat. We need to break down the calculation to make it suitable for all possible future including classes. Lets see what we can do:

module Stats
  {...}
  def max_health
    constitution * constitution_health_factor
  end
  assignable 'constitution', 'constitution_health_factor'
end

See what we did here? Instead of hardcoding the ‘2’, we introduced a new stat that is used in the calculation. Each class may implement its own default factor for this stat.

OK, this gives us a little bit more flexibility. But how do we know that this will be enough to cover all future possible cases? Consider the following required specification: The character should be able to equip an item that will increase his max_health stat by 5% if his name begins with a ‘b’. Uh. How will we go about this? Should we modify our max_health method like so?

def max_health
  if name.start_with? 'b'
    constitution * constitution_health_factor * 1.05
  else
    constitution * constitution_health_factor
  end
end

I hope that you are protesting right now. This is ugly in so many ways.

Decorator Pattern to the rescue

Do we know a better way of doing this? Oh yes. You might be familiar with a concept that has been named the decorator pattern by the gurus of static object oriented languages. Implementing the decorator pattern in languages like Java or PHP feels quite clumsy at times. But in Ruby, it is a thing of beauty. How? Again, think about it: A module is just an object that can be created and used at runtime. So how about we try to create a module that changes the behavior of our character’s max_health method and include it dynamically? This is exactly what the decorator pattern intended but without any additional classes. And that’s exactly what we’re going to do. Here we go.

module Stats
  {...}
  def refine_stat stat, &block
    mod = Module.new do
      define_method stat, block
    end
    extend mod
  end
end

Lets see how it works:

c = Character.new
c.name = 'buhrmi'
c.constitution = 100
c.constitution_health_factor = 2

puts c.max_health
# => 200

c.refine_stat 'max_health' do
  name.start_with?('b') ? super * 1.05 : super
end
puts c.max_health
# => 210

Are you still with me? Good :) What did we do here? Well, once you have embraced the idea that modules and classes are just objects, it becomes quite easy to understand: We have created a way to create a new module at runtime and extend the object with it. The method refine_stat takes two parameters: a stat name and a block of code (defined by do..end). It then goes ahead and creates a new module with exactly one method with the name of the stat and the block of code for the method’s body. After the method did its work our class hierarchy looks like this:

Lets cover this again: Once we created the module using Module.new we assign it to the local variable mod. Then we call extend mod on our instance. Ruby then creates a new anonymous class and sets it as the class of our instance, while the old class serves as the superclass for the new class. This new class is also called an eigenclass, metaclass or singleton class, but they are all the same thing. Once the class is created, the module is included by the anonymous class.

We now have created a framework that allows us to be extremely flexible in the way how character stats are being generated. All thanks to meta programming. Yay. I hope that you were able to get a good idea what’s possible with meta programming and understand why I love it. If you never programmed this way before it takes time to swallow these concepts. Just give it a try. Comments and questions are welcome, as always. Thanks for reading!

Open Questions

Performance
The question about performance always comes up. As soon as the stat system gets bigger and more elaborate, the call hierarchy to calculate a single stat becomes really deep. To circumvent the performance problems that arise from this you may want to implement a technique called result caching. This is quite easy to do in Ruby and is left as an exercise for the reader.

Order of inclusion
The results of your methods vary depending on the order of adding refinements to stats. Consider these two examples:

# First example:
c1 = Character.new
c1.constitution = 50
c1.refine_stat 'max_health' do
  super + 50
end
c1.refine_stat 'max_health' do
  super * 2
end
puts c1.max_health
# => 300

# Second example:
c2 = Character.new
c2.constitution = 50
c2.refine_stat 'max_health' do
  super * 2
end
c2.refine_stat 'max_health' do
  super + 50
end
puts c1.max_health
# => 250

Conclusion: You need to carefully consider the order of your refinements.

Isn’t this dangerous?
Well. The possibilities meta programming opens up seem to be endless. Joining on the way are endless possibilities to do something wrong. However, you might have heard this quote before: “With great power comes great responsibility.” You’ll find that almost every ruby project is covered in a huge pile of documentation and test cases, and that the maintainers are open and communicative. This is what is so great about the ruby community. You’ll find these programmer traits missing in most other communities. So this “dangerousness” is almost never a problem.

Permalink 13 notes Comments

blog comments powered by Disqus