iPhone App Directory
RPG Maker VX
 
Gallery Members Search Calendar Help


Welcome Guest ( Log In | Register )

Collapse

>Announcement

The 2nd Quarter Contest has arrived! Go check it out in the Community Announcements now!
 
Reply to this topicStart new topic
> [-Scripting-] Event driven programming and Ruby, Learn to use the powerful "Observer" pattern
BigEd781
post Aug 16 2009, 10:29 PM
Post #1


No method: 'stupid_title' found for 'nil:NilClass`
Group Icon


Type: Coder
Alignment: Chaotic Good




I thought that I would take some time to teach those who do not know about the powerful programming paradigm known as the "Observer" pattern. Ruby does not have any built in language support for this pattern (some languages do, i.e., C# and Java), but it does provide a module named "Observer" for using it. Instead of showing you how to use that module (there is already documentation for that here), I will be implementing my own solution so that you can see how it all works.

The concept is simple really, but it does require that you understand some basic programming principles before we start. You need to understand the following terms:

1. Method (or function, subroutine, etc.)
2. Class
3. Object (aka, instance of a class)
4. Encapsulation
5. Hash Table

If you do not understand one or more of the concepts listed above, please take some time now to read up. You really do need to understand what they mean before going forward.

--------------

OK, done reading? Great, let's learn what this is all about.
In object oriented programming we strive to achieve encapsulation between our classes. Also know as "information hiding", encapsulation helps to create robust and maintainable code. When we create a class, we would like it to be responsible for one purpose. Whatever that purpose is, our class is the only class in our program which contains the logic for dealing with this responsibility. for example, let's say we are writing a class used to represent an "actor" in a video game. Since this obviously sounds familiar, we will simply be adding to/modifying the Game_Actor class in the RPG Maker VX. This class is responsible for maintaining the state of an actor. It has properties like 'hp', 'mp', 'str' (strength), and so on. It also has methods used by outside users of the class to change the state of the actor. No other class in our program is responsible for actors, only the Game_Actor class.

This type of design allows us to keep all of the logic needed to represent an actor in one place. Users of this code can call methods like "hp" to ask the actor "what is your current hp level?". Outside code does not know how the Game_Actor class keeps track of hp, nor does it care. Outside code need only know that the Game_Actor class contains that information, and that is encapsulation.

So, how do "events" and the "Observer" pattern fit into all of this? This pattern helps us to increase the level of encapsulation in our classes and reduces the chance of breaking external code by changing the internal implementation of the class. for our purposes, a HUD window will serve as a nice example. We will create a HUD that displays the current hp level of the first actor in the database. Before we create the HUD window, let's make a class that we will use to encapsulate the logic behind an "event". (Not that in this tutorial I use the term "event" to signify something happening in a class, not an in game event. For example, an actor's hp changing would fire an "hp_changed" event). So instead of writing code to explicitly check conditions such as hp changing, you can just ask an object to let you know when it happens.

Here is our event class. I have added comments to explain what is going on:

Spoiler:

CODE
class EventHandler
  
  def initialize
    # initialize our listener (client) id map.
    # this hash will map a client id to a callback function
    @client_map = { }
  end
  
  # Add a client and callback function to the client map
  def add_handler( id, func )
    # use the Object#hash method to generate our client id.
    # this method will ensure that we have a unique id for each client
    # this implementation can be changed without affecting outside code.
    hash = id.hash
    # if the element at 'hash' is nil, set it to an empty array
    @client_map[ hash ] ||= [ ]
    # add the function to the client map
    @client_map[ hash ] << func
  end
  
  def remove_handler( id )    
    # remove the client entry in our map
    # this is needed because as long as the handler is
    # in our map it will be called when the event is raised.
    # if the listener (the object that added the callback) is
    # nil we will get an exception when the method is called,
    # so give users of this code a way to detach the callback function.
    return @client_map.delete( id.hash )
  end
  
  def alert_listeners
    # calls each function in our client map
    @client_map.each_pair { |obj,funcs| funcs.each { |func| func.call } }
  end
  
end



Alright, so now we have a class that can attach and detach callback functions mapped to an id. So, how can we use this? We will add an event handler to the Game_Actor class that clients can use to map a callback function. We will add a handler to be used when the actor's hp changes. Again, comments explain what we are doing.

Spoiler:

CODE
class Game_Actor < Game_Battler
  
  # this is out EventHandler object exposed to the outside world
  attr_accessor :hp_changed
  
  alias :pre_event_ga_initialize :initialize
  def initialize( *args )
    # initialize our EventHandler object
    @hp_changed = EventHandler.new
    # if you don't know what the '*' does, google for "ruby splat operator"
    pre_event_ga_initialize( *args )
  end
  
  # method to be called when 'hp' changes.  
  def on_hp_changed
    @hp_changed.alert_listeners
  end
  
  alias :pre_event_ga_hp_change :hp=
  def hp=( hp )
    # only fire the event if the hp value actually changed
    changed = ( @hp != hp )    
    # call the original method
    pre_event_ga_hp_change(hp)
    # fire the event handling function if changed
    on_hp_changed if changed
  end
  
end



OK, so now we have added an event to the Game_Actor class, but how do we use it? Clients (users) of the Game_Actor class will subscribe to the event like so:

Spoiler:

CODE
class Client

  def initialize( actor_id )
    @actor = $game_actors[ actor_id ]
    # assign a function to the event handler.
    # this function will display a messagebox with the actor's ho whenever it changes.
    # a 'lambda' is an anonymous method.  We could have also defined a 'standard' method and passed that in.
    @actor.hp_changed.add_handler( self, lambda { print @actor.hp } )
  end

end



When the above code runs, a messagebox will be displayed whenever the actor's hp value changes. You do not need to write any logic in that class that checks the actor's hp against some cached value, you are automatically informed of the change by the Game_Actor class.

Alright, we are ready to write our HUD window class:

Spoiler:

CODE
class Window_ActorHud < Window_Base
    
  @@TEXT_ALIGN = 2
  
  def initialize( x, y, actor_id )
     # fit the width to the size of 4 digits, i.e., '0000'
     # also account for the padding on each side of 16 pixels
    width = Bitmap.new( 1, 1 ).text_size( '0' * 4 ).width + 32    
    # call base class constructor
    super( x, y, width, WLH + 32 )
    # get out actor to watch
    @actor = $game_actors[ actor_id ]
    # add our event handler.  The refresh method will be called
    # every time the event is fired by the Game_Actor class.
    @actor.hp_changed.add_handler( self, lambda { refresh } )
    # draw the initial hp display
    refresh
  end
  
  def refresh
    self.contents.clear
    # draw the actor's current hp
    self.contents.draw_text( self.contents.rect, @actor.hp, @@TEXT_ALIGN )
  end
  
  alias :pre_event_wb_dispose :dispose
  def dispose
    # remove our event handler
    @actor.hp_changed.remove_handler( self )
    pre_event_wb_dispose
  end
  
end



And this is the addition we need to add to Scene_Map to make it all work in game. Notice that we do not need to modify the 'update' method in this class. Most huds would expose an update method and, inside of that method, refresh the window if anything has changed. We do not need to do any of that because our hud refreshes itself automatically when a parameter changes due to our event architecture.

Spoiler:

CODE
class Scene_Map < Scene_Base
  
  @@HUD_TOGGLE_INPUT = Input::A  # Shift button by default
  
  alias :pre_event_sm_start :start
  def start    
    # create out hud window
    @hp_hud = Window_ActorHud.new( 0, 0, 1 )
    pre_event_sm_start
  end
  
  alias :pre_event_sm_terminate :terminate
  def terminate
    # dispose the hud when the scene ends
    @hp_hud.dispose
    pre_event_sm_terminate
  end
  
  alias :pre_event_sm_update :update
  def update
    # watch for player input to hide the hud
    update_hud_input
    pre_event_sm_update
  end
  
  def update_hud_input    
    # give the player a way to toggle the window on or off
    @hp_hud.visible = !@hp_hud.visible if Input.trigger?( @@HUD_TOGGLE_INPUT )                      
  end
  
end



Well, that's it. This should provide a good basis for understanding event driven programming. Please let me know if you guys find this confusing so that I can improve it.


--------------------
My blog - It's awesome, I assure you
QUOTE
While sloppy writing does not invariably mean sloppy thinking, we've generally found the correlation to be strong -- and we have no use for sloppy thinkers. If you can't yet write competently, learn to. - Eric Raymond

---
My awards for being so awesome
Spoiler:
Go to the top of the page
 
+Quote Post
   
Overlord_Dave
post Aug 25 2009, 09:53 AM
Post #2


Overlord of your face! Yeah, I went there.
Group Icon


Type: Coder
Alignment: Neutral Good




Very nice BigEd smile.gif. Each individual section is easy to follow, well commented etc... Though I may suggest adding a flowchart-esque summary to the end, since it took me a bit of thinking to see how it all fits together wink.gif


--------------------
Overlord_Dave's super-awesome blog





EVERYBODY LOOK: modern algebra's Script Searching Tutorial
I'm getting tired of seeing him plug this every time someone requests a script that already exists :)

BEFORE REQUESTING A CUSTOM BATTLE SYSTEM, THINK! (<-- clicky clicky)

My To-do list:
Spoiler:
  • v2.0 of Overlord_Dave's World Map (needs a complete re-write :s)
  • RTP dungeon crawler (as yet unnamed)
  • InstaGame script
  • The Corridor (this'll take a while)
  • TREv3 battle system (which will take the remainder of my life)
  • Pillars of D'Mahnia (which, due to the fact above, is as yet beyond the realm of physical possibility. Yay!)

My contributions:
Spoiler:

My tutorials:

My scripts:
Spoiler:

DRIAC System - massive overhaul of the item system, with a restricted inventory and chests which remember their contents
Overlord_Dave's World Map - highly customisable world map system
Overlord_Dave's Full HP After Battle - does what it says on the proverbial tin
Overlord_Dave's AnimSprite class - a class that makes animating sprites ludicrously simple


I did a silly test:
Spoiler:

(I AM AWAKE AT 4AM TO THE TERRIFYING UNDENIABLE TRUTH THAT THERE IS NOTHING I CAN DO TO STOP THE MONSTER)
Go to the top of the page
 
+Quote Post
   
BigEd781
post Aug 25 2009, 04:28 PM
Post #3


No method: 'stupid_title' found for 'nil:NilClass`
Group Icon


Type: Coder
Alignment: Chaotic Good




QUOTE (Overlord_Dave @ Aug 25 2009, 02:53 AM) *
Very nice BigEd smile.gif. Each individual section is easy to follow, well commented etc... Though I may suggest adding a flowchart-esque summary to the end, since it took me a bit of thinking to see how it all fits together wink.gif


That's a good idea, thanks Dave wink.gif`


--------------------
My blog - It's awesome, I assure you
QUOTE
While sloppy writing does not invariably mean sloppy thinking, we've generally found the correlation to be strong -- and we have no use for sloppy thinkers. If you can't yet write competently, learn to. - Eric Raymond

---
My awards for being so awesome
Spoiler:
Go to the top of the page
 
+Quote Post
   
Cyl
post Sep 19 2009, 03:33 AM
Post #4


inactive.
Group Icon


Type: Coder
Alignment: Neutral Good




If it came from BigEd, this is sure to be good.
I'll read it later. (last post was aug 26, I hope this is not necropost)

Thanks!


--------------------
One word:
Paradichlorobenzene

(Currently inactive)
Go to the top of the page
 
+Quote Post
   
D.elisy
post Dec 3 2009, 01:16 AM
Post #5



Group Icon






Dear Stu,

Thank you very much for time taken to send me valuable information.

With many thanks,

Saku


--------------------
Go to the top of the page
 
+Quote Post
   
BigEd781
post Dec 3 2009, 06:11 AM
Post #6


No method: 'stupid_title' found for 'nil:NilClass`
Group Icon


Type: Coder
Alignment: Chaotic Good




QUOTE (D.elisy @ Dec 2 2009, 05:16 PM) *
Dear Stu,

Thank you very much for time taken to send me valuable information.

With many thanks,

Saku



...What? (Don't answer that)


--------------------
My blog - It's awesome, I assure you
QUOTE
While sloppy writing does not invariably mean sloppy thinking, we've generally found the correlation to be strong -- and we have no use for sloppy thinkers. If you can't yet write competently, learn to. - Eric Raymond

---
My awards for being so awesome
Spoiler:
Go to the top of the page
 
+Quote Post
   
MysticTrunks
post Dec 5 2009, 06:35 AM
Post #7


Learning RGSS(2)
Group Icon


Type: Coder




So, is this for scripting inside events? Now this is what I like as I hope to script(in events if possible) my ABS.
Just read the title and was asking, if it's from ED I'm reading.

This post has been edited by MysticTrunks: Dec 5 2009, 06:36 AM


--------------------


Go to the top of the page
 
+Quote Post
   
BigEd781
post Dec 5 2009, 11:35 PM
Post #8


No method: 'stupid_title' found for 'nil:NilClass`
Group Icon


Type: Coder
Alignment: Chaotic Good




QUOTE (MysticTrunks @ Dec 4 2009, 10:35 PM) *
So, is this for scripting inside events? Now this is what I like as I hope to script(in events if possible) my ABS.
Just read the title and was asking, if it's from ED I'm reading.


An "Event" in computer science terms is simply a design pattern which helps to loosen coupling between components of your application. If you had two objects, A and B, which each needed to know some detail about the other, you can do so by creating object A such that it were able to tell object B when something relevant happens instead of B asking A constantly if something has occurred. It helps us to write more modular and maintainable code.

So, you won't be able to implements something like this directly in an event. You can always use the "Script..." event command to call into any global data object, static object, or anything defined in Game_Interpreter.


--------------------
My blog - It's awesome, I assure you
QUOTE
While sloppy writing does not invariably mean sloppy thinking, we've generally found the correlation to be strong -- and we have no use for sloppy thinkers. If you can't yet write competently, learn to. - Eric Raymond

---
My awards for being so awesome
Spoiler:
Go to the top of the page
 
+Quote Post
   
Aviose
post Jun 15 2010, 01:44 PM
Post #9



Group Icon


Type: Designer
Alignment: Neutral Good




This is great, Ed... since this is a sticky, I'll assume me thanking you isn't necro. Just wanted to say thanks for this, you're awesome.
Go to the top of the page
 
+Quote Post
   
lorddon
post Mar 4 2011, 03:39 AM
Post #10



Group Icon


Type: Artist
Alignment: Chaotic Good




You're the first person that's explained why encapsulation is necessary in a way I can understand, Ed. Thank you.
Go to the top of the page
 
+Quote Post
   
IMP1
post Mar 4 2011, 07:30 PM
Post #11


Retributionist.
Group Icon


Type: Writer




I get that the above post was a necro-, but Ed's still around, and the topic is obviously still helpful.

I remember when this was posted, all of it just sailed over my head. But I actually get it now. Ed, you have some crazy good teaching skills. You keep it simple, but still tell the reader what is actually going on, avoiding (hopefully) those cargo cult programmers.
I guess I'm proud that I've improved enough to understand this, however basic it may be.


--------------------
Go to the top of the page
 
+Quote Post
   

Reply to this topicStart new topic
1 User(s) are reading this topic (1 Guests and 0 Anonymous Users)
0 Members:

 

Lo-Fi Version Time is now: 21st April 2014 - 06:21 AM

RPGMakerVX.net is an Privacy Policy, Legal.