Game Maker For Beginners: Part IV
Main Topics: Sprite Fonts, HUDs, ScriptsLevel: Total newb to Game Maker (that has read the previous tutorials). Suitable for programming nubs, also.
Abstract: We're making a simple, one-level side-scrolling shoot 'em up using Game Maker and GML. In this fourth part, we're going to add a simple title screen with some text on it and add a HUD to the game. We're also going to learn about scripts, which are player-created functions.IntroductionLast time, we made the screen scroll and added stars to the background and other boring junk like that. This time, we're dealing with some REALLY interesting stuff... TEXT.
Okay, text boxes, menus, user interfaces... these are really important aspects of a game. The player might not spend much time thinking about them when they're done well, but they stick out like a sore thumb when they're done lazily. Unfortunately, as a game developer you can spend a lot of time working on them.
You can easily use Arial, Courier, Helvetica, Verdana, or some other standard system font, but it looks cheeseball and people will notice. You also run the risk that the player doesn't have the font installed on their computer or their computer renders the font slightly differently. Fortunately, Game Maker supports sprite fonts. It may take a little longer to implement one (especially if you're drawing your own from scratch), but the results are well worth it.
We're also going to learn how to write scripts for commonly-used or important code. This will make our code much easier to read and write.
Creating a Sprite FontTo make a sprite font, first create a new sprite. Each of the frames of the sprite will be a different character of the font.
You can use as many or as few of the characters as you need. If you only need numbers, for example, you can just include those. The ordering, however, is important - it follows ASCII, and you can't skip any characters. So if you want to include numbers AND capital letters, like we are going to want in this tutorial, you'll need to include the 7 characters in between them - ":;<=>?@" - or at least 7 frames representing them.
This is the ASCII table:

And this is our sprite, sFont, that includes numbers and capital letters:

I made it using a combination of Dr. Mario, Megaman, and PC Bios characters! I got my characters from
here.
Using the Sprite FontLet's create a title screen to test our new font in. Make a room called rTitle and put two new objects in it, oInit and oTitle. oInit will be where we'll initialize the game. We won't have very much to initialize to begin with, but when your games get more complex, an object like this might become very useful. oTitle will handle everything that happens on the title screen.
Since we'll be using sFont throughout the entire game, let's make it a
global variable. If you'll recall, variables have scope that determines how far they exist. Global variables exist everywhere. Inside the Create Event for oInit, let's define our sprite font (we'll call it myFont) like this:
// Initialize the game
global.myFont = font_add_sprite(sFont, ord(' '), false, 0);
font_add_sprite() takes four parameters. The first parameter is the sprite containing your font. The second parameter is where in ASCII your font starts. We're starting with a blank space, or ' ', so we put in
ord(' '), but you can start with any character, e.g.
ord('0'),
ord('A'),
ord('$'), etc.
The third parameter is a boolean describing whether the font is proportional, i.e. whether the font should use the bounding box for the character's width (variable width) or the width of the sprite itself (fixed width). The last parameter is how many pixels of space between each character. Our font is a mono font and we don't want any extra space between the characters, so we put
false and 0 for these last parameters, respectively.
There's another way you could have defined myFont, though:
globalvar myFont;
myFont = font_add_sprite(sFont, ord(' '), false, 0);
If you declare a global variable this way, you don't have to add
global before the variable name when you call it. i.e. you can use myFont by just writing "myFont" and not "
global.myFont". However, I prefer writing out "
global.myFont" because it reminds me that it's a global variable.
Okay, to use the font, let's go to oTitle. Add a Draw Event and put in this code as the action:
draw_set_font(global.myFont);
draw_set_color(c_yellow);
draw_text(128, 96, "EL SHMUP!");
draw_set_color(c_white);
draw_text(88, 224, "PRESS 'X' TO START!");
I think this should be fairly obvious: we're setting the current font as our new sprite font, changing the color of the font, and then drawing the text we want on the screen. (I'm calling my game "El Shmup," by the way.) This code should write both the title and the instructions in the center of the screen:

But it's kind of a pain to figure out where the center of the screen is each time. Wouldn't it be nice we had a function that figured that out for us? Unfortunately, Game Maker doesn't have any built-in functions that centers text for us. But it does give us a way to write our own...
Need to know what a function does or what its parameters are? Look it up in Game Maker's built-in help! It's actually very good and should answer most of your questions, provided you know what to look for...
Scripts Are Very Useful!Game Maker has a lot of built-in functions that we've been using, but you can also write your own, called scripts. Scripts can take arguments and return values, too. Scripts are good for things that you want to use over and over again.
Let's write a script that draws centered text, because that seems pretty useful. Right-click on the Scripts folder on your left and create a new script. Add this code to it:
// Draws text centered within a certain area
// Give the arguments more descriptive names
str = argument0;
xPos = argument1;
yPos = argument2;
widthOfChar = argument3;
widthOfArea = argument4;
// Calculate the margin
strLen = string_length(str) * widthOfChar;
n = widthOfArea - strLen;
n = ceil(n / 2);
// Voila!
draw_text(xPos + n, yPos, str);
So... the words "parameter" and "argument" are used somewhat interchangeably by lazy computer scientists, but they're actually different. Using a vending machine as an analogy, "coins" would be the parameter, and if you put in a quarter, that's the argument. A parameter defines what the function accepts, and an argument is like the actual value you end up feeding it.
Scripts always take in something like 5-6 arguments, although you don't have to use all of them. In that case, the value will default to zero. The arguments are named argument0, argument1, etc.
Our new script, which we'll call "drawTextCentered", will take four arguments: (1) the string of text, or simply string, that we want to write out, (2) the x-position of the drawing area, (3) the y-position of the drawing area, (4) the width of each character, and (5) the width of the area in which we want to center the text.
The way
drawTextCentered() works is that first it figures out the difference between the length of the text in pixels and the length of the area in which we want to center the text. Then it divides that by two. That resulting number, n, must be how much a margin to give the text in order to center it.
Let's use our new script. Go back to the Draw Event of oTitle. Instead of using draw_text(), we'll use
drawTextCentered() instead, like so:
draw_set_font(global.myFont);
draw_set_color(c_yellow);
drawTextCentered("EL SHMUP!", 0, 96, 8, 320);
draw_set_color(c_white);
drawTextCentered("PRESS 'X' TO START!", 0, 224, 8, 320);
Okay, let's have a look:

The results are the same, but now we're coding with the POWER OF SCIENCE instead of just using brute force. And if we ever need to center text again, it will be easy as pie.
In conclusion: Scripts are really useful for when you need something reusable. They can also be useful for compartmentalizing your code. By breaking up your code into more manageable chunks and hiding some of the details behind a script/function name, it makes it much easier to program. All the coder needs to know is what he/she needs to put in and what he/she will get out on the other end. This is part of an important concept in computer science called
abstraction.
The Random Number ScriptOur
drawTextCentered() script just ran some code, but scripts can also
return values. Here's a very useful random number script that takes two arguments and returns a random integer between and including those arguments:
randNum = argument1 - argument0 + 1;
return floor(random(randNum)) + argument0;
I call my random number script "rand" and you can use it like this:
n = rand(1,10);
That will set n to a random number from 1 to 10. Useful for random enemy behavior, or random anything, really!
Finishing the Title ScreenTo finish up the title screen, add a Press Key Event and use the built-in "Different Room" action to have it go to rLevel1 when you press 'X'. For fun, I added an "Interlaced from left" transition to it, to make it fade in and out in a fun way. You could also do this in code, like so:
transition_kind = 10; // See GM Help for transition types
room_goto(rLevel1);
Make sure rTitle is the first room in the list of rooms. The first room is always the room that the game will start in.
Adding a HUDNow that we have a sprite font and we know how to draw text, let's make a simple HUD. We're only going to add a little life counter to the screen, but it should be easy enough for you to extend this to a score counter or whatever else you need. Our life counter will go in the lower-left and will stay in the same place even as the screen scrolls.
I drew a little "extra ship" icon for this very purpose, called sPlayerShipIcon:

But first we need to give the player some lives to lose. We COULD add this to the oPlayerShip object. I guess that would make sense... although really, lives is something that the game itself is more concerned with. For example, if the player loses all his or her lives and the oPlayerShip object is destroyed, then we will lose that variable. The game needs to know at all times how many lives the player has.
So let's make pLives a global variable, instead. We can declare and define this variable in oInit or in oTitle. I'm just going to declare and define it in oTitle, when the player starts a new game:
// Initialize a new game
global.pLives = 2;
Now, let's return to oGame and draw our player ship icon and the number of lives in the lower-left corner of the screen:
// Draw HUD
draw_sprite(sPlayerShipIcon, 0, view_xview[0]+3, 226);
draw_text(view_xview[0]+14, 228, string(global.pLives));
string() is a useful function that converts a number into a string of text.
draw_sprite() will draw a single frame of a sprite somewhere on the screen. You can also use
draw_sprite_ext(), which has more parameters, such as the rotation and color of the image.
Actually, in the absence of a Draw Event, Game Maker will call draw_sprite_ext() automatically. All of our previous objects, like oPlayerShip and oEnemyFlyer, are being drawn even though they don't have Draw Events, remember? But you can imagine that they have Draw Events with this single line in them:
draw_sprite_ext(sprite_index, image_index, x, y, image_xscale, image_yscale, image_angle, image_blend, image_alpha);
Don't forget to include
view_xview[0] to account for the screen scrolling! We want the HUD to remain in the same place on the screen at all times. If it all works out, you should get something like this:
Game States and Game OverWe have lives, we have a HUD, we have a centered text script... let's make it so that when the player ship explodes, the player loses a life, and if he/she runs out of lives, it displays "GAME OVER" on the screen and then returns to the title screen.
First, let's add this line of code when the player collides into an enemy or a wall:
global.pLives -= 1;
The full code block should look like this now:
oGame.reviveTimer = 20;
instance_create(x, y, oExplosion);
global.pLives -= 1;
instance_destroy();
Hm, we have the same code written twice, for both an enemy and wall collision. Maybe this is a good place to use a script? Let's copy that code into a script called "killPlayer" and run the script when we want the player to die:
killPlayer();
Next, the game needs to check for when pLives < 0 so that it can start the game over sequence. But before we do that, we need to define some
game states.
So we're going to declare a variable called "state" in oGame that keeps track of what state the game is in. "Play", "Pause", "Menu", "Game Over"... these are all states that your average game could be in at a given time. Just like a player, an enemy, or some other object, the game itself needs to keep track of what state it's in so that it's doing the right thing at the right time.
In our case, we'll start with two states: "Play" and "Game Over".
Now, we could have a variable named "state" and simply assign it 0 or 1 and remember that 0 = Play and 1 = Game Over... but it'd be easier if the states were named in the game itself. So let's make a couple of variables to represent the states (this is in the Create Event of oGame):
// Game State Constants
PLAY = 0;
GAME_OVER = 1;
state = PLAY;
In programming, variables like PLAY and GAME_OVER are called
constants. These are variables whose value never changes, but are used to make code look more natural and easy to read/remember. For example, it's a lot easier to understand "state = PLAY" then to understand "state = 0". Even with just two states it's a lot more readable, right? So imagine situations where you may have hundreds of such states!
Some compilers will actually throw errors if you declare a constant variable and then try to change its value. In Game Maker there is no distinction made, but for all intents and purposes, we are treating these as constants. We designate that they are constants by naming them in ALL CAPS... hopefully that will remind us that we do not ever want to change the values of these variables.
So with our states defined, let's make it so that when the game is in the PLAY state, it displays the HUD, but if it's in the GAME_OVER state, it displays "GAME OVER" in the center of the screen:
if (state == PLAY)
{
// Draw HUD
draw_sprite(sPlayerShipIcon, 0, view_xview[0]+3, 226);
draw_text(view_xview[0]+14, 228, string(global.pLives));
}
else if (state == GAME_OVER)
{
drawTextCentered("GAME OVER", view_xview[0], 112, 8, 320);
}
Also, let's wrap the code in the Step Event of oGame in an if statement:
if (state == PLAY)
{
view_xview[0] += 1; // Screen scroll
// Revive the player if he dies
if (not instance_exists(oPlayerShip))
{
if (reviveTimer > 0) reviveTimer -= 1;
else instance_create(view_xview[0]+32, view_yview[0]+112, oPlayerShip);
}
else
{
oPlayerShip.x += 1;
}
// Deactivate objects outside of the view
instance_deactivate_region(view_xview[0], 0, view_wview[0], room_height, true, true);
instance_activate_region(view_xview[0], 0, view_wview[0], room_height, true);
}
All of that code should only be run when the player is still alive.
Now, go into the
killPlayer() script and add a line to it to change the game's state to GAME_OVER when the number of lives < 0:
oGame.reviveTimer = 20;
instance_create(x, y, oExplosion);
global.pLives -= 1;
if (global.pLives < 0) oGame.state = 1;
instance_destroy();
Notice that I said "oGame.state = 1;" and not "oGame.state = GAME_OVER;". Since we declared GAME_OVER in oGame, oPlayer doesn't know what it is. We could define the constant as a global variable, perhaps, to be really thorough about it, but then it might be worth it to give the variable a more distinctive name, like GAME_STATE_GAME_OVER or something.
Okay, almost there... let's just add a Press X-Key Event to oGame that will take us back to the Title Screen during the game over:
if (state == GAME_OVER)
{
room_goto(rTitle);
}
Whew! A game without a title screen and a game over is like a story without a beginning or an end. We covered a lot of ground to get to this point. Good work!


Extra Credit: Can you add a pause to the game using
instance_deactivate_all()? What about a Gradius-style power-up system? Also, it might be worthwhile to make sure the ship doesn't reappear inside a wall or enemy after the player dies. How would you set checkpoints that the player could reappear at?
I did NOT include this in the source code this time, so you'll have to figure it out yourself! You have a lot of knowledge at this point to make the game really interesting. Go to it!
[Download the Source for this Tutorial]Next Up: The Sound of Music... and sound effects, too!