Pages

22 February 2010

Code of the Ninja: Text Boxes

If you haven't already, read the Code of the Ninja: Introduction

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!

If you use my code or scripts in your game or engine, no credit is necessary. But I'd love to hear about your project if you do! Just drop me a comment below, or e-mail me at us.mercurysilver@gmail.com

04 February 2010

Sonic 4?

Well, if you're a Sonic the Hedgehog fan, you're probably aware that Sega has finally unveiled Project: Needlemouse, announcing it to be "Sonic the Hedgehog 4: Episode 1".

What follows is an extremely critical and negative piece on what they've revealed. If you're excited and happy with Sega right now, and don't want any buzzkill, I urge you to consider not reading any further.

Firstly, before anyone starts slinging around the "What can we tell from 3 seconds of video?" canard, I'll tell you - a hell of a lot. The classic Sonic community can tell by a lone screenshot whether one palette slot is off by a shade, or whether the HUD is moved by a single pixel. They even make purposeful "tech hoax" screenshots in order to hone these skills. No one is better equipped to drain bucketloads of info from a short video clip.

And what a clip! Let's break it down.

First, we see some clips of the classic games, in a sort of timeline, to build up the suspense. Finally we reach 2010 and see a polygon Sonic (in the modern style) jumping on the heads of some polygon Badniks.

(Motora/Motobug, who we know to be in the game already, and Halogen/Batbot from Carnival Night (Sonic 3). Another rehash Badnik? I'm not really surprised at this point. The thing that bothers me is that Badniks in the old days were (mostly) tied to their Zone. Halogen was part of Carnival Night. Seeing it anywhere else is like seeing Knuckles with Tails' tails. Come on, Sega, not even all fangames cut and paste like this!)

Then we finally see some footage of the game in action. Ironically, the clips of the classic games were shown behind a crappy filter, so that when footage of Needlemouse is finally shown, it'll "pop". What a dirty trick. If they had any confidence at all in their product, they would have shown an honest, direct comparison. A true Sonic 4 should impress, back to back with the classics or not.

What does the footage show?

A Green Hill rip-off. Closer to Green Hill than any main Sonic game has yet dared. Sonic 2 and Sonic CD at least bothered to change the ground pattern.

And a corkscrew - lifted straight from Emerald Hill (and the many Sonics to have rehashed it since).

And... that's all. Yep, they finally show the game in action, and they show absolutely nothing new. Say it with me, now - ABSOLUTELY NOTHING NEW. Way to build excitement, Sega - don't show anything we can get excited over!

My first reaction to the footage, honest-to-goodness, was gales of uncontrollable hysterical laughter. If I didn't know Sega so well, I would have thought it was a joke.

I thought it was hilarious. I don't think that's the reaction Sega wanted.

Don't get me wrong, the game looks alright. It's not a hideous mess by any means. It's just about good enough to be a handheld title, or an iPhone game. A little trip through classic Sonic elements, like a next generation Sonic Pocket Adventure.

But they're calling this Sonic 4. And as Sonic 4, it's a disgrace.

It's a disgrace that they're putting "Modern Sonic" in it, for one. Now, I know some are going to jump on my back for this, saying that I'm harping on a detail. But since when was the look of the main character (the only character, in this case), and the entire art style of the game just a detail!? Yes, the gameplay is more important, but that by no means makes the art style unimportant. Who wants to play the Sonic 4 they've been waiting for for 16 years, starring the smirking fool Sonic that haunts their Sonic Heroes fueled nightmares?

But it's not so much that the modern style is bad. It's more the message Sega sends by using it at all. This was their promise to return to the classics. This was Sonic 4. This was their chance. But they still decide to use the brand-consistent Olympic Games Sonic, so as not to confuse the kiddies. What a kick in the nuts for us classic fans. "Sorry guys. We pretended we were making a game just for you, but that was just to build hype. You didn't expect us to actually care enough to follow through?"

Hell, I would have taken a brand new re-design. Floppy-spined Sonic has been stalking us for over a decade now. It's time he was retired. But no - instead we get a game that elevates Modern Sonic as a direct continuation of the classics. Why don't you just reach into our heads and take our memories of classic Sonic, too?

Actually, fuck "classic" Sonic. He doesn't need a differentiating adjective. He is Sonic. This look-almost-alike they've been using is nothing but a poser.

But you know what bothers me the most about all this? Sega can make a bad game if they want - more power to them, I don't have to play it. It's the fan reaction. The unreasonably beleaguered fans, who have put up with Sega's beatings since 2001. They're starting to just give up and take it.

I - and many others - credit the 16-bit Sonic the Hedgehog games with sparking my creativity. Sonic taught us to dream big and never give up.

But I'm not seeing that from the fanbase anymore. It's like after eating Sega's shit for 10 years they're starting to get used to the flavour. Goddamnit, STAND UP FOR YOURSELVES! Stop acting like the victim in an abusive relationship. "I probably won't ever get anything better." "My expectations were too high..." "It must be me. Maybe I don't deserve any better."

It's not us, it's them. All this talk of the fanbase and their "ridiculously high expectations" is crazy talk. You can make it sound reasonable - you can rationalise all you want - but in the end, all I hear is excuses. People making excuses for being less than stellar.

Maybe it's because I'm American. Cowboys, Astronauts, Bruce Willis. You have to dare to be awesome. Second best isn't good enough. There is no fucking excuse to be less than stellar. This is Sonic 4, and it's barely a cut above Sonic Rivals. Don't you dare pin that on me and my expectations. That's irrational faith, born of years of disappointment, talking.

Don't you think that after waiting 16 years we deserve something that looks better than the hoax screenshots that floated around?

I'd hate to be so desperate for a Sonic game that I'd be willing to compromise my standards. I'm not an addict, jonesing for a classic fix so bad that I'll praise Sega up and down for including a corkscrew. This isn't our only chance for a Sonic 4, it's just one more in a long line of failures and WE DON'T HAVE TO TAKE IT. We've been sending the message that we're willing to take Sega's table scraps for too long.

It's highly offensive that Sega thinks that showing what they have so far shown in any way would excite us. Sadly, though, many are in a euphoric tizzy right now, so I guess Sega was right. I thought more highly of most fans.

But I'm not blown away by borrowing classic elements. Sonic Advance 2 had corkscrews. Where's the creativity?

Perhaps it's in later levels. Yeah, right. Any team so uncreative that they would green-light a rip-off first zone is really able to be trusted to pull out all the stops for a second. Where did the Shigeru Miyamoto philosophy of making the first 30 minutes of a game the most exciting part?

And even if the whole game is an unimaginative failure, the blindered Sega-phants will just keep hoping for Episode 2. Or the 2011 Sonic. It's always next time, next time. ("Maybe God will cure my next cancer!")

It's been explicity stated by Sega that this isn't part of the main Sonic brand (there will be a "real" game in 2011, supposedly), which is greatly outraging on two counts. First, if they're not risking their main brand, why are thay playing it so safe? Second, this reveals exactly what Sega thinks of the old Sonic games. They're not worth being major releases. They're dippier, second class citisens. Sonic the Hedgehog 4 - the continuation of the main series - isn't going to be a triple-A blow-your-pants-off title, but a low-budget downloadable release. What a big, officially sanctioned crap all over the classic games.

Meh. I'm getting too worked up. I wouldn't be so offended, except for the title. If they called this Sonic Genesis 2, then I'd be happier =P.

Well, I hope Sega finds some way to pull off a win and prove me totally wrong. I honestly do. But we'd all be as blue as Sonic if we held our breath for it.

Wake me when Sonic really makes his return.

03 February 2010

Thoughts on Hedgehog Day

Well, yesterday was "Hedgehog Day" - to us classic Sonic fans, anyway, who have thought of it as such ever since the release of Sonic the Hedgehog 3 on that very day over a decade ago in 1994.

To celebrate, I played Sonic 1 and Sonic 2 back to back, straight through. I had a fantastic time. All I gotta say is: if Sega hopes to recapture the magic that those games had, it's gonna take Herculean effort.

About that.... Sega's own method of celebrating Hedgehog Day involved drip-feeding a few new bits of Project: Needlemouse to the salivating public.

Before I get to that, though, I should quickly mention what I thought of the last batch, which I neglected to comment on before. Back in January, they posted a second piece of Badnik concept art and an intriguing paragraph.

A Motobug. It's okay - again, I'm happy that the design is pretty faithful to the original, but I'm not a fan of the sunken eyes at all. It also, being a Badnik from Sonic 1, further reinforces the fear that Needlemouse won't be as creative and original as it should be.

But they also left us with this:

Speed is something that is not given; but rather earned through dedication. Speed is not found by simply pushing a boost button, but by building momentum. It is the reward for skill in the face of difficult challenges – this kind of speed is the most exhilarating, not only because it is fast, but because of the pure perfection such speed exemplifies. This is the truth of the original Sonic games – and this is the truth of Project Needlemouse.

Well, duh, right? The community's been saying this for years. It's kind of irritating to hear Sega quote it back to us as though they were making some sort of genius revelation. But it's nice to know that they've gotten the message.

At this point, my excitement meter would have shot up a few points. It looked like Sega was honestly trying hard. But I held off on airing my thoughts, because a new batch of goodies was so soon forthcoming.

Which brings us to today. This is what they've revealed:

That's right. A flower and a palmtree. A FLOWER AND A PALMTREE.

Note: What follows will be my emotional response. I am a rational person, and I understand that Sega of America isn't allowed to give away too much, and that mere concept art isn't indicative of the finished product. That said, I'm going with my gut reaction just for the sake of historical record.

I am extremely disappointed. Badniks were one thing - classic Sonic games weren't above reusing a Badnik design here or there.

But that flower and tree! Not just any flower and tree, either, but palpably the ones from Green Hill Zone. A spinning sunflower? Not even Palmtree Panic Zone, which is the closest classic Sonic ever got to a true Green Hill remake, had spinning sunflowers. And Sonic CD's zones were really similar to Sonic 1's on purpose!.

This is disappointing because it yet again reinforces the uncreativity and unoriginality that has plagued Project: Needlemouse's image since the first trailer that invoked the winged ring logo. They still have shown us nothing that proves they can be at least as creative as the Sonic fan community.

And there's no good reason. I mean, remember this?

Wow! Trees, growing crystals. And the sea showing behind some mountains, to make it feel more like a nearby islet. It's nothing totally revolutionary, but it's just enough of a cute twist to make it feel like something new and exciting.

This was precisely the type of thing I was hoping to see from the zone concept art on Hedgehog Day. The one thing I desperately did not want to see was anything from Green Hill Zone. Sega blew it completely.

They even make a lame excuse, claiming that sometimes it's better to "renovate than innovate":

...the levels themselves also have to exude a style that matches or improves upon what came before them. Often, it is not about attempting to revolutionize, but rather to stay true to the formulas for Sonic’s success that already exist.

I find that a rather weak way to apologise for not having anything new or exciting to show.

But that's not the worst of it. They also teased the music. It's awful. I know it's a pitifully short sample, but everything about it is wrong. I won't bother saying more about it, though, because Tweaker put it best at Sonic Retro.

You know, I frankly find it almost insulting to think that Sega thinks the classic fans will jump for joy at a stupid rotating flower. It shows an unhealthy focus on the details of the classic games, and not the underlying spirit that made them great. If all it took was a copy and paste from Green Hill Zone to make a good Sonic game, we'd have had one already. It's gonna take a lot more than pointing to an individual foreground element from an early game, saying, "Remember that? We got that!" to inspire our imagination. Remember, Sonic 3 had a tropical island zone - and then it blew it up. Now, that's the way to start a game!

All in all, it's been pretty disappointing. I'm still slightly optimistic, and waiting for Thursday when more news is supposed to come. But I'm getting the strong feeling that Sega ain't got the touch. I'm still pinning my hopes on the fangame community to supply my Sonic thrills.