code of the Ninja();

// in search of swift, efficient, and invisible code

2010-09-13

The Nitpicker's Guide to Sonic Genesis - Part II

Hello again, Code Ninjas! It has been quite a while since Part I, but never fear - slowly but surely I will give Sonic Genesis the drubbing it deserves. Welcome to The Nitpicker's Guide to Sonic Genesis - Part II.

Code Flaw #002: The Demos Are Totally Nerfed

Original:

GBA:

The demo is completely different in the GBA version, and obviously much worse (Sonic is hurt twice, seems disoriented, and demonstrates less of the level). Why should this be? If we were uncharitable, we might chalk it up to the GBA team recording new demos which betray their underdeveloped skill level. However it is much more likely that the game is using the same demo data, but changes in the physics have thrown it off.

You see, the demo "movies" in the old Sonic games were not actual videos of the action - that would have taken up so much space it would have been prohibitive. Instead, the game itself is running, but with two changes: 1) the game resets when the player hits the start button or when 30 seconds have elapsed, whichever comes first; 2) the Sonic object isn't receiving input directly from the joypad, but from a chunk of data read sequentially from the ROM. This chunk of data was made by logging the button presses while someone played the level (for more information about recording joypad input "movies", see this post).

So, if the physics or level layout changes, the recording won't be appropriate anymore. It's as if your joypad were simultaneously plugged into two Sega Genesises (Geneses?), one of which contained a Sonic 1 cart, and the other, Sonic 2. You might be playing Sonic 1 beautifully, but anyone watching the Sonic 2 game will wonder why Sonic is suddenly acting like a drunkard. (They won't notice a difference in Tails' behaviour, though - wantonly flinging himself into harm's way is business as usual for him. =P )

The team who made Sonic Genesis should have recorded new demos after the physics were in place to avoid this issue. But, one can hardly blame them for not doing so; it would, after all, have involved playing the game, and I wouldn't wish that on anybody!

Fixing The Problem In Your Own Hack

This issue with screwy demos isn't confined to Sonic Genesis. ROM hacks will have the same problem if the physics, controls, or layout are changed, even slightly. I encountered it myself when making Sonic: The One Ring, and needed to find a way to record new demos that would be compatible with the ROM. I've turned what I learnt into a tutorial and utility - go here for the complete story. See, I'm not just complaining about Sonic Genesis just to be mean - I'm using it to frame programming tips to help you guys out! Aren't I nice?

Well, not that nice since I'm now going to throw in a few freebie Bonus Flaws out of spite!

Bonus Flaw #003: Wrong Credits Footage

In addition to having broken demos, the "demos" seen during the game's credits are broken, too. But none so badly as the Labyrinth Zone one: this time, not only does the control movie not sync up because of different physics, it's totally inapposite because the wrong area of the level has been loaded!

The famous underwater section where Sonic is pulled through the tunnel by the current, catching on to the breakable poles and avoiding spikes, has been inexplicably replaced by some other region of the zone.

Bonus Flaw #004: Marble Zone Button Keeps Turning Up Like A Bad Penny

This is pretty hilarious. It's supposed to load different graphics depending on the zone ID, but they can't even get that right.

Bonus Flaw #005: Underwater Palettes Incorrect

Sonic and the Badniks don't look right...

Original:

GBA:

...but objects like doors and blocks fare even worse.

(And why does Scrap Brain Act 3 have such a horrible dark blue background? It's supposed to be a lovely, rich purple.)

Bonus Flaw #006: Missing/Incorrect Background Tiles

This flaw really does take the cake. This isn't a matter of not being able to properly port a complicated game's physics to a new platform, or something relatively forgivable. It's a simple matter of gratuitous incompetence and unconcern for the product.

Well, try to keep your lunch down, Code Ninjas - I know it's not easy after that rogue's gallery. Until next time!

2010-03-19

2D Camera

Welcome back, Code Ninjas!

It's a bit anachronistic to use the word "camera" in reference to 2D games. The concept of the viewable area as the view through a director's camera really only took off with Super Mario 64, whose 3D worlds required the player to be actively mindful of the viewpoint. Four entire buttons on the Nintendo 64 joypad were dedicated to camera control (though they often found other uses), and Super Mario 64 even went so far as to characterise the camera as a Lakitu floating on a cloud, following Mario wherever he went.

However, we live in a post-3D world, and it's justifiable to consider the view in a classic 2D sidescroller to be a "camera". In this Code of the Ninja, we'll be looking at how to implement a natural feeling camera in a sidescrolling game.

Game Maker includes built-in camera functionality. Just about anyone who's used it will be familiar with the "views", and the view_object variable. When view_object is set to the id of an instance, the view will follow that object around automatically. You can adjust some border and speed settings, as well.

For a lot of simple games, this works out just fine. But for anything like Sonic or Mario, which require a bit more flexibility in their camera, it's a better idea to write new camera scripts and ignore Game Maker's built-in object following altogether. (You'll still need to define a view, though, of course. Otherwise the entire room will be shown, scaled to fit the window.) So make sure the view_object is set to none, and let's begin.

Camera Follow

We'll make a new script called "CameraFollow()". If you're going to only ever follow one object in your game, such as the player, you could just call this script in the player object. However, oftentimes we'll want to change which object is followed (perhaps keeping an eye on the boss in a boss fight, for instance). That means it's better to write CameraFollow() to take an argument of which instance to follow, and call it in a persistent control object (you can make a dedicated Camera object, or just call it in whichever existing control object you already have, such as the HUD or an Input handler).

Also, CameraFollow() must be called after the target object has already moved. The best way to make sure of this is to call CameraFollow() in the End Step Event.

script: CameraFollow()

//define centre
cameraCentreX = view_xview + (view_wview/2);
cameraCentreY = view_yview + (view_hview/2);

//determine offset
cameraOffsetX = floor(argument0.x) - cameraCentreX;
cameraOffsetY = floor(argument0.y) - cameraCentreY;

//update view
view_xview += cameraOffsetX;
view_yview += cameraOffsetY;

We'll be adding more features to this script as we go, but I've started with this simple version that simply keeps the target object in the centre of the screen. You can try it now, and it should work.

How's does it work, though?

First, we find the horizontal centre point of the view as it currently stands. That's view_xview (the left edge of the view) plus half of view_wview (the width of the view). We store this value in cameraCentreX. Then we do the same thing to find the vertical centre point, and store it in cameraCentreY.

Note: Some games, such as Sonic the Hedgehog, don't use a perfectly centred view. They bias the camera slightly upward, to show more of what's beneath the player. If you wish to do the same thing, you can replace (view_wview/2) and (view_hview/2) with custom values; or, alternatively, you can add bias values on top of the existing calculation, which may be necessary if your view width or height change during the game (for widescreen toggling purposes, etc).

Next, we find how far away from these desired centre points the target object's (argument0's) x and y positions are, by subtracting the centre point values from the target object's x and y. The difference between them - the offset - we store in cameraOffsetX and cameraOffsetY.

Note: We use floor() on the object's x and y at this point because x and y are often at noninteger (subpixel) values, but the view in Game Maker doesn't render at such positions. Instead, it rounds view_xview and view_yview off. Unfortunately, as rounding sometimes results in rounding up and othertimes rounding down, this can cause jitter. All this is avoided by flooring the object's x and y before using them in any calculations.

Finally, we simply add these offset values to the view x and y position, in effect moving the view by the exact same amount the player moved away from the centre point. (It may seem a roundabout way to have done this, but it's being set up for more complicated functionality later on.)

Staying Inside

Before we add new features to our script, though, there is one problem with it we need to patch up. Unlike Game Maker's built-in object following, this code allows the view to exceed the room boundaries. Depending on how you design your game, this might be a bad thing.

The solution? Create a new script called CameraLimit(). It should be called from CameraFollow(), after everything else.

script: CameraLimit()

if view_xview > room_width-view_wview view_xview = room_width-view_wview;
if view_xview < 0 view_xview = 0;

if view_yview > room_height-view_hview view_yview = room_height-view_hview;
if view_yview < 0 view_yview = 0;

Note: In this version of CameraLimit(), I've used the room dimensions. You can use any custom values you want - there's no strict reason why you can't exceed the room dimensions, even using negative numbers. In fact, since Game Maker doesn't let you resize a room while you're in it, the only way to dynamically change the limits is to use your own variables. Why change the limits? Imagine a boss fight in Sonic - the view is extremely limited, to keep the boss on the screen, but of course the actual room (which contains the whole zone) hasn't really changed size.

Free Zone

Now that our camera is properly chastened and stays within its designated confines, we can add a new feature to CameraFollow(). We're going to add a "free zone" - a region in the centre of the screen (of any size you wish) in which the character can move freely before the camera bothers to try and follow.

Why add such a thing? There are probably many reasons, but the major one is that centring the view so strictly on the player can cause it to move around too much when the player is making a small jump, or merely turning around. It's best to have a little buffer area, so that the camera doesn't seem to jerk so drastically.

How do we add this in? First, you need to decide how large this free zone should be. I'm going to use 8 pixels in either direction horizontally, and 32 in either direction vertically. You can use anything you think is reasonable, and it doesn't even have to be symmetrical.

script: CameraFollow()

//define centre
cameraCentreX = view_xview + (view_wview/2);
cameraCentreY = view_yview + (view_hview/2);

//determine offset
cameraOffsetX = floor(argument0.x) - cameraCentreX;
cameraOffsetY = floor(argument0.y) - cameraCentreY;

//free zone
if cameraOffsetX > 8 cameraOffsetX -= 8; else
if cameraOffsetX < -8 cameraOffsetX += 8; else
cameraOffsetX = 0;

if cameraOffsetY > 32 cameraOffsetY -= 32; else
if cameraOffsetY < -32 cameraOffsetY += 32; else
cameraOffsetY = 0;

//update view
view_xview += cameraOffsetX;
view_yview += cameraOffsetY;

CameraLimit();

That takes care of the free zone. All you have to do is subtract the size of the free zone from the camera offset if the camera offset is larger than the free zone, or set the camera offset to 0 if it's smaller than the free zone (so it won't move at all). There are multiple ways to code this; I chose a simple, if long-winded, method.

Speed Limiting

Next, we need to add speed limiting. Sometimes (but not all the time) you want the camera to only move a maximum number of pixels per step. This can be used for a sense of speed, as the camera lags a little behind the player (as sometimes happens in Sonic 2), but it can also be used to scroll the camera from one target object to another when the targets are quickly switched. If there was no limit on the number of pixels the camera could move per step, the view would immediately switch and the player might not understand what happened.

I've chosen 16px as the speed limit here. Let's add the speed limiting (again, there are several ways to code this):

script: CameraFollow()

//define centre
cameraCentreX = view_xview + (view_wview/2);
cameraCentreY = view_yview + (view_hview/2);

//determine offset
cameraOffsetX = floor(argument0.x) - cameraCentreX;
cameraOffsetY = floor(argument0.y) - cameraCentreY;

//free zone
if cameraOffsetX > 8 cameraOffsetX -= 8; else
if cameraOffsetX < -8 cameraOffsetX += 8; else
cameraOffsetX = 0;

if cameraOffsetY > 32 cameraOffsetY -= 32; else
if cameraOffsetY < -32 cameraOffsetY += 32; else
cameraOffsetY = 0;

//speed limit
if cameraOffsetX > 16 cameraOffsetX = 16; else
if cameraOffsetX < -16 cameraOffsetX = -16;

if cameraOffsetY > 16 cameraOffsetY = 16; else
if cameraOffsetY < -16 cameraOffsetY = -16;

//update view
view_xview += cameraOffsetX;
view_yview += cameraOffsetY;

CameraLimit();

Now we've added the speed limit, we can match Game Maker's built-in object following point for point. Now to add some even more powerful stuff.

Looking Around

In Sonic, Mario, and countless other platformers, you can look up and down, shifting the view slightly to see what's above and below you. In Super Mario World, you can use the L and R buttons to shift the view left and right, as well. Let's add these abilities.

In the player control scripts, when looking up and down, or even left and right, you'll need to add to and subtract from variables which CameraFollow() will use to shift the view. I'll call these cameraShiftX and cameraShiftY.

For instance, pressing Up would subtract 2 from cameraShiftY every step, until it reached the maximum shift you desire. Pressing Down would do the opposite, adding 2 until the maximum shift was reached. In the case of neither button, cameraShiftY would slowly return to 0. (For Super Mario World's L and R shifting, the horizontal shift doesn't drift back to normal upon letting up the button, though. It remains shifted until the player shifts it back.)

Some games actually shift the view horizontally depending on the direction the player is facing. I find this annoying, myself - when turning around and making a jump, the whole screen starts moving, making it harder to line up where to land. But this, too, can be done with the same cameraShiftX variable.

Now, to take the shift into account, all we have to do is change the lines in CameraFollow() that determine the offset. Replace them with these:

script: CameraFollow()

...
//determine offset
cameraOffsetX = floor(argument0.x + cameraShiftX) - cameraCentreX;
cameraOffsetY = floor(argument0.y + cameraShiftY) - cameraCentreY;
...

By adding cameraShiftX and cameraShiftY to the target object's x and y when determining the offset, the camera is technically not following where the player is, but where the player is looking. When the player isn't looking around, cameraShiftX and cameraShiftY return to 0, which is the same as following the player itself.

Re-centring Upon Landing

In Sonic the Hedgehog, the camera behaves differently when Sonic is in the air as opposed to running along the ground. In the air, Sonic has a generous vertical "free zone" before pushing the camera around. But on the ground, the camera keeps him at dead vertical centre, so that when he runs over hilly terrain, the camera follows properly. (The camera behaves the same, horizontally, in either state.)

This is simple enough. You can just add a check in the CameraFollow() for whether he's airborne or not, and exit the vertical free zone calculation if he's on the ground.

Note: Though it works well enough to simply check if Sonic is in his air state, it's a better idea to add another flag in the target object, called GroundCamera, which you set to false when he jumps, springs, or falls, etc, and reset to true when he lands. Why a second flag when his state would do? In the case of Knuckles, when he glides and slides into the ground, even though he's technically landed, the camera doesn't return to normal until he stands up. Thus, it's better to have fine control over the mode the camera is in, independent of the actual state of the character.

If that's all we do, though, we'll be left with a problem. When Sonic lands from a jump, the camera jerks immediately to focus tightly on him. That's no good - it's too much of a jerk to put up with comfortably.

There are two ways to fix this. They both involve reducing the vertical speed limit of the camera to 6 instead of 16 after Sonic lands, so that the camera catches up slowly enough that it doesn't cause violent motion.

You can't simply leave the vertical speed limit at 6 all the time. Sonic often runs downhill, and his vertical speed will well exceed 6. The camera would never catch up if it couldn't go faster than 6 pixels per step! So it's necessary to determine whether Sonic has just landed or not.

The first way is to check his speed. If his vertical speed is less than 6, make the speed limit 6. If it's more than 6, make the speed limit 16. Chances are his vertical speed will be very low after landing on the ground. This method is similar to how the 16-bit Sonic engine does it.

The second way is to set a flag called JustLanded to true when Sonic lands (you also have to set it back to false when he jumps). While it's true, the vertical speed limit should be 6, and while it's not, the vertical speed limit should be 16. The second the camera catches up with Sonic, you can reset JustLanded to false. How can we tell when the camera catches up to Sonic? Check if abs(cameraOffsetY) is less than or equal to 6 (i.e., Sonic isn't more than 6 pixels above or below the vertical centre point). In any step where where that's true, the camera will catch up.

script: CameraFollow()

...
//speed limit
if cameraOffsetX > 16 cameraOffsetX = 16; else
if cameraOffsetX < -16 cameraOffsetX = -16;

var cameraLimitY;

if argument0.JustLanded cameraLimitY = 6; else
cameraLimitY = 16;

if abs(cameraOffsetY) <= 6 argument0.JustLanded = false; else
if cameraOffsetY > cameraLimitY cameraOffsetY = cameraLimitY; else
if cameraOffsetY < -cameraLimitY cameraOffsetY = -cameraLimitY;
...

Jump To A Point

Now that that's all working, there's one last thing to add. Because our camera has a speed limit, when the level starts, you'll have to wait for the camera to scroll to where the player is before you can start playing. This kind of sucks.

The remedy is a script called CameraJumpTo(). You can call it to immediately centre the view around any point you specify. Call it as the game begins to focus on the player.

script: CameraJumpTo()

view_xview = argument0 - (view_wview/2);
view_yview = argument1 - (view_hview/2);

CameraLimit();

The script takes two arguments: the x and y to point at.

Example GMK

For an example GMK, click here.

Well, that's it for custom 2D camera. Until next time, happy coding, fellow Code Ninjas!

2010-02-22

Text Boxes

Welcome back, Code Ninjas!

Last time, I talked about sinusoidal motion, a way to make certain movements and animations look more natural. I mentioned that it could be used to make an opening animation for text boxes. This time we'll be looking at text boxes themselves.

We've all seen text boxes. They're the windows full of dialogue that appear when you talk to people in video games.

It's easy enough to slap a single box of text onto the screen in Game Maker. But most text boxes consist of several pages of dialogue, and the player advances through them by pressing a button.

You could achieve this with an array of strings, like this:

Create Event (TextBox object):

//define the pages of text
//(# is the newline character in GML)
Page[0] = "Hello there, traveller!";
Page[1] = "This is a bomb shop! I stock all#sorts of different explosives.";
Page[2] = "No smoking, please!";
//set the page index
PageIndex = 0;

Draw Event (TextBox object):

//draw the string on the screen
draw_string(TextX,TextY,Page[PageIndex]);

Step Event (TextBox object):

//check for a press of the A button
if JoyButtonPressed(A);
{
  //increase the page index
  PageIndex += 1;
}

This is an okay method, but it has an annoying problem: You have to manually cut the dialogue into pages yourself, as well as place the newline characters.

Imagine you have a game where you want the text box to be resizable. Or, not all text boxes are the same size (as in Final Fantasy VII). Or, you've already written all the dialogue for your RPG, and then decide to change the size of the font or the text box. The method above would suck in these cases - you'd be stuck reworking all your strings every time something changed.

There really needs to be a way to just write the dialogue all in one piece, and let the game take care of the rest: deciding when to break lines, and cutting it into individual pages.

Well, let's see what we can do...

Making Pages

This time we'll give the TextBox object only a single string of dialogue. This will be the source text that the pages are made out of. Also, in order to make the pages contain the right amount of text, the TextBox object will need to know about its size.

Create Event (TextBox object):

//define the source text
//(which can be from the calling object, or loaded from a text file, whatever)
DialogString = "...";
//set dialog offset to 1. This is the position in the source text to start reading from.
DialogOffset = 1;
//set dialog length to the length of the source text. This is the position in the source text to stop at.
DialogLength = string_length(DialogString);

//set position of text box
x = 40; y = 300;
//set size of text box
width = 560; height = 100;
//set size of border (horizontal and vertical)
xborder = 8; yborder = 4;
//determine the size of the text area (text box minus the borders)
textwidth = width-xborder*2;
textheight = height-yborder*2;
//set the height of individual lines
linespacing = 23;

//make the first page of text to show
MakePage();

Step Event (TextBox object):

//check for a press of the A button
if JoyButtonPressed(A);
{
  //make the next page of text to show
  MakePage();
}

We call the MakePage() script every time the player presses the button, to construct the page of text that they'll see next. We also call it once in the create event, so that there's an initial page showing.

MakePage() basically bites off a chunk of the DialogString source text and puts it into a new string, CurrentPageString, which is the string that will be drawn.

script: MakePage()

//set up some temp variables
var numLines,line,letter,word;
line = 0; word = "";
//set the font to the current font so that the font measuring scripts work right
draw_set_font(TextBoxFont);
//empty the CurrentPageString, so we can refill it with text from DialogString
CurrentPageString = "";

//get the number of lines that fit in the box, based on line spacing and height of box
numLines = textheight div linespacing;
//show error message if no lines fit in box
if numLines = 0
{
  show_error("No lines fit in the text box!",1);
}

//main loop
do
{
  //read a letter from the source text
  letter = string_char_at(DialogString,DialogOffset);
  //increase the offset by one since you read one letter
  DialogOffset += 1;
  //is the letter the escape char?
  if letter=="^"
  {
    //change letter to return
    letter = "#";
    //increase the line count to full
    line = numLines;
  }
  //add the letter to word
  word += letter;
  //if the letter was a space, hyphen, or return (or the end of the source text was reached), the word is complete
  if letter==" "||letter=="#"||letter=="-"||DialogOffset>DialogLength
  {
    //check to see if word alone exceeds the textbox width
    if string_width(word)>textwidth
    {
      show_error("Single word is too long for the textbox!",1);
    }
    //check to see if word added to current pages's text is too wide
    if string_width(CurrentPageString+word)>textwidth
    {
      //add a return to go to the next line, and increase the line count
      CurrentPageString += "#";
      line += 1;
      //if this was the last line...
      if line = numLines
      {
        //return the offset to the beginning of the word in order for the next page to start at the right point
        DialogOffset -= string_length(word);
        //blank out the word so it won't be added.
        word = "";
      }
    }
    //only add the word if it hasn't been blanked out
    if word != ""
    {
      //add the word to the current page's text
      CurrentPageString += word;
      //if letter was a return, increase the line count
      if letter="#" line += 1;
      //and reset word to blank
      word = "";
    }
  }
}
until (line >= numLines or DialogOffset > DialogLength)
//stop the loop when reach the last line or the end of the source text

With the comments, MakePage() should be pretty much self-explanatory, but there are two points I want to go into more detail on.

The first is the "escape character", ^. What is it for? Well, it's sort of like a page break. Sometimes you want the sentence of dialogue to end, and not start the next sentence until the player advances to the next page, even if there's enough space to fit the next few words. It all depends on the flow of the dialogue.

I used the caret because it's sufficiently obscure, but of course the escape character can be anything you want to define it as. If your RPG townsfolk are going to use emoticons like ^_^ then you might want to pick something else.

The second point is this: Why is the MakePage() script so complicated? Anyone familiar with GML will know that you can use a function called draw_text_ext(), which will automatically word wrap to any width that you specify. Why do I go through so much trouble to manually run through the string and add newline characters to cause it to wrap?

It becomes clear as we move on to the next aspect of text boxes. They have to type out.

Typing Out

In order to make them type out, we shouldn't draw CurrentPageString in the draw event. Instead, we should make a new string, ResultString, and draw it. ResultString will be built up from CurrentPageString in the step event of the TextBox object.

Draw Event (TextBox object):

draw_set_font(TextBoxFont);
draw_set_halign(fa_left);
draw_set_valign(fa_top);
draw_text_ext(x+xborder,y+yborder,ResultString,linespacing,-1);

Step Event (TextBox object):

//if the text box is typing out the text
if printing
{
  //increase CharIndex
  CharIndex += 1;
  //if CharIndex is the size of the page of text
  if CharIndex >= CurrentPageLength
  {
    //fill the ResultString with the entire current page and stop typing out
    CharIndex = CurrentPageLength;
    ResultString = CurrentPageString;
    printing = false;
  }
  else
  {
    //otherwise, make the ResultString as much of the current page as CharIndex is large
    ResultString = string_copy(CurrentPageString,1,CharIndex);
  }
}

We need the new variables, 'printing' so that we know when it's typing out and when it's done, 'CharIndex' to increase each step so we can keep taking more and more of CurrentPageString, and 'CurrentPageLength' so that we know when we've finished going through CurrentPageString. These three will need to be set up at the end of MakePage() now.

script: MakePage()

...

CurrentPageLength = string_length(CurrentPageString);
CharIndex = 0;
printing = true;

Now it'll print out. It's because of this that MakePage() needs to be so complex. If we relied on draw_text_ext() for word wrap, we'd get ugly results. Because we're actually drawing ResultString to the screen, and ResultString builds up letter by letter, the computer wouldn't know if a word was going to run off the side of the text box until after it had printed fully out. This would result in seeing words print out of bounds, and then skip on to the next line. MakePage() comes to the rescue here, determining where the lines should break before ever being printed, so that the words "know" to be on the next line before they even finish printing out.

Well, now that we've got our dialogue typing out, you'll notice a new problem. When the user presses the button, it'll skip to the next page. We don't want to do that, if the current page hasn't finished printing out. Instead, we want to instantly finish typing out the current page. Only if the user presses the button again should it advance one page.

This will require modifying the step event.

Step Event (TextBox object):

//if the text box is typing out the text
if printing
{
  //increase CharIndex
  CharIndex += 1;
  //if CharIndex is the size of the page of text OR the user presses the button
  if CharIndex >= CurrentPageLength or JoyButtonPressed(A)
  {
    //fill the ResultString with the entire current page and stop typing out
    CharIndex = CurrentPageLength;
    ResultString = CurrentPageString;
    printing = false;
  }
  else
  {
    //otherwise, make the ResultString as much of the current page as CharIndex is large
    ResultString = string_copy(CurrentPageString,1,CharIndex);
  }
}
else
{
  //if it's not typing out, pressing the button should advance one page
  if JoyButtonPressed(A)
  {
    //but if we're on the last page, we should close the text box
    if DialogOffset >= DialogLength
    {
      instance_destroy();
      exit;
    }
    //otherwise, determine the next page of text to type out
    MakePage();
  }
}

What we've done is check for a press of the button while 'printing' is true, and made it do the same thing as reaching the end of the page: ResultString becomes CurrentPageString in total, and 'printing' is set to false. Also, we've made the standard check for the button only happen when 'printing' is not true.

I've also added a check at that point if it's the last page or not. If the player presses the button on the last page, there's no new page to advance to, so the text box should close instead of calling MakePage() again.

Now that it's all working, we should add some visual cue so that the player knows that the button does something different at different times. While the text is typing out, the button skips to the end. While it's not, the button advances one page. On the last page, the button closes the text box.

Most games don't bother with a different icon for each possible state. They just show a triangle or something once the text is done typing out, so that you know there's more. If it's the last page, the triangle simply doesn't appear when the text finishes appearing.

It's easy enough to check for all three states, though, so this is how you can do it - add this to the draw event:

Draw Event (TextBox object):

...

if printing
{
  //draw "skip" icon/message
}
else
{
  if DialogOffset >= DialogLength
  //draw "close" icon/message
  else
  //draw "next" icon/message
}

Variable Text Speed

That's pretty much it for text boxes. But there's some nice finishing touches we can add - variable text speed, for one. In the code blocks above, the text types out at 1 character per step. This speed should be under the player's control, because everybody reads at a different rate.

All that needs to be done is replace the line that says

Code:

CharIndex += 1;

and replace it with

Code:

CharIndex += textspeed;

The text speed can be set to 1 at the game start, and then the player can change it from an option menu. Or - and this is pretty cool - since the left and right buttons usually do nothing while a text box is open, you could let the player alter the text speed any time a box is open.

Just add this to the step event:

Step Event (TextBox object):

if JoyButtonPressed(LEFT)
{
  //decrease the text speed
  textspeed /= 2;
  if textspeed < 0.25 textspeed = 0.25;
}
else
if JoyButtonPressed(RIGHT)
{
  //increase the text speed
  textspeed *= 2;
  if textspeed > 8 textspeed = 8;
}

You can make the upper and lower limits anything that seems reasonable to you. However, the difference between settings isn't enough when they're linear. They need to be logarithmic. So, instead of adding to and subtracting from textspeed, I suggest you multiply and divide it (or bit shift it).

There should be lights or pips or something worked into the design of the text box, so that the player has a visual clue to the setting that the text speed is at.

Canceling

Finally, you can add this to the step event.

Step Event (TextBox object):

if JoyButtonPressed(B)
{
  //close the text box
  instance_destroy();
  exit;
}

This lets the player hit a cancel button to close the text box whether the dialogue is finished or not. It's really annoying to accidentally re-talk to a character and be forced to page through their entire diatribe when you've already read it. You might want to add a check so that the player can only leave like this if they've talked to the person before, but I think the player ought to be able to cancel even if it's new dialogue. There might still be certain important story driven dialogue that they can't cancel, though.

Example GMK

For an example GMK, click here.

Until next time, happy coding, Code Ninjas!