Improve script performance by efficiently evaluating formulas

Formulas are one of the most powerful features available to RPG Maker game developers. They allow developers to enjoy the full capabilities of the Ruby scripting language while keeping their databases and events separate from their scripts.

My old way of writing scripts was to simply use the eval command that Ruby provides. This is great, as you can evaluate any (valid) formula using the scripting engine, but it comes with a performance cost of actually evaluating your formula, which is not cheap.

Rather than constantly evaluating a formula over and over again, however, what if there was a way to evaluate it once, and then simply re-use it later?

This tutorial shows one way to achieve this with a few minor tweaks to your scripts are written.

Introducing Lambda

In Ruby, there is a function called a lambda.

Suppose we have a method that takes two variables, adds them together, and then returns the result. This is commonly called addition. But to make it more relevant to RPG Maker, let’s say it takes a base param value and adds a bonus to it.

def add_bonus(base, bonus)
  return base + bonus
end

Now we can call this method and we will get what we expect

puts add_bonus(100, 50)  # prints out 150

Instead of defining a method, we can use lambda:

a = lambda {|base, bonus| base + bonus }
puts a.call(100, 50)  # prints out 150

Note that instead of defining a method, we simply created a lambda and called it. Rather than defining a method in our script, we have created something that achieves the same result at run-time.

Handling formulas

Here is an example of a method that evaluates a condition. This was how I used to write all of my scripts that took a formula as a condition and returned true or false.

class RPG::State
  def can_add?(formula, battler)
    eval_add_condition(formula, battler)
  end
  
  def eval_condition_met?(formula, a, v=$game_variables)
    eval(formula)
  end
end

This method is used when I am checking whether a state can be added to the specified battler and will likely be called in add_state. Note that it is very simple: pass it the battler you would like to check, and then evaluate the formula. If the formula references the variable a then it will be evaluated within the context of the battler you passed in.

A problem with this code is that it is evaluating the formula every time the condition needs to be checked. This is unnecessary and only cuts into our performance.

Using Lambdas with Formulas

You have seen that we have used lambda to create something that functions very similar to a method. This can be used with formulas as well.

The technique here is to evaluate the formula once as a lambda, and then store it for future use. Because the formula is specified by the user, you will need to eval the entire lambda in order to properly evaluate the formula.

class RPG::State
  def can_add?(formula, battler)
    eval_condition_met?(formula, battler)
  end
  
  def eval_condition_met?(formula, a, v=$game_variables)
    @cond_met_formula ||= eval( "lambda {|a,v| #{formula} } ")
    @cond_met_formula.call(a,v)
  end
end

Now, rather than evaluating the formula everytime, we can simply evaluate it once, store it, and then call it when we need to. Note the order of quotes, braces, and parentheses as you might run into syntax issues if you start doing this yourself. But those are easily fixed.

Note that the variables are being passed to the lambda explicitly. This is important, because any local variables are bound to the lambda when it is defined, so if you had omitted the a for example, then any evaluation of the formula will use the battler that you first passed in, rather than the battler that you are calling it with.

Performance

Lambda might seem cool and new, but at this point you might be wondering what are the benefits of doing it. After all, it just looks like we’re changing our simple eval call to a very complicated mix of punctuations.

To demonstrate the effects of evaluating a formula over and over again compared to evaluating it once as a lambda and then calling it, consider the following piece of code:

require 'benchmark'

$game = [1,2,3]
Runs = 1000000
Formula = "$game[0] + 3"

def normal_eval
 eval(Formula)
end

def lambda_eval
 @lamb ||= eval("lambda { #{Formula} }")
 @lamb.call
end

Benchmark.bm {|x|
 x.report("Normal") { Runs.times { normal_eval }}
 x.report("Lambda") { Runs.times { lambda_eval } }
}

When you run this code, it will show you the amount of time it took to evaluate a formula a million times, and then the amount of time it took to create a lambda and then call it a million times.

In order to run this, you will need a standard installation of Ruby on your system. RPG Maker can’t run this out-of-box. These are the results I get:

     
           user     system      total        real
Normal    9.111000   0.015000   9.126000 (  9.626000)
Lambda    0.343000   0.000000   0.343000 (  0.334000)

The “normal” eval took more than 30 times as long to complete the task than the lambda eval. Granted, this assumes you are doing a million calls in your own code for some reason, which doesn’t seem very reasonable.

However, remember that there are some formulas that may be evaluated at every frame, and this is required in order for correctness. In these cases, it would be nice if each formula check was performed as fast as possible to avoid performance issues.

Traps to avoid

If you are convinced that lambdas are a better way to handle formulas and decide to change your code to implement this pattern, there are some things that you should keep in mind.

They cannot be serialized. This means that if you try to save a lambda in your game, it will fail. A common scenario is when you store the lambda with a game object that WILL be dumped to a save file (such as Game_Actor, Game_Map, and so on). If you have formulas that are stored with these objects, you will need to find a way to store the lambdas externally to avoid having them dumped to a save file.

Conclusions

Hopefully this will provide you with a tip on how to write more performance-efficient scripts. If you like formulas as much as I do, then you might consider looking over your own code and see if there are some optimizations that could be made.

References

You can read more about lambdas, procs, and methods here: http://eli.thegreenplace.net/2006/04/18/understanding-ruby-blocks-procs-and-methods/

You may also like...

6 Responses

  1. Hellen says:

    Hi, after reɑdіng this amzing piecе of writing i am as welⅼ happy tⲟ share my famoliarity here with colleagueѕ.

  2. і would love to sip a cup of green tea each morning because it cߋntains L-theanine which
    caⅼms tthe mind~

  3. Ancurio says:

    At first I was a bit confused about what was going on, but then I noticed that the entire point of the article is to compile the formula code to bytecode once and reuse it instead of compiling it every time with eval.

    I think my confusion stemmed from you using the word "evaluate" meaning the Kernel call 'eval' (which compiles and runs a piece of code), not actually evaluating the formula against a set of parameters, something that does need to happen every frame. This is a key difference that you might have wanted to stress a bit more IMO.

    • Hime says:

      You're right, I used the word "evaluate" fairly loosely which may cause confusion (as it evidently did).

      In fact throughout the entire article I do use the term "evaluate" to mean both things, which makes it even more confusing.

      I will use the term "eval" to refer to Ruby's eval function, and "evaluate" to mean evaluating the formula using a set of arguments.

  4. RogueDeus says:

    I want to understand this…. lol

    I really do.
    Baby steps…

Leave a Reply

Your email address will not be published. Required fields are marked *