SLIP INTO RUBY PART 3: MAKING A SCENE CONTINUED.

Also known as "Holy crap! Trihan actually finished the next bit?"

  • Trihan
  • 02/11/2015 11:27 PM
  • 7106 views


Hello class! I'm back! I know I promised this just a liiiiiittle bit earlier, but better late than never right?

...right?

Well anyway, when last I left you we had gotten to the point in making our bestiary where it looked like this.



If you hadn't been following along to this point and just want to jump in from this part, here's what your code should look like so far.

class Scene_Bestiary < Scene_MenuBase
  def start
    super
    @list_window = Window_EnemyList.new(0, 0, 200)
    @list_window.set_handler(:cancel,  method(:return_scene))
  end
end

class Window_EnemyList < Window_Selectable
  def initialize(x, y, width)
    super(x, y, width, Graphics.height - y)
    data = []
    self.index = 0
    activate
    refresh
  end
  
  def item_max
    @data ? @data.size : 1
  end
    
  def make_item_list
    @data = $data_enemies.compact
  end
  
  def draw_item(index)
    item = @data[index]
    if item
      rect = item_rect_for_text(index)
      draw_text(rect, item.name)
    end
  end
  
  def refresh
    make_item_list
    create_contents
    draw_all_items
  end
end


Now we're going to make the bestiary look a bit more like this.



It might not look like much, but it's going to take a fair bit of work to add the enemy image, so let's get started.

The first thing we're going to do is add a new method to our existing Window_EnemyList class so that the window we're about to make is able to tell which enemy is currently highlighted:

def item
  @data && index >= 0 ? @data[index] : nil
end


This is what's called an inline if, or a shorthand if statement if you prefer. The syntax is [condition] ? [true clause] : [false clause]. In this case, the condition is that the @data array has something in it and the index of the window is greater than or equal to 0, which means an enemy is highlighted. If this condition is true, the method returns the indexth element of @data, otherwise it returns nil. If we didn't have these checks in place, the game would crash under certain circumstances (if we had no enemies in the database, for example). It's not like you're going to have a bestiary in a game with no enemies, but it's good coding practice to plug up all potential holes in your code, even ones that are unlikely to actually occur.

Now that we have this, we can create our actual enemy window. Just like with the bare bones of the enemy list the creation of the window itself is easy:

class Window_EnemyBattler < Window_Base
  def initialize(enemy_window, x, y, width, height)
    super(x, y, width, height)
    @enemy_window = enemy_window
    refresh
  end
end


Note that we're including an extra parameter in the constructor: enemy_window. Why? Because we want to link the battler window to the list of enemies, otherwise there would be no way for it to know which enemy was highlighted in the list.

And so it doesn't crash, we'll make the refresh method as well.

def refresh
  contents.clear
end


To see the results of our handiwork, add the following lines underneath the ones in Scene_Bestiary pertaining to @list_window:

@battler_window = Window_EnemyBattler.new(@list_window, 200, 0, Graphics.width - 200, Graphics.width - 300)
@battler_window.viewport = @viewport


Now if you playtest the project and talk to your bestiary NPC you should see this.



Now let's make it much cooler and have it show the graphic of the selected enemy!

First we want the refresh method to be able to figure out which enemy is selected, so add the line
@enemy = @enemy_window.item
to it. This will set the instance variable @enemy to the item property of @enemy_window, which we'll know (as we just added the method for it) will return the currently-selected piece of data.

Next we want to have a method that allows us to figure out what battler graphic needs to be drawn, so add the following method to Window_EnemyBattler:

def get_battler
  Cache.battler(@enemy.battler_name, @enemy.battler_hue)
end


Cache is a module, one of the built-in scripts, which speeds up load times by returning existing objects when requested. Basically if we didn't have this, every time we referred to an image it would create a new instance of that image, which would cause your memory usage to go through the roof.

Looking up the battler method of Cache shows that it takes two parameters, filename and hue, then calls load_bitmap to find the file in Graphics/Battlers that matches. We don't really need to know too much about how this works, as long as we understand that it will load the battler graphic for us.

As we already have the information on the selected enemy in @enemy, it's a simple matter to refer to its battler_name and battler_hue properties to give Cache.battler its required data.

Now let's actually draw this bad boy.

def draw_battler(x, y)
  bmp = get_battler
  src_rect = Rect.new(0, 0, bmp.rect.width, bmp.rect.height)
  contents.blt(x, y, bmp, src_rect, 150)
  bmp.dispose
end


Okay, so bmp is being set to the battler graphic for the currently selected enemy. src_rect is set to a new Rect object with X and Y of 0, and width/height equal to that of the battler graphic. If we look up the help for blt (block transfer), it takes an x and y coordinate, a source bitmap, a source rect, and optionally an opacity value. We're basically drawing the entire battler bitmap into the contents of the window at the provided X/Y coordinate with an opacity of 150 (slightly see-through). This is of course optional and you may choose to make your battlers entirely opaque, but I find that a bit of opacity makes it look cooler. Then of course we dispose of bmp so it doesn't cause memory leaks.

Finally, add this line to the refresh method underneath where we set @enemy.

draw_battler(1, 1)


Now take a look at the fruits of your labour!



"What gives, Trihan!? You promised us centred slimes! This slime isn't centred at all! In fact, NONE of my awesome monsters are centred!" I hear you cry. Worry not, friends! We're about to fix that. Although...it's not going to be easy, mainly because we can have so many different sizes of graphic in the box. I'll give you all the code needed, and then do my best to explain what it does.

def setup_battler_graphic(x, y)
  bmp = get_battler
  rect = Rect.new(x, y, contents_width, contents_height)
  dest_rect, fits = battler_rect(bmp, rect)
  dummy_bmp = Bitmap.new(contents_width, contents_height)
  if fits
    dummy_bmp.blt(dest_rect.x, dest_rect.y, bmp, bmp.rect)
  else
    src_rect = Rect.new((bmp.rect.width - dest_rect.width) / 2, 
    (bmp.rect.height - dest_rect.height) / 2, dest_rect.width, dest_rect.height)
    dummy_bmp.blt(dest_rect.x, dest_rect.y, bmp, src_rect)
  end
  dummy_bmp
end
  
def battler_rect(bmp, rect)
  dest_rect = bmp.rect.dup
  fits = dest_rect.width <= rect.width && dest_rect.height <= rect.height
  if !fits
    dest_rect.width = rect.width if dest_rect.width > rect.width
    dest_rect.height = rect.height if dest_rect.height > rect.height
  end
  dest_rect.x = ((rect.width - dest_rect.width) / 2)
  dest_rect.y = ((rect.height - dest_rect.height) / 2)
  return dest_rect, fits
end


After this, change the first line in draw_battler to

bmp = setup_battler_graphic(x, y)


Now if you playtest, you'll notice that your graphics are centred! Let's look at why this is line by line, starting with setup_battler_graphic.

bmp = get_battler


This is just the same line as we previously had in draw_battler, which sets bmp to the enemy's battler graphic.

rect = Rect.new(x, y, contents_width, contents_height)


This creates a new instance of the Rect class with the X and Y coordinates supplied to setup_battler_graphic (which in turn get their values from the X and Y supplied to draw_battler, which as we know from the refresh method are 1 and 1), a width equal to the window's contents width (which in this case is 320) and a height equal to the window's contents heights (which in this case is 220).

dest_rect, fits = battler_rect(bmp, rect)


The great thing about Ruby is that methods can return multiple values. Here we're setting dest_rect and fits to the return value of calling battler_rect with parameters of bmp (the battler graphic) and rect (the 1, 1, 320, 220 rect we just made). To better understand what's happening here, we'd better jump over to battler_rect and go through that line by line as well:

dest_rect = bmp.rect.dup


So far so good, all we do in the first line is set dest_rect to an exact duplicate of the battler graphic's rect (136x47 in the case of the RTP slime).

fits = dest_rect.width <= rect.width && dest_rect.height <= rect.height


Okay, so fits is a boolean. Its value is true IF the width AND height of the destination rect are less than or equal to those of the rect we're putting the bitmap into: in other words, it's asking "is this graphic bigger or smaller than the box we're putting it in?"

For the example of the slime, this works out to "fits = 136 <= 320 && 47 <= 220" which is obviously true.

if !fits
  dest_rect.width = rect.width if dest_rect.width > rect.width
  dest_rect.height = rect.height if dest_rect.height > rect.height
end


If fits is false (which means the graphic is too big for the box), whichever one doesn't fit will be "cropped" by setting the desired width/height from the bitmap to be equal to the width/height of the box instead.

dest_rect.x = ((rect.width - dest_rect.width) / 2)
dest_rect.y = ((rect.height - dest_rect.height) / 2)


Simple enough, here we're setting the X and Y of the destination rect to half the difference between the box's width/height and that of the bitmap. Slotting in our values for the slime example:

dest_rect.x = ((320 - 136) / 2) = 92
dest_rect.y = ((220 - 47) / 2) = 86.5 (rounded down to 86)

So we know now that the destination rect is going to start at 92, 86 in the contents box.

return dest_rect, fits


And the final line simply returns the destination rect and the value of fits, which in this case is going to be (92, 86, 136, 47) and true.

Back to where we left off in setup_battler_graphic...

dummy_bmp = Bitmap.new(contents_width, contents_height)


Here we're creating a new instance of the Bitmap class called dummy_bmp, which is as wide and tall as the box in the enemy window.

if fits
  dummy_bmp.blt(dest_rect.x, dest_rect.y, bmp, bmp.rect)
else
  src_rect = Rect.new((bmp.rect.width - dest_rect.width) / 2, 
  (bmp.rect.height - dest_rect.height) / 2, dest_rect.width, dest_rect.height)
  dummy_bmp.blt(dest_rect.x, dest_rect.y, bmp, src_rect)
end


Okay, so if fits is true, we block transfer into dummy_bmp using the destination rect's X and Y, the battler graphic as the source graphic, and the graphic's rect as the source rect. In our example this equates to dummy_bmp.blt(92, 86, whatever the slime's graphic is called, (0, 0, 136, 47)).

If fits is NOT true, first we have to make a couple of modifications to the source rect. It's set to an X/Y of half the difference between the graphic's width/height and the destination rect's width/height, with the width/height of the rect unchanged. Let's see how this works using an example of a graphic that doesn't fit in the box, the Demon God:

The rect for the Demon God's bitmap is (0, 0, 408, 276), which doesn't fit in the box by width OR height. This means that the dest_rect's height and width will both be cropped to (320, 220) and subsequently dest_rect will end up being (0, 0, 320, 220).

After this, src_rect's X is going to be set to ((408 - 320) / 2) = 44, the Y will be ((276 - 220) / 2) = 28. So the rect will be (44, 28, 320, 220). What this means is that the block transfer of the Demon God's image will start at (44, 28) on the bitmap, then show 320 pixels in width and 220 in height. In Demon God's case this means the end point (bottom right) will be at (364, 256).

Following this step, we block transfer to dummy_bmp as before. Using the Demon God's values as example again, this equates to dummy_bmp.blt(0, 0, whatever Demon God's graphic is called, (44, 28, 320, 220)).

dummy_bmp


Regardless of whether the graphic fit or not, this method returns the value of dummy_bmp, which is the bitmap with the graphic block transferred on.

Now because setup_battler_graphic is returning a block transferred image, when draw_battler block transfers that bitmap into contents, the battler appears in the box, centred. Cool, huh?

This was a LOT of stuff, so I'm going to leave it there. I had intended on having part 3 include the enemy stats, but it's getting long enough as it is so I'll leave that for part 4, which will be coming out a lot sooner than this one did!

I've covered a lot of pretty detailed concepts here, so I imagine there are going to be at least a few questions.

As usual, comments, corrections and criticisms are welcome. Please feel free to suggest topics for further parts, too.

Posts

Pages: 1
kentona
only 90s kids will like this admin
20443
Holy crap! Trihan actually finished the next bit?
Quick! Front Page it!


If only it had been a day earlier. Welp... maybe for next week (barring anything better coming along).
Ratty524
The 524 is for 524 Stone Crabs
12637
Whoah, I just realized this existed.
These should be useful considering I'm trying to learn Ruby from the ground up elsewhere as well. Thanks for posting!
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3029
Once I finish the bestiary tutorial, what aspects of programming/particular systems would people like me to cover next?

If nobody has any suggestions what I may do is do a line-by-line analysis series on the default scripts explaining what they do.
author=Trihan
Once I finish the bestiary tutorial, what aspects of programming/particular systems would people like me to cover next?

If nobody has any suggestions what I may do is do a line-by-line analysis series on the default scripts explaining what they do.


That sounds like an excellent idea actually. It would greatly help to show us how things operate and would certainly show the many different aspects and uses of RGSS.
BurningTyger
Hm i Wonder if i can pul somethi goff here/
1289
LOL I expected the shoes on the title bar :P
Battle classes! Battle classes! I want battle classes! :3
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3029
The most recent one covers the BattleManager module. We're not going to get to Scene_Battle for a while, but it's coming!
First of all, just want to say that this is great. I've been working as a programmer for over 10 years so I have no issues with the basics or the coding logic, but having never worked in game programming or with Ruby (and my C was rusty too, mostly worked with Perl/JS/PHP) this is really helpful at figuring out what I need lack to script efficiently in RPG Maker.

There is one missing part in this tutorial though, which after trying to figure it out for a while and finally looking at your Part 4 I found out. You're missing this bit of code :

def update
super
refresh
end

Otherwise your battler window never updates and so no matter how much I went through different enemies, I stayed with the image of the first one in the battler window. It's in your full code in the Part 4, but it could be helpful for people reading this in order and actually doing the tutorials in game to have it here not to look too long for the bug it creates.

I'd also have a quick question about when "update" is called if you know. Is the "update" method of all active windows called whenever you move in one of them? Or is there something I might not notice in the code linking the battler window to the main list in a way that would tell it to call this specific window's update method when you move in the list?

Thanks!
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3029
Whoops! Must have forgotten to add the update method before I finished writing. Thanks!

Here's the way update works:

Every window has an update method, because every window class is a child of Window_Base, which has update defined as:

def update
    super
    update_tone
    update_open if @opening
    update_close if @closing
  end

Now note that every scene also has an update method, because every scene class is a child of Scene_Base, with the following update method:

def update
    update_basic
  end

Which contains...

def update_basic
    Graphics.update
    Input.update
    update_all_windows
  end

And update_all_windows contains...

def update_all_windows
    instance_variables.each do |varname|
      ivar = instance_variable_get(varname)
      ivar.update if ivar.is_a?(Window)
    end
  end

So basically, every scene calls its update method in main, which is processed every frame. This calls the update method of every instance variable of the scene that's a window, which in the Window class refreshes the cursor blink and animation.

In this particular instance, what links the windows is the following:

def item
  @data && index >= 0 ? @data[index] : nil
end

Adding the item method to Window_EnemyList allows us to get the data pertaining to the currently-selected enemy from a particular instance of the class. Then in the initialize method for Window_EnemyBattler we have

@enemy_window = enemy_window

Now you'll notice that enemy_window is a parameter taken by initialize, and when we actually create an enemy battler window in Scene_Bestiary we set this value to @list_window, which is the window containing the list of enemies. This means that the line...

@enemy = @enemy_window.item

Sets the @enemy variable of the enemy battler window to the result of calling item on @enemy_window. In this case, @enemy_window is the instance of Window_EnemyList we created, and .item will return whichever enemy is currently highlighted. Now that we've stored that in @enemy, any time we refer to @enemy in the code that deals with the battler, it's going to be using the highlighted battler as a reference.


Thanks for the fast and detailed explanation, really appreciated! It's quite clear now.
Pages: 1