Ik_guy

Geekskillz_logo1


"Everything that boots is beautiful."


Jul 20
2007

The Ruby Is The Message

Tags: Programming   Ruby on Rails


I found an interesting presentation about Ruby aimed at Java programmers: 10 Things Every Java Programmer Should Know About Ruby. It says it isn't trying to convince you that Java sucks and Ruby will make you a much happier programmer, but you might start to feel that way by the time you get to the last point. I don't think Ruby can replace Java, but that doesn't stop me from wishing. Ruby is an object-oriented language, but differs from Java's implementation in some important ways.

Number 8 was new information to me, and I had to think about it for a while. It says that almost everything in Ruby is a message. When you call a class method, that call is a message. The method and its arguments are the message, and the class (or instance of the class) is the destination of the message. The class and the message are not joined to each other in any special way. The message could have just as easily been directed at another class object. I now think of Ruby classes this way: Classes are written to receive messages.

Is this only a distinction without a difference? I don't think so. I'll try to illustrate with an example.


A while ago, I was working on a text-based MUD (multi-user dungeon, the ancestor to the modern MMORPG). I was philosophizing about where actions belonged in an object-oriented language. To unlock a door with a key in the game, how would the code be written? door.unlock(key)? key.unlock(door)? unlock(door,key)? use(door,key,unlock)? I argued against the first two examples, for reasons I can't remember. Looking at them side-by-side makes it clear that making one choice over the other may limit your design for any future features. But in practice, either one will probably work fine, so just make a choice.

Now I'm thinking about it again in terms of Ruby's messages. What if a game action was implemented as a message that could be sent to any game object? Would that solve anything? Does it give a level of abstraction that opens up a game's design? Let's see.

I made an example where we've got a "Creature" class, which represent any living thing, including the players, and a "Door" class, which is just a door. Then I implement an explosion in the room that is performed on everything in the room. Since explosions hurt, I will then apologize to everything in the room.

I'm not going to create an "Explosion" class, or an "Apology" class. I'm not even going to make it more abstract by making "Attack" or "TextMessage" classes. I'll just make one class to encapsulate them all: ActionRecorder. The ActionRecorder is like a proxy. You record the "message" (ie, the explosion and apology) into the ActionRecorder, and then perform the action on any other class. This creates an instance of the action that can be reused, serialized, stored, logged, or whatever you want. A game action can have multiple effects, so the ActionRecorder will store each effect as a Ruby message.

Enough blah blah blah, it's time to see some code. Here's the ActionRecorder, the most interesting part of the code (this is taken from the presentation):

class ActionRecorder

def initialize
@messages = []
end

# Record all messages received, using this method:
def method_missing(method, *args, &block)
@messages << [method, args, block]
end

# Send the messages to an object
def perform_on(obj)
@messages.each do |method, args, block|
obj.send(method, *args, &block)
end
end
end
Note that method_missing is a built-in method that all Ruby classes have. It gets called automatically if you call a class for a method that it has not defined. More specifically, it catches the "NoMethodError" exception. Also, all Ruby classes have the send method, which receives all messages (aka, method invocations) and passes them to the methods. I assume that the send method is what throws the NoMethodError exception.

So, the ActionRecorder is taking advantage of method_missing to record all messages that an ActionRecorder instance receives. I'm not sure how you would do this in Java without getting compiler errors. It's probably possible, but won't be as elegant as this Ruby code.

Now let's create some game objects:

class Creature

# Creatures should have names:
def initialize( name )
@name = name
end

# Define some actions that can happen to Creatures:
def physical_damage(num_points)
puts "#{@name} is hit for #{num_points} points!"
end

def fire_damage(num_points)
puts "#{@name} is burned for #{num_points} points!"
end

def love_message(msg)
puts "#{@name} feels the love."
end

# Ignore actions that can't happen to Creatures:
def method_missing(method, *args, &block)
# Think carefully about using this method!
end
end

class Door
# Define some actions that can happen to Doors:
def physical_damage(num_points)
puts "Door sustains #{num_points} points of damage."
end

def fire_damage(num_points)
puts "Door is burned for #{(num_points*1.5).to_i} points, and catches fire!"
self.apply_damage_over_time(1, 10) # not implemented
end

# Ignore actions that can't happen to Doors:
def method_missing(method, *args, &block)
# Think carefully about using this method!
end
end
This implementation is a bit sloppy. I would probably implement a "DamageEvents" module and apply it to those two classes, from which they would get the physical_damage and fire_damage methods, but whatever. Also, the method_missing method should not just ignore all illegal methods. There are probably cases where you will want to raise an exception if an object receives an illegal method message. But let's just go with it for the example.

Now, how do you perform actions on the objects? Let's have some action!

# ------- Test the classes ----------------------


puts "Start..."

# Populate the game with stuff:
human = Creature.new("Neil")
door = Door.new

# Put stuff in a room:
things_in_room = [human, door]

# An explosion happens in the room!
explosion = ActionRecorder.new
explosion.physical_damage 2
explosion.fire_damage 10

# See what happens to everything in the room:
things_in_room.each do |thing|
explosion.perform_on thing
end

# Apologize for the explosion
apology = ActionRecorder.new
apology.love_message "Sorry!"

things_in_room.each do |thing|
apology.perform_on thing
end

puts "Done"
And here's the output:

Start...

Neil is hit for 2 points!
Neil is burned for 10 points!
Door sustains 2 points of damage.
Door is burned for 15 points, and catches fire!
Neil feels the love.
Done
I definitely like this way of coding this example! The code is written almost in english. Look at the definition of the explosion. It doesn't get much more readable than that! Also, "explosion.perform_on thing" is very easy to read. This is the kind of code that makes a programmer smile.

I can see more ways that the ActionRecorder will facilitate new features. Here are some thoughts about how to extend this type of proxy class:

- ActionRecorder can log all messages it receives, or send the messages to a Logger class that can choose which methods to log and which to ignore, in exactly the same way that we implemented the Creature and Door classes.
- A similar recorder can be used to execute scripted effects. A ScriptRecorder class. So, if your game's content developers write scripts in Lua or some home-grown script language, you can read them in and record them as ScriptRecorder objects that can be applied to game objects. You can write a Lua script that defines the explosion, read it in to a ScriptRecorder, and you've got the implementation of that script done. (Actually, I would use Ruby as the scripting language, so nevermind...)
- In-game chat messages that pass through a recorder (ChatRecorder?) could be parsed, logged, or blocked. Messages may need to go to some players, but not others. Whispers, guild chat, party chat, in-game languages, etc.
- Convert in-game chat messages to telnet protocol strings before sending them over the network.

What do you think? How would you take advantage of this?



StumbleUpon This! Bookmark This Article Digg This Story

  Tatapneumma, on Sunday, November 11, 2007 at 11:46 Eastern Standard Time:
spam removed... scram, spam!!


Post a comment:

Name:
E-mail: (will not be sold or published)
Website: (Optional)

Your comment:

Are you human?
Please enter the
text in the picture:


About Me

ThinkGeek

Feed-icon Technorati


Loot For Geeks:
4inkjets Great Prices and Best Quality!
Man's Wig! All size heads! Handsome! Sideburns! Modacrylic Fiber!
Protection
!
Make money online selling grit! Famous men sell grit!