Blog Info: I’m mostly done reading a great 65816 (SNES) assembly guide; this guide is so sensibly written that I think I’m actually ready to do a fun text hack for my favorite game of all time. So this week’s hack will help me prepare by introducing the algorithm in a high-level language. Plus, I think the RPG Maker community will like this one.
Back-story: Ok, our last attempt at speech balloons kind of flopped. They stretched text, they jittered, and they just popped into existence. Their worst sin, in my opinion, is that they didn’t “open” like the default RPG Maker ones, nor could they “type” their text out letter by letter. Let’s fix everything in one fell swoop.
Goal: Spruce up our speech balloon code, particularly in regards to opening/closing them when the player walks within range, and typing their message text out letter-by-letter.
You should read through tutorial RM-2 before starting this task. Instead of using a clean (new) project, I’m going to start where we left off after that tutorial. Don’t worry, I flushed my own project and copied it letter-for-letter from RM-2. One thing to note:
- Your “Talkbox_Window” module needs to be in a location that the interpreter can find. For example, add it below the “Windows” tab, or “Materials”. If you just add it anywhere (say, at the end of the file), you might get an error when you try to run the program.
I added this factoid to the original article, too. People, please tell me if you can’t get your code to work following one of my guides; I’ll be happy to fix it.
Step 1: Variable Width and Jitter Reduction
Add the following line to “Main” right after Graphics.freeze:
$Debug = Window_Base.new(544, 0, 544, 416)
$Debug.windowskin = nil
This just gives us an “offscreen” area to calculate stuff on. If you’re combining tutorial RM-1 with RM-2, you can ignore this, and just use the regular $Debug window for offscreen stuff.
Replace the Talkbox_Window class with the following one:
#A slightly improved RM-2 talkbox
class Talkbox_Window < Window_Base
#RPG Maker defaults
MAX_WINDOW_WIDTH = 544
MAX_WINDOW_HEIGHT = 416
WINDOW_PADDING = 32
#npc is a GameEvent
def initialize(npc, txt)
#Figure out a proper size, and initialize it offscreen
my_size = $Debug.contents.text_size(txt)
super(0, 0,
my_size.width + WINDOW_PADDING,
my_size.height + WINDOW_PADDING)
#Put off setting this variable until later
@offset_y = -1
#On top and invisible
self.z = 90
self.visible = false
#Draw the text once
self.contents.draw_text(0, 0, draw_width, draw_height, txt)
end
#Call this to update the talkbox’s position over an NPC
def update_pos(npc)
#Store the npc’s height if we haven’t done so yet
@offset_y = npc.sprite_height if @offset_y==-1
#Update this talkbox’s position
self.x = npc.screen_x - self.width/2
self.y = npc.screen_y - self.height - @offset_y
end
#Get the width of the client area
def draw_width
return self.width - WINDOW_PADDING
end
#Get the height of the client area
def draw_height
return self.height - WINDOW_PADDING
end
end
The MAX_WINDOW_WIDTH and MAX_WINDOW_HEIGHT variables just happen to be the window sizes RPG Maker ships with. I couldn’t for the life of me find these anywhere in the documentation, so I just stored my own values here. The DEFAULT_PADDING represents how much space is used as “border space” by any RPG Maker window —if you don’t add this value to each window you create, you may find that everything you draw is “cut off”.
We cache the return value of sprite_height because we’re worried it will affect performance. We’ll describe why later.
The remainder of the code is pretty simple to understand, even though not all relevant functions have been implemented yet. We’re basically resizing the talkbox to hold all of its text, and centering it above the NPC in question.
Now we need to add all those missing functions. Open Game_Event and add the following two member definitions:
def sprite_width
#Easy if this is a tile-based event
return 32 if @tile_id > 0
#Harder if this is a sprite
unless $scene.is_a?(Scene_Map)
return 0 #A generic Scene isn’t good enough
end
return $scene.get_event_width(self)
end
def sprite_height
#Easy if this is a tile-based event
return 32 if @tile_id > 0
#Harder if this is a sprite
unless $scene.is_a?(Scene_Map)
return 0 #A generic Scene isn’t good enough
end
return $scene.get_event_height(self)
end
There’s probably a better way to get this information, but this will have to do until I get a chance to read all the RMVX documentation. Basically, we just return the size of a tile or the size of a sprite depending on what this event is represented by. Of course, our caching code will position talkboxes improperly if you keep changing your event’s graphics from a Tile to a Chara representation. You can remove the caching if this bothers you, but I find it to be an unlikely edge case.
As you might have guessed, we now have to edit Scene_Map. Add the following functions anywhere:
def get_event_width(event)
return @spriteset.get_event_sprite(event).width
end
def get_event_height(event)
return @spriteset.get_event_sprite(event).height
end
Often, you’ll find that the Scene_* classes contain the down-and-dirty details of the game engine, while the Game_* classes contain a higher-level “logical” set of data points. Why, I’d bet we could fix the screen jitter we discovered earlier by simply updating talkboxes here instead of in Game_Map. This is completely correct. Find the following line in Game_Map’s “update” method:
update_talkboxes
…and remove it. Then, add the following line to Scene_Map’s “update” method, right after “@message_window.update”.
$game_map.update_talkboxes #Update NPC talkboxes
That’ll fix the 1-pixel jitter we noticed earlier. Why? My guess is that “@spriteset.update” (which occurs after “$game_map.update”) does a minor pixel correction on every sprite that’s scrolling. For Game_Map’s objects, this tiny offset isn’t important.
By the way, a lot of game objects have both a Game_* and a Sprite_* representation. We might consider doing the same thing for our windows; then we wouldn’t need this minor hack. However, I don’t think it’s important.
Add the following function to Spriteset_Map. The explicit loop this function contains, by the way, is the reason we cache @offset_y.
def get_event_sprite(event)
for character in @character_sprites
return character if character.for_event?(event)
end
end
Finally, add the for_event? function to Sprite_Character:
def for_event?(event)
return event == @character
end
All this is rather hackish, but it’s self-contained, so I don’t consider it such a sin. More importantly, it makes our talkboxes work! Create a few NPCs and give them long-ish textboxes. Have them walk around if you want. Start a new game and enjoy the much, much nicer talkboxes (note the walking cow).
Don’t worry about the fact that boxes appear on the map for a split second before moving above their respective NPCs; we’ll eventually start them all “closed”, which will fix this problem.
Step 2: Opening Boxes When In Range
We’ve come a long way, but our talkboxes still need some major work. For one thing, it doesn’t make sense to show them all on-screen at once. Besides introducing an unnecessary bottleneck, this is also rather boring to look at from the player’s perspective. So, let’s start all talkboxes “closed” and then “open” them when an NPC steps into the farthest visible tiles. (We’ll close them if he steps out of bounds).
This will require oodles of code to be written. Phase one will be to make the windows open and type their text; the second phase will invoke this code when an NPC steps into bounds. We will borrow most of our new code from Window_Message. Here’s our new class:
#A version of a Message which appears over a single
# NPC’s head. Supports most text tags, and auto-sizing.
class Talkbox_Window < Window_Base
#RPG Maker defaults
MAX_WINDOW_WIDTH = 544
MAX_WINDOW_HEIGHT = 416
WINDOW_PADDING = 32
#Create a new Talkbox_Window.
# @param npc – The GameEvent above which we show our talkbox
# @param txt – A String (or array of strings) which
# represents our message. (Arrays aren’t yet
# supported).
def initialize(npc, txt)
#Figure out a proper size, and initialize it offscreen
# Height is +3 to allow for g/p/y/etc.
my_size = $Debug.contents.text_size(txt)
super(0, 0,
my_size.width + WINDOW_PADDING,
my_size.height + 3 + WINDOW_PADDING)
#Put off setting this variable until later
@offset_y = -1
#On top and invisible
self.z = 90
self.visible = false
#Fully shrunken, and neither opening nor closing.
self.openness = 0
@opening = false
@closing = false
#Pause after opening? Note that we can’t use
# self.pause; that'll set a bouncing graphic
@passive_pause = false
#Reset variables used to track the state of the
# drawing routine
@text = nil #Our text buffer
@contents_x = 0 #Next character’s X
@contents_y = 0 #Next character’s Y
@line_count = 0 #Lines drawn so far
@wait_count = 0 #How long to pause (for \., etc.)
@line_show_fast = false
#Save our text-to-draw
@orig_texts = Array(txt)
end
#
# New functionality for text processing
#
#Frame Update – All opening/closing/typing happens here
def update
#Handle opening/closing
super
#All other actions must wait for a fully-opened window
unless @opening or @closing
if @wait_count > 0
#We’re pausing _within_ the text stream
@wait_count -= 1
elsif @passive_pause
#The window is fully open; leave it open
# (optionally, we can add code to scan for Input)
elsif @text != nil
#There's text we haven’t shown yet
update_message
elsif continue?
#Open the window and show its text
start_message
open
end
end
end
#Start a new line of text
def new_line
@contents_x = 0
@contents_y += WLH
@line_count += 1
@line_show_fast = false
end
#Should we be displaying the next message?
# For now, the answer is “always”, but you can add your
# own control code if you like
def continue?
return true
end
#Align all our text for processing into a single string with
# embedded control characters
def start_message
@text = ""
for i in 0...@orig_texts.size
@text += @orig_texts[i].clone + "\x00"
end
convert_special_characters
new_page
end
#Replace all special letters with single-character
# equivalents. (Copied verbatim from Message)
def convert_special_characters
@text.gsub!(/\\V\[([0-9]+)\]/i) { $game_variables[$1.to_i] }
@text.gsub!(/\\V\[([0-9]+)\]/i) { $game_variables[$1.to_i] }
@text.gsub!(/\\N\[([0-9]+)\]/i) { $game_actors[$1.to_i].name }
@text.gsub!(/\\C\[([0-9]+)\]/i) { "\x01[#{$1}]" }
@text.gsub!(/\\G/) { "\x02" }
@text.gsub!(/\\\./) { "\x03" }
@text.gsub!(/\\\|/) { "\x04" }
@text.gsub!(/\\!/) { "\x05" }
@text.gsub!(/\\>/) { "\x06" }
@text.gsub!(/\\</) { "\x07" }
@text.gsub!(/\\\^/) { "\x08" }
@text.gsub!(/\\\\/) { "\\" }
end
#Start a new “page” of input on the current message
# box’s surface. Right now, we only have one page of
# input, but there’s no reason you can’t add more.
def new_page
#Clear the background bitmap
self.contents.clear
#Reset our drawing state variables
@contents_x = 0
@contents_y = 0
@line_count = 0
@line_show_fast = false
@passive_pause = false
#Reset our text color to the default
contents.font.color = text_color(0)
end
#Called when a message has been printed in its entirety
# (used below)
def finish_message
@passive_pause = true
@wait_count = 10
@text = nil
end
#This is the main text processing loop of the talkbox window.
# It works by slicing one character at a time off of @text,
# and either reacting to it as a control character,
# or displaying it.
def update_message
loop do
#Get next text character
c = @text.slice!(/./m)
case c
when nil
#There is no text waiting to be drawn
finish_message
break
when "\x00"
#Our custom "newline" character
new_line
#Multiple pages; left in for your reference
#if @line_count >= MAX_LINE
# unless @text.empty?
# self.pause = true
# break
# end
#end
when "\x01"
#\C[n] (text character color change)
@text.sub!(/\[([0-9]+)\]/, "")
contents.font.color = text_color($1.to_i)
next
when "\x02"
#\G (gold display) -ignore
break
when "\x03"
#\. (wait 1/4 second)
@wait_count = 15
break
when "\x04"
#\| (wait 1 second)
@wait_count = 60
break
when "\x05"
#\! (Wait for input)
break
when "\x06"
#\> (Fast display ON)
@line_show_fast = true
when "\x07"
#\< (Fast display OFF)
@line_show_fast = false
when "\x08"
#\^ (No wait for input)
break
else
#Normal text character
contents.draw_text(@contents_x, @contents_y, 40, WLH, c)
c_width = contents.text_size(c).width
@contents_x += c_width
end
break unless @line_show_fast
end
end
#
# The rest of our functionality is unchanged
#
#Call this to update the talkbox’s position over an NPC
def update_pos(npc)
#Store the npc’s height if we haven’t done so yet
@offset_y = npc.sprite_height if @offset_y==-1
#Update this talkbox’s position
self.x = npc.screen_x - self.width/2
self.y = npc.screen_y - self.height - @offset_y
end
#Get the width/height of the client area
def draw_width
return self.width - WINDOW_PADDING
end
def draw_height
return self.height - WINDOW_PADDING
end
end
What a whopper! Fortunately, this is fairly easy to understand: update() determines whether or not we should be calling update_message(), which reads letters one-by-one. All other methods are helpers, and their functions are relatively simple. I’ve got to hand it to the RPG Maker VX team: their code might be a bit cluttered, but it’s very easy to follow and understand. I’m consistently amazed at how sensible it is to hack the RPGMVX engine.
Now, add the following code in Game_Event’s update() method, right below check_event_trigger_auto:
#Update NPC talkbox
@npc_talkbox.update if @npc_talkbox != nil
This is much easier to understand; we just needed to hook up our frame update method when the Game Event calls its update method. Finally, add a new event to your map, with the following “Script” command:
for event in $game_map.events.values
tkb = event.npc_talkbox
next unless tkb
tkb.openness = 0
tkb.start_message()
tkb.open()
end
Since windows stay open forever, we need a way to “re-type” the window, to make sure our system is working properly. Since we didn’t bother writing such a method (we will later), we just hooked it up manually for now.
Run your program; talk to your new NPC any time you want to reset every window. Ah…. Isn’t that nice? As an added benefit, our windows no longer show up on the title screen, since their “openness” only gets updated in the main game loop.
Before we make these talkboxes appear on demand, we have to decide when exactly we want to show them. I chose to wait for a talkbox to be fully onscreen vertically, and halfway onscreen horizontally before starting to open it. I made this decision partly because boxes are centered over NPCs, so a talkbox will now appear slightly before or after its respective NPC becomes visible. Add the following function to your Window_Talkbox class; it checks whether or not this box should be opened:
#Is this box within the vertical boundaries, and at
# least halfway within the horizontal boundaries?
def in_range?
#Store some marker variables
mid_x = self.x+self.width/2
min_y = self.y
max_y = self.y+self.height
#Check bounds
return true if min_y>=0 and mid_x>=0 and
max_y<=MAX_WINDOW_HEIGHT and
mid_x<=MAX_WINDOW_WIDTH
return false
end
As you should have noticed, this function relies on the update_pos() function being called first. And, if you think about it, update_pos doesn’t work unless called in the Scene_* routine; calling it in the context of Game_* is a mistake. So, instead of juggling a whole slew of function calls, we’ll just append our code to update_pos(). Your update_pos function should now look like this:
#Call this to update the talkbox’s position over an NPC
def update_pos(npc)
#Store the npc’s height if we haven’t done so yet
@offset_y = npc.sprite_height if @offset_y==-1
#Update this talkbox’s position
self.x = npc.screen_x - self.width/2
self.y = npc.screen_y - self.height - @offset_y
#Now, show/hide we show this box?
if @onscreen != in_range?
unless @onscreen
retype_text()
else
close_window()
end
end
end
This relies on a new variable, @onscreen. Add it to your init function (anywhere)
@onscreen = false #In-range?
Finally, you’ll need the functions retype_text() and close_window(). Add them anywhere:
def retype_text
@onscreen = true
openness = 0
start_message()
open()
end
def close_window
@onscreen = false
close
end
This should do the trick. The only thing left to do is to find the following lines in update’s “elsif continue?” branch and comment them out:
# start_message
# open
…otherwise, the window will always open itself, regardless of its position onscreen. Run your code:
The nice thing is, since we check this every tick (instead of just when the NPC moves) it should work properly for teleporting to a new map and teleporting NPCs around the map.
Note: The documentation states that if a Window’s “openness” value is less than 255, its “contents” Bitmap won’t be displayed. I am kind of assuming that, if its “openness” is 0, the windowskin also won’t be displayed, and processing will be minimal. I feel this is a safe assumption, but I want to make it clear that, if you experience a performance hit, you might want to look here first.
Possible Improvements
This tutorial was rather longish, so I’ll only give you two assignments this week.
- It’s well known that inheritance simplifies code reuse. Yet, rather than reusing the code in Window_Message, we just copied it and deleted what we didn’t want. Wouldn’t it be better to abstract it into a common superclass, and have both Window_Message and Window_Talkbox subclass this new class? For our tutorial: no. Extracting this code could have broken both normal messages and our talkboxes. Our lightweight class only risks breaking itself, which makes it ideal for development. Now that we’re done proving the concept, however, it would be a good idea to create a common superclass. That’s your first assignment.
- We copied the convert_special_characters() function directly from Window_Message, but closer inspection makes it clear that only a small number of backslashed “special letters” are used. Might we extend the list of possible escape sequences? Since we extract the text for our message balloon directly from a ShowMessage event, this provides a very elegant way of specifying additional properties of talkboxes. For example, we might use \{ACCEPT} to mean “this talkbox responds to the Accept key”, and \{EVENT:2} to mean “call event 2 when the user presses Accept”. Perhaps \{CLOSE:10} could means “close this talkbox 10 ticks after all text has been displayed” —indeed, we’ll need a lot more control if we’re to use these things for cutscenes.




Pingback: [RM-4] TCP Sockets in RPG Maker VX « Making Games the Hardest Way Possible