Main Menu
  • Welcome to The RPG Maker Resource Kit.

Concepts in RGSS/2/3 ~ Questions and Discussion

Started by modern algebra, August 09, 2011, 11:05:24 AM

0 Members and 1 Guest are viewing this topic.

modern algebra

Concepts in RGSS/2/3
Questions and Discussion



This topic arose out of a discussion concerning metaprogramming in the Gradual Leveling thread (that is also the context for the first couple of concepts) and I felt it would be a good idea to have a general thread where we could mutually explore neat concepts and useful tricks available in Ruby. While there is a lot of documentation for Ruby out there, another focus of this thread should be on how they can be used to make better scripts in the context of RPG Maker, and as such I hope that it can become a useful resource for scripters.

This is not a place to learn how to script nor to ask specific questions on how to accomplish a particular goal (things like that will be split from this thread and given its own topic) - rather it is meant for more general conceptual matters. If you want to know how to use a particular concept you've seen in other scripts or what the logic behind certain bits of code are, then feel free to ask them here. It should ultimately be not just about how it works and how to use it but also why it works. On the other side, if you have a good understanding of a particular concept that is not widely used and think it would be a good idea to share it with others, then post it here. Naturally, none of us (or at least very few) are professionals, myself included, and as such, the idea should be to create a friendly atmosphere of mutual exploration and dialogue which, hopefully, will leave us all better scripters. If anyone (probably me) makes mistakes or does something you do not understand, feel free to correct him/her or ask for clarification. No question is too stupid. Well, some are, but we'll be polite about it and just split it out of the thread :P

Also, be creative - it is meant to be a discussion, not necessarily a lecture - feel free to introduce a concept and have others work out its potential uses in RGSS/2.




Concepts Discussed

modern algebra

#1
A fun little trick that you might find helpful is that when you have a lot of methods that are virtually identical (as you have with base_atk, base_agi, etc...), you can use this in order to do it all at once (and it makes it so that if you ever need to change the method, you only have to do it once)


stats = [:base_maxhp, :base_maxmp, :base_atk, :base_def, :base_spi, :base_agi]
stats.each_index { |i|
  alias_method ("tds_gradual_leveling_game_actor_#{stats[i]}".to_sym, stats[i])
  define_method (stats[i]) {
    return self.send ("tds_gradual_leveling_game_actor_#{stats[i]}") + gradual_parameter (i)
  }
}


It sacrifices readability a little bit, but if, for instance, you had made a mistake, you would only need to change it the once instead of six times. I find it is a useful trick, personally. In this case, you didn't need to pass arguments, but if you had wanted to, you would just put |arg1, arg2, ..., argn| after the { in the define_method () line. I haven't figured out a way to pass a block through like that though (except with eval, but that's neither here nor there). You can also pass the arguments through the send method like so:
self.send ("tds_gradual_leveling_game_actor_#{stats[i]}", arg1, arg2, ..., argn)

While in this case it would only be for your own convenience (it wouldn't improve performance in any way), a better way to use metaprogramming like this is, in my opinion, to allow for user input (though you would need to take some precautions in that case, like checking to see if the stat they asked for is defined in the first place). While not in this case since there would be a problem with passing the correct index to the gradual_parameter method, you could use this metaprogramming to allow the user to define for which stats you wanted to alter things. This could be useful for compatibility with other scripts that might add new stats. Again, it is not possible in this case without altering the gradual_parameter method, but if it were another script where you were just altering the stats in some other way (like, for instance, multiplying it by 1.25 whenever X state is applied), then you could allow the user to define for which stats the script should apply, and that would allow them to add stats from other scripts.

Also, you don't need the unless $@ for your aliases in this case since they are redefined in the original class. I can explain that if you want.

All in all though, a nice idea and well implemented. Good job.

pacdiggity

Or, you could use Crtl+H and press replace all. But I think your idea's better...
I tend to not use crazy tricks like that, mainly because I don't quite understand it. But basically that thing you posted, MA, was another way of sayingstats = [:base_maxhp, :base_maxmp, :base_atk, :base_def, :base_spi, :base_agi]
for method in stats
  alias_method("tds_gradual_leveling_game_actor_#{stats[method]}".to_sym, stats[method])
  define_method(stats[method]) {
    the rest of that
  }
end
if I am correct?
And, could you explain the $@ thing? I know its purpose, but not when it's appropriate. Tell us a story, great one.
it's like a metaphor or something i don't know

modern algebra

#3
Umm, no - this is one case where the block is necessary and operates differently than that loop would and so was iteration by index (in this case, since it is used as an argument for the gradual_parameter method).

However, I am at work right now. I will try to get back to you when I'm on my lunch break.

I'm not a great one though  >:(

pacdiggity

*writes everything in notebook*
What in the hell would I do without you?
it's like a metaphor or something i don't know

modern algebra

#5
First, I would just like to remind you that I am not, by any means, a professional. I do this as a hobby, and as such am neither formally trained nor particularly skilled. Basically, when I come across some new problem or concept I just play with it until I find an explanation for its behaviour that satisfies me. However, I am kind of an idiot and am satisfied fairly easily. I am frequently wrong and QED'd to be so when in the company of people who actually know this stuff. So even when I sound like I know what I'm talking about, don't believe it.

Anyway, first things first, it was useful to iterate by index since the gradual_parameter method took it as an argument - I think you just overlooked that so don't think you were asking about that.

More importantly, there is a subtle difference between using a block and a for loop, and that is basically that blocks introduce a new lexical scope, such that variables defined within a block are not available outside of it. In other words:


for i in [0, 1, 2]
  a = 0 if !a
  a += i*3
end
p a # -> 9



[0, 1, 2].each { |i|
  a = 0 if !a
  a += i*3
}
p a # -> ERROR : a is an undefined local variable


Moreover, when iterating like that, variables newly defined in the block are "redefined" for each iteration. In other words, the a defined in the 0th iteration is not the same a in the 1st iteration. So, if I were to put p a after a += i*3 (in the block example), you would find that it would be 0 on the 0th iteration, 3 on the 1st iteration, and 6 on the 2nd iteration (not 9). It doesn't carry over and, similarly, the i I define as the iterator is not the same object either.

I always use .each for iterating through an Enumerable because I am a little dense and it allows me to better anticipate the potential values of the variables I work with, thus leading to less mistakes and unforseen side effects. In most circumstances however, the distinction between the two will be irrelevant.

Not in this case though; it matters particularly for what we are doing here since we don't want our methods to respond to alterations of the iterating variable. In other words, this:

class Bla
stats = [:a, :b, :c]
for i in 0...stats.size
  define_method (stats[i]) {
    return i, stats[i]
  end
end
end

bla = Bla.new
bla.a # -> 2, :c
bla.b # -> 2, :c
bla.c # -> 2, :c


Since i ends up being 2, that is the value of i anytime you call any of those methods (:a, :b, or :c). As such, you will get 2, :c. We don't want that at all. We want the value of i used when defining our variable to be the one used when running the method. So, that's why a block is necessary:

class Bla2
stats = [:a, :b, :c]
stats.each_index { |i|
  define_method (stats[i]) {
    return i, stats[i]
  end
}
end

bla = Bla2.new
bla.a # -> 0, :a
bla.b # -> 1, :b
bla.c # -> 2, :c


I have a feeling that was a terrible explanation. Sorry.

As for $@, I have to get back to work, but I will write that up when I finish.

cozziekuns

First of all, great script TDS! I like how clear cut your script is and how elegant it's code is. It's a really nice concept too!

@Pacman: Those that only use the simplest method are losing their freedom! Practise everything and anything and your specialities will improve nonetheless. If you don't like that method, you can always revert back to your old one.

@MA: That was a great explanation and I learned a lot! I think that there's a hiccup in your code though. define_method is calling a Proc in your case, so the end should be replaced with a brace (I think?) In other words:


class Bla
  stats = [:a, :b, :c]
  for i in 0...stats.size
    define_method (stats[i]) {
      return i, stats[i]
    }
  end
end


and:


class Bla2
  stats = [:a, :b, :c]
  stats.each_index { |i|
    define_method (stats[i]) {
      return i, stats[i]
    }
  }
end


Also, I too would like to learn about "$@". Whenever I try to get a printout of the variable, I just get a bunch of gibberish like:


["Section074:17: in `update'", " Section074:17:in `main'", etc..]


Also, so we don't detract from the original intention of this topic, maybe some posts should be moved to Scripts?

modern algebra

#7
Yes, you're right about that - I was just doing it blind and I made that mistake - define_method should end in a brace.

But... back to work. I think I'll split this topic when I return as well.

TDS

We should have more topics like these.

Thank you very much for the help Modern, the whole similar methods thing was bothering me a lot, but I could not find a solution for it.

And I would like to know more about the $@.

@cozziekuns: Thanks, but I'm just a hobbyist so my code is probably far from elegant, I just try to do my best to cram as much stuff as possible within as little code as possible and still be able to read it.

Yeyinde

Special variables in Ruby:
$! # latest error message
$@ # location of error
$_ # string last read by gets
$. # line number last read by interpreter
$& # string last matched by regexp
$~ # the last regexp match, as an array of subexpressions
$n # the nth subexpression in the last match (same as $~[n])
$= # case-insensitivity flag
$/ # input record separator
$\ # output record separator
$0 # the name of the ruby script file
$* # the command line arguments
$$ # interpreter's process ID
$? # exit status of last executed child process

modern algebra

#10
I like it too - maybe when I split this I'll make it into a sticky and convert it into a general topic for discussing scripting concepts and asking questions.

Sorry, this is a lot longer than it really should be but I need to run right now and can't make it cleaner and prettier. I'll fix it up when I get back and make sure I didn't make any stupid mistakes.

In essence, you only need to put the condition on aliasing when you are aliasing an inherited method or a method in an otherwise hidden class (by hidden, I mean classes like Bitmap or the RPG module, etc). As far as I know, the following is why

I'll start with the F12 error; I find this part the hardest to explain, but basically, when F12 is pressed, the scripts in the Script Editor are re-interpreted, but the old interpretations aren't deleted in the way they would be if you closed and restarted the program. In other words, if this were our script list:

Sprite_Base
Window_Base
Scene_Base

For the purposes of describing the error, when we press F12 it is as if our script list were instead:

Sprite_Base
Window_Base
Scene_Base
Sprite_Base
Window_Base
Scene_Base

With the default scripts, it is perfectly fine to do that as there are no aliases or anything - all it's doing is redefining methods (to be the same that they were). Ie, it's equivalent to doing this:


class TDS
  def cozziekuns
    return "cozziekuns is awesome"
  end
  def cozziekuns
    return "cozziekuns is awesome"
  end
end


Redundant, but totally fine. If you call the cozziekuns method, you'll get "cozziekuns is awesome".

Now, let's take the situation where we alias it. So this:


class TDS
  def cozziekuns
    return "cozziekuns is awesome"
  end
  alias cozzie_new cozziekuns
  def cozziekuns
    return cozzie_new + " and also handsome"
  end
end


The cozzie_new alias is identical to the original cozziekuns method, so what you get is "cozziekuns is awesome and also handsome". Great. Now in the case of F12, it is effectively:


class TDS
  def cozziekuns
    return "cozziekuns is awesome"
  end
  alias cozzie_new cozziekuns
  def cozziekuns
    return cozzie_new + " and also handsome"
  end
end
class TDS
  def cozziekuns
    return "cozziekuns is awesome"
  end
  alias cozzie_new cozziekuns
  def cozziekuns
    return cozzie_new + " and also handsome"
  end
end


But this is also totally fine - since cozziekuns was redefined again to be
  def cozziekuns
    return "cozziekuns is awesome"
  end
before the second alias, it is only that which is aliased and so again, there is no recursion error. So that's why the "unless $@" was unnecessary in TDS's script. all of those base_stat methods were redefined back to normal in Game_Actor, so when aliased, it copied a method which had no reference to the alias name.

So, in essence, there is no reason to prevent the re-aliasing; it does no harm, it does no good; it does nothing. It aliases the exact same method it would have if it were the first time going through.

The problem arises when the original method is not redefined, and, as a result, the script aliases the method as altered (that has reference to the alias name). In other words, when this is effectively happening:


class TDS
  def cozziekuns
    return cozzie_new + " and also handsome"
  end
  alias cozzie_new cozziekuns


There, the alias is aliasing a method which calls it, ie, it is:


  def cozzie_new
    return cozzie_new + " and also handsome"
  end


I think it's fairly obvious why this is a problem - when cozzie_new is called, it can never complete because it is recursive with no way to ever finish. As such you get the "F12 error", which is just that recursion error when it occurs as a result of the reinterpretation of the scripts.

In RGSS/2, there are at least two ways this can happen: (1) when you are aliasing a method in a "hidden" object; and (2) when you are aliasing an inherited method that is not already redefined in the class (ie, only defined in its superclass or inherited module).

The first scenario is pretty straightforward. Unlike the scripts in Scripts.rvdata, the "hidden" classes (Bitmap, Sprite, Window, etc.) aren't reinterpreted when you press F12. As such, this is what is effectively happening (something nonsensical):


class Bitmap # The actual method
  def disposed?
    # Whatever
  end
end

class Bitmap # Your edits
  alias ma_disposed_new disposed?
  def disposed?
    return false # if whatever
    return ma_disposed_new
  end
  alias ma_disposed_new disposed? # Reinterpretation
  def disposed?
    return false # if whatever
    return ma_disposed_new
  end
end


As such, ma_disposed_new now calls itself without end when it is called and we have our recursion error.

For the second scenario, consider this:


class TDS
  def cozziekuns
    return "cozziekuns is awesome"
  end
end

class Pacman < TDS
  alias cozzie_new cozziekuns
  def cozziekuns
    return cozzie_new + " and also handsome"
  end
end

class TDS
  def cozziekuns
    return "cozziekuns is a pumpkin"
  end
end

t = TDS.new
p = Pacman.new
t.cozziekuns # -> "cozziekuns is a pumpkin"
p.cozziekuns # -> "cozziekuns is awesome and also handsome"


As you can see, the alias in Pacman retains the first definition of cozziekuns from TDS and naturally ignores the second (because the alias is created before the method is ever redefined). From this, it should be apparent why the "re-"interpretaton forced by F12 causes a problem. When it redefines the method in TDS, that has no impact on the Pacman class. As such, on the re-interpretation, when it gets to the alias, the current cozziekuns method in Pacman (since it had already been defined on the first interpretation and was not redefined since) is:

  def cozziekuns
    return cozzie_new + " and also handsome"
  end


And so when the alias for cozzie_new happens, it is that method it aliases. So that is why the recursion error happens in that scenario.

Hopefully that explains when a fix is necessary and why.

The Fix

So, in those scenarios is where the "unless $@" fix comes in - putting that will prevent the second aliasing, thus the alias name will still refer to the original definition and the recursion error will not occur. So, then the question is, what is $@ and why does "unless $@" prevent the recursion error?

The answer to that is easier. As Yeyinde mentioned, $@ contains the backtrace for the last exception raised.

When you first start the program, $@ will be nil, which is why "unless $@" will not prevent the initial aliases when first "loading" the scripts. The reason "unless $@" works to avoid stack errors later is because when F12 is pressed, it throws the "Reset" exception to tell the program to reset, which means that the second time the program interprets the scripts, $@ will have the backtrace for the Reset exception and therefore will not be nil, so the "unless $@" will be true and the alias will not happen again.

Personally, I think the reason hardly anybody knows why or when checking "unless $@" is necessary is precisely because it's checking $@ which, though reliable and related to the ultimate cause of the recursion error, doesn't really give the reader of the script much of an indication of why the recursion error happens in the first place. I think it's bad form to use it and instead use:unless self.method_defined? (:alias_name)It's a little uglier/longer and a little slower (totally negligible) than just checking $@, but I think it is more helpful.

pacdiggity

Damn you Yeyinde and your knowledge!
@MA, that was a fantastic explanation. I understood completely why I was wrong. And TDS, it is a good script. MA just makes it easier for you to edit.
it's like a metaphor or something i don't know

pacdiggity

Good idea, MA. This thread will be a good reference.
Here's something I hear misinterpreted a lot: thinking class variables are the same as instance variables or global variables. They're different.
We know that when creating an object, we use the class as a blueprint and create an instance with it. This means we can have multiple at the same time and work with them. We know that a global variable begins with a $ and is accessible from anyway, both getter and setter. And an instance variable begins with an @ sign and is open to all methods within an instance of a class.
A class variable begins with two @ signs (@@) and is accessible throughout ALL instances of a class, not just the one. I can't think of a practical example to demonstrate this with, so if anyone could that would be fantastic. I just wanted to clear this up.
The one other thing I want to make sure everyone knows, and I know that everyone on this thread does, are attrs.
class Thing
  def initialize(n)
    @n = n
  end
  def n
    return @n
  end
end
The n method can be expressed much quicker with one line:
class Thing
  attr_reader :n
  def initialize(n)
    @n = n
  end
end
This is a 'getter' method, which will return the value of the variable.class Thing
  def initialize(n)
    @n = n
  end
  def n=(i)
    @n = i
    return @n
  end
end
The n= method can also be expressed in one line:class Thing
  attr_writer :n
  def initialize(n)
    @n = n
  end
end
Finally, both methods can be written either as this:class Thing
  def initialize(n)
    @n = n
  end
  def n
    return @n
  end
  def n=(i)
    @n = i
    return @n
  end
end
Or, much more easily:class Thing
  attr_accessor :n
  def initialize(n)
    @n = n
  end
end

Now nobody has an excuse.
it's like a metaphor or something i don't know

cozziekuns


class Foo
  def initialize
    @@variable = 5
    @variable = 6
  end
end

class Bar < Foo
  def variable_class
    p @@variable
  end
  def variable_instance
    p @variable
  end
end

b = Bar.new
b.variable_class # => 5
b.variable_instance # => nil


I think this is what you were getting at, Pacman.

Zeriab

Pressing F12 behaves differently in RGSS and RGSS2.
In both cases it seems that a Reset exception is create if it does not exist which may look like this:
class Reset < Exception
 
end


It then throws this error which you can handle if you want. For RGSS either Graphics.update or Graphics.transition or throws it.
For RGSS2 it looks like the exception can be thrown at any given point.

Assuming you don't handle the exception it looks like the editor simply starts over from the first section keeping all global variables, classes and etc, although it may dispose visual elements. I haven't checked what happens if you store a reference to one in a global variable.

The most visible error caused by the behavior of reseting without properly cleaning the enviroment is the endless recursion case due to it's game crashing nature, but it can easily cause more subtle behaviors simply because the assumptions you have about a global variable at the start of the game may not hold after you press F12. Just consider new game plus style script. Who knows how they can malfunction. In RGSS2 it's even worse because there is a much higher chance of a global variable getting corrupted.

For RGSS2 I would always recommend using an F12 cleaner which simply starts a new instance of the game and closes the current one.

*hugs*

cozziekuns

That was very informative, Zeriab. Thanks for the input!

Anyway, I've been playing around with the sort method for arrays; specifically when you make a comparison without a block.
array.sort do |a, b|
#stuff
end

My question is: how exactly does ruby sort the array? I know it has something to do with <=>, but what does it do with the value obtained by said operator?

TDS

Well, since no one has tried I'll give it a try.

This is a complete shot in the dark.

From what I know <=> compares by a -1 1 +1 sort of system.


1 <=> 0   »   1  More
1 <=> 1   »   0  Same
1 <=> 2   »  -1  Less


So it would seem that Ruby sorts the arrays based on that, by going through the values on the array and putting them in an order based on their comparative value of the arguments |a, b|.

This is just what I understand about it and someone else could probably explain it better.

cozziekuns

This isn't really a question more so it is a curious observation, but I haven't found any documentation on it (I guess because no one has been stupid enough to do it before). The problem is, what happens if I declare two sprite classes in the same variable? Example:


sprite = ::Sprite.new
sprite.bitmap = Bitmap.new(32, 32)
sprite.bitmap.fill_rect(0, 0, 32, 32, Color.new(255, 255, 255))
sprite.x = 0

sprite = ::Sprite.new
sprite.bitmap = Bitmap.new(32, 32)
sprite.bitmap.fill_rect(0, 0, 32, 32, Color.new(255, 255, 255))
sprite.x = 50

loop do
  Graphics.update
end


In this code, the variable sprite represents a 32*32 pixel block of white. I inserted a sprite into the variable sprite, and then inserted another variable. I thought that the second sprite would override the first sprite, but something odd happens. Instead, you have a ghost sprite object representing the initial bitmap. In this example, you have 2 squares instead of one on the right. So the first sprite declared becomes a ghost sprite that can't be interacted with; changing values of the variable sprite will only result in the changing properties of the second sprite, and not our ghost sprite.

Any ideas why this happens?

TDS

My take on it is that the first sprite object still exists in memory as an object marked to be removed by something called Garbage Collection which usually takes care of removing stuff like that. I don't personally know a whole lot about it though. I just know that Zeriab once told me not to directly call it, as I was doing so in a battle system to prevent a battle system from crashing the game.

If you add this to your script "GC.start" before the loop you will see that it will remove the previous sprite.

I hope someone can expand more on Garbage Collection as I would like to know more about it.

Zeriab

#19
The important part is that you loose the reference to the first sprite. What the sprite then references to is irrelevant. I cut down the code to the issue:
sprite = ::Sprite.new
sprite.bitmap = Bitmap.new(32, 32)
sprite.bitmap.fill_rect(0, 0, 32, 32, Color.new(255, 255, 255))
sprite.x = 0
foo = sprite

# Loose the reference to the sprite
sprite = nil
# Loose the other reference as well
foo = nil


When you have no references either directly or indirectly you have the garbage cleaner taking care of disposing the object eventually. How exactly it does it is transparent and it doesn't really matter except in rare situations. The only reason you notice something is that the visual element disappears whenever the sprite is disposed, not when you loose the last reference to it.

As TDS say you can force the garbage cleaner to start now though generally speaking it often slows down rather than speeds up the game. There are cases where calling it can make sense which typically are when removing the references to large numbers of objects or objects taking up significant memory. I could imagine it being beneficial to call GC after clearing the RPG::Cache for example. (They may already do that in the method, I haven't checked).

In your case it is way more desirable to dispose the sprite explicitly rather than rely on the garbage cleaner.

P.s. If you maintain a reference to the sprite it will not disappear. You can try using a global variable to see the effect. Yes, you don't have to update a sprite for it to be present. You only have to update a sprite if it changes.
P.s.s You can disable the garbage cleaner with GC.disable which is a great way to turn your game into a memory hog. (Can be useful when debugging)

*hugs*

lpz_Nq

Umm... I don't know where to ask this, so I do it here:
Are the VX-Scripts compatible to VXA-Projects?
Is there any study, about the incompatibility?
English isn't my first language, but I hope you understand what I mean.

DoctorTodd

Only some are, this is because VX uses RGSS 2 and VXA uses RGSS 3. So ya there is definitely a study on it.  ;)
If you have any more questions please ask them in the script section.

cozziekuns

Hey guys, I have a random question that's been bugging me about ruby. What exactly does this code do?


module Cozziekuns
class << self # This is the part I need clarification on
 
end
end


Specifically, I want to know what exactly the << operator does, and what exactly it is doing. Is it transforming the module into a static class?

Tsunokiette away!

modern algebra

#23
Basically, it's syntax which sets self to the metaclass of the module, thus allowing you to operate on the metaclass. The metaclass is an instance of Class and is like any other class in that you can give it methods, but it only attaches to the object itself (in this case, Cozziekuns), and it is effectively invisible (ie. Cozziekuns.class #=> Class). All modules and classes have a metaclass.

As an answer to your particular question: no, it is not transforming the module into a static class - it is allowing you to operate on its metaclass, which is distinct from the module. Cozziekuns remains a module and can be used as a module, and when a class includes a module, the metaclass methods are not included (since it's a Class). Maybe an example helps:


module Cozziekuns
  class << self
    def box
      p "I am a box"
    end
  end
  def bump
    p "I am a bump"
  end
end

class ModernAlgebra
  include Cozziekuns
end

p Cozziekuns.box #=> "I am a box"
p ModernAlgebra.new.bump #=> "I am a bump"
p ModernAlgebra.box #=> Undefined method: box


Yehuda Katz wrote a useful article on metaprogramming that is worth your time: http://yehudakatz.com/2009/11/15/metaprogramming-in-ruby-its-all-about-the-self/

It doesn't mention modules specifically, but the metaclass of a module operates in the same way as the metaclass of a class. I also highly recommend that you watch this presentation by Dave Thomas: http://scotland-on-rails.s3.amazonaws.com/2A04_DaveThomas-SOR.mp4