SLIP INTO RUBY PART 2 - MAKING A SCENE

Part 2 of a series explaining Ruby and RGSS in a way you can understand.

  • Trihan
  • 06/28/2013 11:05 AM
  • 16411 views


Welcome back, RGSS scripters of tomorrow! Man, it's been a while since my last article, but I promised I wouldn't forget about the series and I'm finally ready to give you the next lesson. Today, we're going to cover how to actually make a new scene for your game, which is pretty much the heart of any new system you'll want to add.

The very first thing you'll want to do, in order to test things as you go and make sure everything is working as it should, is make a skeleton of your scene and implement a way to access it. Let's say we want to make a bestiary which shows info on the enemies in the game. Create a blank entry in Materials, just above Main, and type the following:

class Scene_Bestiary < Scene_MenuBase
  def start
    super
  end
end


This is pretty much the minimum you'll need for a scene. Note that it inherits from an existing class called Scene_MenuBase, which already contains some code for making the background etc. which will make life easier than having to code it yourself. However, even though this scene exists now, it's not accessible anywhere in the game. We can either access it through an event, or add it to the main menu. Adding it to the menu involves the "alias" keyword, which I'll cover soon, but for now let's just have an NPC that gives us info on monsters we've fought.

Create an event for your NPC, and in the event commands select "script". Write the following:

SceneManager.call(Scene_Bestiary)


Now playtest the game, talk to the NPC, and...well, nothing much'll happen yet. You will notice, though, that the screen goes all blurry. This tells you that you've entered your new "scene", which contains nothing right now but a blurry background.

Let's fix this by adding some windows. A window class is more-or-less self-sufficient and isn't restricted to any particular scene; once you've created the framework for a window you can create one anywhere you want. The first thing I'm going to add for our bestiary is a window that shows a list of the game's enemies, which can be scrolled through with the up and down arrow keys.

class Window_EnemyList < Window_Selectable
end


Again, this is the bare minimum you need for a window, but even if we create one in the scene it won't do anything yet. The first thing we want to do is add a constructor method, which tells RGSS what to do when first creating the window. Underneath the line beginning "class", write the following:

def initialize(x, y, width)
    super(x, y, width, Graphics.height - y)
    @data = []
    self.index = 0
    activate
    refresh
end


The part in brackets defines parameters, or variables you will pass in when calling the method which will be used somehow later. In this case, when you create this window, you will have to provide an x/y coordinate and a width.

The first thing we do is call "super". This is a keyword that will call the method with the same name from the first available parent class; in this case, it will call the initialize method of Window_Selectable. If you look at this class in the script list you'll see it also requires four parameters: x, y, width, and height, so we have to provide these when using super. Note that I've provided a height of "Graphics.height - y", which simply means "the total height of the screen minus the provided Y coordinate" and will effectively stretch the window from whatever Y coordinate I set down to the bottom of the screen.

Next we set an array called data to [], which initialises it as empty. We do this on creation of the window to make sure we don't end up with duplicate entries when populating the variable.

Next self.index is set to 0, so that the window will open with the first entry highlighted.

"activate" will set the active property of the window to true. This doesn't so much do anything by itself but it means you can check to see what windows are active at any given time.

Finally, "refresh" will call the refresh method of the window. You'll note that it doesn't yet have one, and if you tried to use the window like this it would cause a NoMethodError to close the game.

Our next port of call is to create the refresh method so that error doesn't happen:

def refresh
    make_item_list
    create_contents
    draw_all_items
end


Although create_contents and draw_all_items are not defined in this window class, they do exist in the parent class Window_Selectable, so they won't cause errors. make_item_list doesn't exist anywhere yet, though, so we need to define it.

def make_item_list
    @data = $data_enemies.compact
end


Note that instead of calling make_item_list we could just have put that line in refresh. It's really personal preference, but I would recommend that for any "block" of code that's more than a couple of lines, put it in its own method and then call that method where the block of code was. It keeps things neater. The only reason I used make_item_list here was to keep it consistent with the existing menu-type classes.

$data_enemies is a global variable containing an array of all the enemy data from the database. Compact is a new method available to all arrays which removes any empty elements from it. Basically, make_item_list will set the @data array to a list of all your enemies, taking out any blank entries.

If you were to create the window now, you'd notice that you could see a flashing cursor, but you can't move it and there are no entries. This is because we have one slight problem.

Window_Selectable is smart, but it doesn't know everything. Unless you tell it otherwise, it assumes that you want to draw 0 items in the list. We have an array called @data which will tell us how many monsters there are, but we still need to add a method called item_max to let the window know this.

def item_max
    @data ? @data.size : 1
end


If you were to create the window NOW, you'd be able to scroll through the list but the entries would still be blank. Jeez, what gives? Well, draw_all_items might exist, but all it does is call a method called draw_item, which in Window_Selectable is blank. In short, it doesn't know how to draw our list unless we tell it what to do. Let's define draw_item:

def draw_item(index)
    item = @data[index]
    if item
      rect = item_rect_for_text(index)
      draw_text(rect, item.name)
    end
end


Okay, let's walk through what we're doing here. draw_item requires a parameter, index. draw_all_items will loop through all the items in the list, so index will simply be the array element it's currently looking at.

We set a temporary variable called item to the indexth element of the @data array. We then have an if statement checking whether item actually exists, because if there isn't an indexth element there, the game will crash if we try to do something with it (specifically it'll crash on the line with item.name, because there won't be a name property to access if there's no item)

item_rect_for_text is a lovely little method available to Window_Selectable and its child classes, which basically works out the area required to display text in one "slot" of a selectable window. We need to do this because draw_text requires a rectangular area to draw the text inside, and we can't provide static values because we're looping through a list and don't know at any given moment what that text is going to be or which position we're drawing it in. Aren't dynamic methods great?

And finally, we draw_text using our previously-calculated rect, and item.name. In this case, item is going to be the current enemy, so the .name property will return the enemy's name. You don't need to worry about the text's Y coordinate (to make the list read downwards); that's all handled by the item_rect_for_text method.

Okay! You've worked long and hard with no tangible reward yet, so let's show you the fruits of your labour. Go back to Scene_Bestiary and underneath "super" write:

@list_window = Window_EnemyList.new(0, 0, 200)


Now playtest your game, talk to your NPC, and you'll see something like this:



If you can see the same thing, congratulations! You've totally just created a new scene, a new window, and put them together. Wasn't as hard as you thought after all, was it?

Now that's all well and good, but you'll notice that although you can access your new scene, once you're in you can't get out again. We're going to need a way to return to the map when ESC is pressed, and we do that by adding the following line in the start method of Scene_Bestiary:

@list_window.set_handler(:cancel,  method(:return_scene))


set_handler is another very useful method of Window_Selectable that allows you to specify an input symbol and a method to call when the associated key is pressed. The great thing is that for things like OK, Cancel and the like, all the gruntwork is done for you already. In this case, we're going to call :return_scene (itself a built-in symbol that calls the scene prior to the one you're currently in, which is obviously the map) when the :cancel key is pressed (which unless you change the key setup will be ESC). I'll cover set_handler, symbols, and the available default options a bit more in part 3.

Our final script (for now) will look like this:

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


There's quite a lot of new stuff in here and I don't want to overwhelm you, so I think we'll end today's lesson there. Next time I'll show you how to display a picture of the enemy, and a window listing their stats.

Recap:

  • Scenes will usually inherit from Scene_MenuBase.

  • The "super" keyword will call the method with the same name from the first available parent class.

  • Windows don't do anything unless you create them as an instance variable in the scene.

  • Selectable windows need, at the minimum, a refresh method to tell it what to display, an item_max method to tell it how many things are in the list, and a draw_item method to tell it what it actually needs to draw in each space.


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

Posts

Pages: 1
Sweet dude. This is very useful.
Adon237
if i had an allowance, i would give it to rmn
1743
I know this tutorial isn't supposed to completely finish the bestiary, but how would you load 'description' text from notetags from an enemy in the database? That is the only thing I would actually want in a bestiary, it's more of a strategy guide.
I have a question. I'm trying to make a battle system that is very much like the main overworld. I want it to be a new scene that has the battle background images and I want the player to be able to walk around and attack enemies as they move around. I guess similar to the Ocean Star: First Departure battle system:


My question is well, is something like this even possible in RPG Maker? And if it is would I be better off modifying the current battle scene, making a new scene based on the overworld or just implementing battle into the overworld much like the XAS ABS: http://xasabs.com/portfolio/rmvxa-v0-6-beta/
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
Adon: I'll explain this in part 3, which I'll be doing soon.

guitargod: You probably could do it, but it would be a massive amount of work to get the movement and input to work properly.
so how about dem part 3s?
Adon237
if i had an allowance, i would give it to rmn
1743
yes part 3 would be a marvelous thing right now~!
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
Sorry guys, was on a bit of a hiatus due to working in the games industry and gorging myself on anime/gam. Part 3 is coming, promise!
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
It's coming, don't worry. :P
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
Okay, just to let everyone know I'll be starting to write part 3 tomorrow, so your wait is nearly over!
SceneManager.call(Scene_Bestiary)

this isnt working for my npc.
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
When you say "this isn't working" what's not happening that should, or happening that shouldn't?
author=Trihan
When you say "this isn't working" what's not happening that should, or happening that shouldn't?


I'm not OP but I have a similar problem. When I put in:

class Scene_Bestiary < Scene_MenuBase

def start
super
end
end



Upon opening the project: it said: TypeError occurred, undefined superclass "Scene_MenuBase". I'm using XP instead of VX Ace so I figured this was a compatibility error, I looked around in the other default scripts and figured Scene_Menu was basically the same thing, so I put in:

class Scene_Bestiary < Scene_Menu

def start
super
end
end


However now when I run SceneManager.call(Scene_Bestiary) it says: NameError occurred while running script. Uninitialized constant Interpreter::Scene_Manager. I figure it's another compatibility error but I can't for the life of me figure out what's wrong.
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
This won't work for XP; RGSS2 is quite significantly different from RGSS3.
Hey, I've just started learning Ruby, and, inspired by RPGM games, I've decided to have a go at learning RGSS while I'm at it using your guide. I'm just confused about some things.
In the constructor for Window_EnemyList, you write
data = []
self.index = 0
1. Should data be @data or self.data, since its not used in the constructor? If not, how come?
2. What actually is the difference between saying @index and self.index, and why do you opt for self.index here, and @data when referring to the data later?
Also, the Scene Manager calls a class rather than creating a new instance of the class, so there's no initialize happening or anything, how does that work? Does it run 'start' instead?
And one more thing, which is more related to me not knowing a thing about RGSS. 'refresh' only occurs once, when you open up the scene right?
Trihan
"It's more like a big ball of wibbly wobbly...timey wimey...stuff."
3359
author=fishure
Hey, I've just started learning Ruby, and, inspired by RPGM games, I've decided to have a go at learning RGSS while I'm at it using your guide. I'm just confused about some things.
In the constructor for Window_EnemyList, you write
data = []

self.index = 0

1. Should data be @data or self.data, since its not used in the constructor? If not, how come?
2. What actually is the difference between saying @index and self.index, and why do you opt for self.index here, and @data when referring to the data later?
Also, the Scene Manager calls a class rather than creating a new instance of the class, so there's no initialize happening or anything, how does that work? Does it run 'start' instead?
And one more thing, which is more related to me not knowing a thing about RGSS. 'refresh' only occurs once, when you open up the scene right?


1. Oh goodness you're absolutely right, it should be @data or self.data.

2. As a property, they're pretty much interchangeable. I didn't have any particular reason for using one over the other. self. can also call methods though, which instance variables obviously can't.

SceneManager's call method creates an instance of the destination scene as part of its implementation, so that part is taken care of for you. This will include calls to the initialize and start methods, which you can see by looking at Scene_Base.

Yes, refresh is only called once when objects are created because it's only called from the constructors. If you wanted it to run every frame, you'd put it in update instead.
author=Trihan
1. Oh goodness you're absolutely right, it should be @data or self.data.

2. As a property, they're pretty much interchangeable. I didn't have any particular reason for using one over the other. self. can also call methods though, which instance variables obviously can't.

SceneManager's call method creates an instance of the destination scene as part of its implementation, so that part is taken care of for you. This will include calls to the initialize and start methods, which you can see by looking at Scene_Base.

Yes, refresh is only called once when objects are created because it's only called from the constructors. If you wanted it to run every frame, you'd put it in update instead.

Thank you, that cleared things up.
Pages: 1