This tutorial will take you through the process of creating a small game using GameMaker Studio 2, with the aim to get you familiarised with the interface and the work flow. The game we've chosen for this tutorial is a typical asteroid shooter game, which we'll call Space Rocks. Almost all novice programmers make this game at some point, as it's a fun game to play, but fairly simple to make and shows all the most basic components of a game - things you'll find in the most basic arcade game up to the most advanced RPG - like movement, logic and gameplay design.
We know that getting started with any new skill or tool can be tough at first, which is why this tutorial will attempt to make the introduction to game making as smooth and fun as possible. The tutorial itself is split into 4 parts which are listed below:
- Chapter 1 - Setup And Moving
- Chapter 2 - Attacking and Collisions
- Chapter 3 - Score, Lives and Effects
- Chapter 4 - Sound Effects and Polish
For those of you that have not used the GameMaker Studio 2 IDE before, we have prepared a short introductory video here, and you can also check out the Quick Start Guide in the manual.
Chapter 1: What We Need
In this chapter, we're going to start building our game, but before we do that, we need to just take a moment to think about what we'll actually need. In general, before starting any project, it's a good idea to plan out what you want to achieve, even if it's in very broad terms, as that gives you something to work towards when you sit down to work.
Our tutorial game is going to be called "Space Rocks", and as mentioned in the introduction, it's going to be a game about shooting asteroids and getting high scores:
What does a game like this need? Let's list the two most basic of the gameplay components:
- First we'll need a ship for the player to control, and it will have to rotate and move forwards as well as shoot.
- Second we'll need some asteroids for the player to actually shoot at
Now, before continuing, you should make sure that the game is set to run at 60FPS. For that you should open the Game Options now (from the Quick Button at the top of the IDE), and set the Game Frames per Second to 60:
With that done, we can now move on to the next part, which is to create the sprites that the game needs.
Making Sprites
The first sprite we'll make is the "ship" sprite for the player to control. To do this, go to the "Sprites" resource and click the right mouse button and select Create:
This will open the sprite editor and permit you to set the name of the sprite. In this case we'll call it "spr_ship", as shown in the image above.
You may now also need to resize the sprite, as for this game we want it to be 32x32px. To resize a sprite, you simply click on the Resize Sprite button to open up the resize window:
You'll want to choose the Resize Canvas option and set it to 32x32px:
With that done, the next thing to do is create our sprite in the Image Editor. You can open this by clicking on the Edit Image button, which will bring up the editor with a blank canvas ready for you to start drawing:
Our sprite is going to be a simple line-drawn polygon, much like an arrow head, so let's draw that now. First you should probably enable the grid overlay and set the grid to 8x8px (this will help you to position the drawing tool):
Now we can draw the sprite using the Draw Polygon tool (be sure to click the top left of the icon to get the outline tool and not the fill tool):
Once you're happy with the result, simply close the Image Editor workspace and the image will be saved to the sprite. We now need to set the sprite's origin, which is the value which will be used as its top-left position when it is drawn in-game. We want our ship to rotate around its middle, so we need the origin at the centre of the sprite.:
You can see the sprite editor allows us to change this in several ways, but go ahead and select the Middle Centre option from the drop-down menu in the Sprite Editor.
We can now go on and make the asteroid sprites. So, for this, you'll need to create three new sprites, as we are going to make three different sizes of asteroid. Call the sprites "spr_asteroid_small", "spr_asteroid_med" and "spr_asteroid_huge". All the sprites will have been created at the default image size, so you will want to change their size to be 16x16px, 32x32px and 64x64px respectively (as shown at the beginning of this article). When finished, you can take a moment to draw the asteroids, again using the outline polygon tool to create some irregular shapes. You can see some examples below (shown at twice the size required for the game):
When finished, make sure the origin for each of the sprites is set to Middle Centre. The idea now is that the asteroids in our game will start of huge and then break into the medium and smaller parts as the player shoots them.
It's now time to make some objects.
Making Objects
Let's jump right in and make our first object. As we did with sprites, simply go to the "Objects" resource and click the right mouse button and select Create to create the object. Give it the name "obj_ship" to identify it, and then assign it the ship sprite we made earlier. To assign the sprite, click the button that currently says "No Sprite" and select the sprite "spr_ship":
While we're at it, we'll also create an asteroid object, so do that now (create a new object) and call it "obj_asteroid". Assign this object any one of the asteroid sprites that you created. We don't need to worry about which one, as we're only going to have a single asteroid object in the game, and we'll set the sprite_index (the sprite the object will draw) using code in the Create Event. We'll come to that part in a moment, but first we'll do an experiment...
Editing Rooms
All projects in GameMaker Studio 2 start with a room already in the resource tree called "Room_0". This means we don't need to create one for now, but you should rename this room to "rm_game" so it has a less generic name (this is done by performing a slow double-click on the name in the resource tree and then typing the new name). Now, what happens if we add instances of our objects to the room and test it?
Let's find out! Open the Room Editor by double clicking on the room in the resource tree. When the Room Editor opens, you'll see a lot of new tabs appear in the IDE. We're not going to worry too much about them right now, so just click and drag an instance of the object "obj_ship" into the room:
Now drag in a few instances of the object "obj_Asteroid", so that it looks like this:
If you click the Play quick button now, the game will compile and you should see a window open with the instances we've placed in the room visible:
Okay, that's not very interactive, but it's a start! Seeing the game like this though does show up a minor problem... the game window is too big. We want the game to have a retro feel, so that means a smaller resolution and play area. Close the game window and go back to the room editor now.
We are going to edit the size of the room, so go to the Room Properties and change the Width and height to 500px each:
This will change the room size, but may leave some instances outside of the room. You can simply click on them then drag them back into the playable room area.
If you test play the game again, you can see that the game window is a much more appropriate size.
We can now go back to our game objects and start to add some logic into them so they actually do something...
Game Logic
When programming, everything can be broken down to a fairly simply rule:
if this then that
So, all a program does is check if this happens, and if it does then that happens - like, "if a key is pressed then the player will move". Put into a more GameMaker Studio 2 friendly format it would be expressed as
if event then act
Which means that if an event is triggered, then a specific action must be performed. An event is simply a moment in time when something happens, and some events can be triggered once (like the Global Mouse Left Down event) or can be triggered every game frame (like the Step Event). Let's look at how to use these events to make our ship perform an action, in this case, move.
You'll need to open the object "obj_ship" (if it's not already open) by double clicking it in the resource tree. When open, you can then click on the Add Event to bring up the Event List:
There are two ways that we can check if the player is moving:
- Use the discreet Keyboard Events, which will only be triggered when a key press is detected, or
- Use the Step Event and check using code for a keypress every step (game frame)
What you use in your projects is largely a matter of choice and will depend on how you like to work and the structure of your project. In this case, we are going to use the Step Event, as it can sometimes be clearer to see the game logic where it's all together, so go ahead and add a Step Event now.
We're going to add some code here, and it will be run 60 times every second. Why? Because we set the game speed to be 60, which means that there will be 60 game frames ("steps") for every second that passes, and since the Step Event is triggered every game frame, any code we add into this event will be run 60 times each second too.
The basic interpretation of what we want to do first would be "if the left key is pressed, rotate the ship to the left". To get this into code we need to structure it like this:
if (expression)
{
<statement>
<statement>
<statement>
}
So, we have our "if" at the start and we'll be checking whatever expression goes inside the parenthesis () to see if it evaluates to a true or a false result. So something like "if (my_variable == 1)" checks to see if a variable is exactly equal to 1 and if that is true then it will go on to perform the code that you have in the {}.
Let's add the actual code into the Step Event of the "obj_ship" now:
if (keyboard_check(vk_left))
{
image_angle = image_angle + 5;
}
Here we check the function keyboard_check() to see if the left arrow key is being held down, and if it is (the function will return true if it is or false if it's not) then we add 5 to the image_angle of the instance. The image_angle variable governs the angle at which the assigned sprite will be drawn. Note that at the end of each statement we add a semi-colon ;, which is the code equivalent of the full-stop at the end of a sentence.
Why are we adding 5 if we are turning left? That's because angles in GameMaker Studio 2 go anti-clockwise with 0° being to the right:
This is why we made our ship sprite facing right - it makes setting the angles when rotating much easier!
We can use almost the exact same code for turning the ship to the right, so copy the lines of code for the left key (you can select the lines and then use / + "D" to duplicate them) and then make the following changes:
if (keyboard_check(vk_right))
{
image_angle = image_angle - 5;
}
You can press the Play button now, and if you press the left/right arrow keys you should see your ship instance rotating.
We are now ready to make the player ship move!
Moving
Our player ship can turn left and right, but it's not able to move yet, let's fix that now! To start with, go back to the object obj_ship, and in the Step Event add the following code, after the code we have already for rotating the image_angle:
if (keyboard_check(vk_up))
{
motion_add(image_angle, 0.05);
}
We already know what the keyboard_check() function does, but what about motion_add()? Well, to explain this you have to understand that there are two different types of movement available to you when using GameMaker Studio 2.
The first type of movement is to simply set the x/y instance variables for the instance in the room. You can do this through code, for example, the following code would move the instance to the right along the X axis by 5 pixels each step:
x = x + 5;
Subtracting 5 each step would move the instance left, and the same goes for the Y axis, where adding to y will move the instance down and subtracting will move the instance up. To see this in action, simply go into the room editor, double click on the "obj_ship" instance, and then change the x/y values, or click on it and drag it around to see them change:
So, by changing the x and y values of the instance variables we can move the ship around.
The other method of movement is to set the speed and direction. The "speed" of an instance in GameMaker Studio 2 is the number of pixels the instance should move each step of the game, so setting speed = 2; means that the instance will move 2 pixels every step of the game. However, speed means nothing without a direction, and so we also have the direction variable to set the direction of movement when speed is anything other than 0. Together these create a vector.
Now, explaining vector maths is outside of the scope of this tutorial, but luckily there's no need as GameMaker Studio 2 has functions that will help you , without having any advanced mathematical knowledge. In this case it's the motion_add() function. This function takes an angle of direction, and then adds a certain amount of momentum to the speed of the instance in that direction - in the above code we are using the current image_angle to get the direction of motion and setting the amount of momentum to add as 0.05 pixels per step. This might seem a small amount, but adding to the ship momentum is a cumulative effect, so each step that we add this value, the ship will get faster and faster (in the same direction). We use this method over setting the x/y position directly, as it enables us to easily create a "floaty" feel for the movement, perfect for a game set in space!
With that done, you can test the game again, and you should be able to move the ship around by tapping or holding the Up Arrow key, and if you rotate then the direction of movement should change over time too.
Wrap The Room
Testing the game, you likely noticed an issue: the ship can go out of sight beyond the room! This isn't very much fun and certainly not what we want to happen. Instead, we want the ship to "wrap around" the room, so when it goes out one side it will appear again on the opposite.
We could add in some code to check the x and y position and then move the player instance ourselves, but there is an easier way...
We're going to use another function from the GameMaker Language, move_wrap(). This function permits you wrap horizontally and/or vertically, as well as set a margin for wrapping to occur in., and we'll place it after all the other code in the "obj_ship" Step Event:
move_wrap(true, true, 0);
This will wrap the ship around the room along both the horizontal and vertical axis. You can test the room now and see what happens!
That's working okay, but there is a visible error as the ship reaches the edge of the screen. Because we have set the wrap margin to 0, the wrapping will occur the moment the player ship x or y position leaves the room. This means that the ship can be seen to "disappear" and then just as abruptly appear on the other side of the room. To resolve this we need to set the margin to a different value:
move_wrap(true, true, sprite_width / 2);
Here we use another instance variable, sprite_width, and set the wrap margin to be half the width of the sprite. This means that the instance will not be considered out of the room and be wrapped if it's not gone at least half the sprite width outside. So, since our sprite is 32px wide (and tall), it won't be wrapped until the position is greater than 16 pixels outside the room bounds. If the instance is moving right, for example, it will wrap when the x position is greater than 516 (room width is 500 plus half the sprite width which is 16).
If you test it again, you'll see that the wrapping looks much better.
With the player ship movement completed, let's go ahead and get the asteroids moving too.
Asteroids
Currently, the asteroid object does nothing. It has a single sprite and doesn't move, so we need to fix that and make things more interesting!
We are going to add some code into the object "obj_asteroid" Create Event. Unlike the Step Event, this event is only triggered once, when an instance of the object is created, and because of this it's an ideal place to initialise variables and run any functions that you want when creating the instance.
We want our asteroids to be different from each other when they're created, and so we'll set the instance variable sprite_index to use a random sprite from our resource tree. The sprite_index is the variable that holds the assigned sprite ID value, or "index", and so we can change it at any time to change the sprite the object will draw. We'll also use the choose() function, to randomly pick from one of the three sprites we previously created.
So, open up "obj_asteroid" and add a Create Event to the Event Editor. This will open a code editor where we'll add the following:
sprite_index = choose(spr_asteroid_small, spr_asteroid_med, spr_asteroid_huge);
If you test the game again you'll get something like the following:
We need to set the asteroid moving in a direction now, and we want that direction to be random. We briefly mentioned the direction built-in variable earlier, and we'll use it now, like this, after the code to choose the sprite_index:
direction = irandom_range(0, 359);
Now, direction is not the same as the image_angle, since one is the direction of movement, and the other is the angle a sprite will be drawn at, so we might as well change the image_angle to a random value too, as that will increase the visual variety:
image_angle = irandom_range(0, 359);
In both lines of code we are using the function irandom_range(), which will return a random integer value between 0 and 359 inclusive.
Finally in this event, we need to set the speed of the instance, so add this line of code:
speed = 1;
So now the asteroids will move 1px per step in a random direction and be drawn at a random angle. But before we test, there is one final thing we need to do, and that's add the wrapping code we used on our player ship object to also wrap the asteroids, otherwise when they leave the room they'll be lost forever!
Add a Step Event into the "obj_asteroid" and copy/paste the move_wrap() code from the player ship object:
move_wrap(true, true, sprite_width / 2);
We'll also make the asteroids spin a little as they move, again to add more visual interest to the game. For that, you'd add this into the Step Event
image_angle = image_angle + 1;
Test the game again now and you will see that the asteroids now move, spin and wrap around the room!
Chapter 2: Collision Masks
In this chapter, we're going to add in bullets and have collisions for them with the asteroids, as well as collisions for the asteroids with the player ship.
Since we haven't defined any bullet objects or sprites, let's first deal with the collision between the player ship object "obj_ship" and the asteroid objects "obj_asteroid". Now, collisions in GameMaker Studio 2 are based off of what is called the collision mask, which is defined in the Sprite Editor:
This "mask" is the area that will be used by GameMaker Studio 2 to detect collisions, so that two instances are considered "in collision" when their collision masks overlap, or a mouse click is detected when the mouse x/y position is inside the defined mask area. There are a number of different mask options to choose from, including the mode - whether the mask should be defined automatically or manually - and the type - the shape that should be used to generate the mask.
If you look through the type options, you'll see that there is a precise option. This makes the collision mask "pixel perfect", so that transparent areas will be ignored. You might think that this is the best option of all, as it means that all your collisions will be realistic, but it's not. Calculating collisions is costly at the best of times, and using a precise pixel-perfect check for everything would increase the cost of doing collisions greatly. This is why we generally use rectangular collision masks, as they are relatively fast to check and deal with.
That said, for our player object, we'll be using a Diamond collision mask. This is still a faster mask type than "precise", and has the benefit that is fits the "nose" of our ship sprite nicely. So, select the diamond type now, and set the mode not manual, then position the mask over the ship sprite, as shown below:
You'll notice that the "wings" of the player ship sprite aren't covered by the mask, which means that they will not detect collisions. In general, this is fine and pretty much normal for video games. Players don't want every little thing to kill them, and as long as the collision mask is consistent and they can discover what area is okay and what area isn't, then it's fine. What would be worse would be a rectangular collision mask, as some things would be considered in a collision when visually they aren't, and so the player would feel cheated.
With that done, let's open each of the asteroid sprites and set the collision mask for them. We don't need anything fancy here, so leave the mode on Automatic and set the type to ellipse:
When you've given each asteroid sprite an ellipse collision mask, we can move on and code the collisions themselves...
Collisions
We need to add another event to our player object "obj_ship", so open that now and add a Collision Event between the ship object and the asteroid object "obj_asteroid":
We could instead add a collision event to the asteroid object to look for collisions between them and the player ship, but in general you want to have the instance there is least of (in this case "obj_ship") do the checking, as less checks means better performance, so having one instance checking for a collision is better than having 10 instances checking for the same collision.
In this event we're simply going to add this code:
instance_destroy();
We'll add more things to this event later to control lives and stuff, but for now all we're doing is destroying the instance (removing it from the game room), and if you test the game now, you'll see that the ship "disappears" (is destroyed) when it collides with an asteroid.
Shooting
Asteroids can now destroy the player, but we haven't got any mechanism for the player to destroy the asteroids! Let's add that now...
The first thing we need to do is make the bullet sprite, so make a new Sprite Resource, call it "spr_bullet" and set its size to 2x2px:
Now, edit the sprite in the Image Editor and colour it white, so that you get a 2x2px white square. Close the Image Editor, and then set the origin of the sprite to Middle Center and leave the collision mask on its default properties, which are perfectly fine for this sprite.
The next thing to do is make a new object, call it "obj_bullet" and assign it this new sprite that you've just created:
We can go back to the player ship object now, and open up the Step Event again. We are going to add the following code to detect a key press and then create an instance of the bullet object:
if (keyboard_check_pressed(vk_space))
{
instance_create_layer(x, y, "Instances", obj_bullet);
}
So, we're using keyboard_check_pressed() instead of the previously used keyboard_check() function, because we only want to create 1 bullet for every key press. If we just used keyboard_check(), then we'd be creating a new bullet instance every step the key is held down (so 60 bullets in one second since our game is at 60FPS). We obviously don't want that and by using the _pressed function, the "if" will only evaluate to true once each time the key is pressed, and no more even if it's held down.
This code means that every time the player presses the spacebar , a bullet will be created on the layer named "Instances". A layer is simply a plane on which we place instances in the room editor, and if you look in the room editor you can see in the top left corner (by default) the list of layers in the room. You can add or remove layers here too, and each layer has a "depth" value associated with where the higher the depth, the "further" from the viewer it is and the lower the depth, the "closer" to the viewer, so that a layer at depth 100 will be drawn under a layer at depth -200.
We're not quite finished with that code yet though... When created, the bullet won't be moving and even if it was, it wouldn't know which direction to move in. So, to solve that, we need to tell it the direction to move using code.
The function instance_create_layer() returns an instance ID value, which is the unique ID for the instance that it created. We can use this ID to set values and properties on the instance, straight after it has been created, as shown in the following code:
if (keyboard_check_pressed(vk_space))
{
var inst = instance_create_layer(x, y, "Instances", obj_bullet);
inst.direction = image_angle;
}
What's going on here then? Well, we have create a local variable called "inst", and we're using it to store the returned instance ID from the create function. A local variable is a "use and throw away" variable, that will only exist for the duration of the script or the event that it was declared in. This is useful for data that we don't need to hang around (for more information on variables and variable scope, please see the manual).
We then use this variable to set the direction of movement for the instance we just created. When we use the ID value followed by a point"." like this, we are telling GameMaker Studio 2 that the "direction variable we are setting is in the instance stored in the "inst" variable, and not in the instance running the main code block. So, i this way we are setting the bullet direction to match the angle of rotation of the ship sprite.
The final thing we need to do is set the speed of the bullet. Now, you could do this in the player object using inst.speed = VALUE, as we did for the direction, but that's unnecessary as the speed for all bullets is always going to be the same, and generally accessing an instance this way is only used for values that are going to change (like the direction). So, we need to go back to the bullet object "obj_bullet" and add in a Create Event with this code:
speed = 6;
You can test the game now, and you should see that every time you press the keyboard spacebar down, a single bullet will be released, and you have to release and press the spacebar again to create more:
More Collisions
We need our bullets to destroy the asteroids, so to do that we need to open the object "obj_bullet" (if it's not already open) and add a Collision Event to it with the object "obj_asteroid":
The first thing we're going to do in this event is tell the bullet to destroy itself using instance_destroy():
instance_destroy();
You might think that this will prevent any further code from running after the function is called, but in GameMaker Studio 2 destroying an instance doesn't happen until the end of the event, so although we've called this function, it doesn't exit the event and the instance won't actually be removed from the room until the collision event is resolved.
What other code do we need? Well, we want the bullet to destroy the asteroids it hits, and we also want it to "split" the bigger asteroids into smaller ones. To do this we need to be able to access the instance ID of the asteroid that is being detected as colliding with the bullet, and for that we'll use the special keyword "other". This keyword, when used in the collision event, will reference the "other" instance in the collision, so in our game, the bullet is colliding with an asteroid, so "other" will reference the unique ID of the asteroid.
To use this keyword we'll do this:
with (other)
{
}
Here we introduce the with() statement. When you call "with (instance)" you are telling GameMaker Studio 2 that everything within the curly brackets {} afterwords should be run as if it was native to the instance. So, in this case, while the code is in the bullet object, it will be run as if it was in the asteroid object. This means that we can access variables and run functions on the asteroid in the collision.
Like the bullet, we also want the asteroid to destroy itself, so we fill in the code like this:
with (other)
{
instance_destroy();
}
Now, because we changed the scope of the code to the "other" instance in the collision (the asteroid instance), the instance_destroy() function will destroy the asteroid. We also want to "split" the asteroid based on what size the sprite is, so for that we'd add the following:
with (other)
{
instance_destroy();
if (sprite_index == spr_asteroid_huge)
{
repeat(2)
{
var new_asteroid = instance_create_layer(x, y, "Instances", obj_asteroid);
new_asteroid.sprite_index = spr_asteroid_med;
}
}
}
Here we're showing just the code for when the other asteroid in the collision is a "huge" asteroid. What it's doing is checking the sprite of the asteroid, and if it's a "huge" sprite, then it will create 2 new asteroids and assign them the "medium" sprite. Two things to note here: the first is the use of the repeat() statement, which will simply repeat the code contained within the {} that follow by the number of times given (in this case, 2), and the second thing to note is how we change the asteroid sprite index. You'll remember that we set it to a random sprite in the Create Event of the object, and here we're overwriting it it with a different value. This works because the moment an instance is created, its create event is run and then the code continues in the event that created the instance.
We can duplicate this code for the medium asteroid, so that it will look like this:
with (other)
{
instance_destroy();
if (sprite_index == spr_asteroid_huge)
{
repeat(2)
{
var new_asteroid = instance_create_layer(x, y, "Instances", obj_asteroid);
new_asteroid.sprite_index = spr_asteroid_med;
}
}
else if (sprite_index == spr_asteroid_med)
{
repeat(2)
{
var new_asteroid = instance_create_layer(x, y, "Instances", obj_asteroid);
new_asteroid.sprite_index = spr_asteroid_small;
}
}
}
We've used the "if.... else if..." code format for this. Using "else" is simply a way to have another set of code run when an "if" evaluates to false, and adding another "if" after the "else" we are saying:
if (this_evaluates_to_true)
then do this {}
else if (that_evaluates_to_true)
then do that {}
We could add another "else if" after that to check for a small asteroid sprite, but instead we'll do something slightly different...
Debris
We're going to add a "debris" effect into our game, and not just for the small asteroids, but for whenever any asteroid is destroyed. For that you need to create a new sprite, set it to be a 1x1 pixel only, then colour it white. This will be our debris sprite, so give it an appropriate name like "spr_debris", and then you can close the Sprite Editor, as we don't need to change its collision mask or do anything else.
Now we need to make a new object, called "obj_debris". Create it and name it now, and assign it the sprite you just created, "spr_debris". We'll give this object a Create Event where we'll use the following code to give it a random direction and some momentum:
direction = irandom_range(0, 359);
speed = 1;
We also want to give this object a Step Event, so do that now. In this event we'll make instances of the object fade out and then destroy themselves when they are no longer visible. To do this we'll be working with the image_alpha, which is a built in variable that controls the transparency (alpha) of the sprite assigned to the instance. A value of 1 is fully opaque and a value of 0 is fully transparent, and what we'll have our object do is gradually lower the image_alpha from 1 to 0 with this code:
image_alpha = image_alpha - 0.01;
if (image_alpha <= 0)
{
instance_destroy();
}
This will take a small amount off the image_alpha and when it gets equal-to or below zero, the instance destroys itself. Note that we don't do the check as "if (image_alpha == 0)"! Most numbers in GameMaker Studio 2 are floating point which means they can get minute rounding errors that can accumulate and cause issues with exact "==" checks. In the above case, it may be that the image_alpha value never reaches exactly zero and instead hits a number like 0.0000002, which would then roll over to be -0.0900002 and so never be exactly zero... which is why we check if it's less than or equal to 0. This may seem a bit contrary to common sense, but it's a fact of life when programming!
The final thing to do now is add some code to create these instances when the bullet hits the asteroids, and, just because we can, let's add them into the player object when it hits an asteroid and is destroyed too. So, open up the bullet object "obj_bullet" and in the Collision Event with "obj_asteroid", edit the code so it looks like this:
with (other)
{
instance_destroy();
if (sprite_index == spr_asteroid_huge)
{
repeat(2)
{
var new_asteroid = instance_create_layer(x, y, "Instances", obj_asteroid);
new_asteroid.sprite_index = spr_asteroid_med;
}
}
else if (sprite_index == spr_asteroid_med)
{
repeat(2)
{
var new_asteroid = instance_create_layer(x, y, "Instances", obj_asteroid);
new_asteroid.sprite_index = spr_asteroid_small;
}
}
repeat(10)
{
instance_create_layer(x, y, "Instances", obj_debris);
}
}
Then, open the player ship object "obj_ship" and in its Collision Event with "obj_asteroid", and add the following after instance_destroy():
repeat(10)
{
instance_create_layer(x, y, "Instances", obj_debris);
}
Run the game now, and shoot some asteroids! If all has gone well, then they should explode into smaller asteroids and create a nice puff of debris:
Clean Up
Before we finish this section of the tutorial, we need to do some cleaning up. In programming, there are many ways you can leave things lying around that will "clog-up" the computers memory and cause performance issues or worse. In general this kind of error is called a memory leak, and it's something you want to avoid at all costs in your own proects, meaning that you have to be careful to make sure that your game is programmed efficiently, and you don't leave things when no longer needed, but instead destroy them in some way.
In our game as it stands, we have a memory leak! Our room is only 500x500px, and we wrap our player and our asteroid instances if they go outside that area. But what about our bullets? They fly out the room space... and then what? Well, then nothing! Once outside the room, they are just taking up memory space without actually performing any useful task in our game, so we want to destroy them when they can no longer be seen.
We want to add an Outside Room event to our bullet object "obj_bullet", so do that now:
This event will only be triggered when the instance x/y position goes outside the room edges. In this event we'll simply add:
instance_destroy();
That's all we need to tell the instance that if it leaves the room, it should destroy itself. Memory leak averted!
Chapter 3: Controller Objects
In this chapter we're going to be creating lives and score to make our game more interesting, as well as a few more rooms to deal with different game "states".
To get started we're going to make a new object. This will be a "controller" object, which means that we won't be assigning it a sprite as it's going to sit in a room and deal with things "behind the scenes". So, make a new object now and call it "obj_game" and give it a Create Event:
We want this object to track the player's score and lives values, so we'll just use the built-in global variables score and lives. A global variable is basically a variable that has no "owner". It belongs to the entire game, and not one particular object, and can be accessed and changed by everything at anytime.
Add these lines into the Create Event:
score = 0;
lives = 3;
We want to show these values to the player too, so for that we'll add in a Draw Event:
As the name implies, this event will draw things to the screen. Like the Step Event, it will run every game frame, otherwise the things you drew would only be visible for one frame and then disappear. We want to use it to display the player score and lives, so we'll use the draw_text() function for this. Add the following code now:
draw_text(20, 20, "SCORE: " + string(score));
draw_text(20, 40, "LIVES: " + string(lives));
You'll notice we call the function string() on the global variables. This is because you can't add two values that are of different data types, and in this case we have a string "SCORE:" and a number (the value of the score global variable). To avoid this issue, we use the string() function to turn the value of score into a string data type - so if the score was 300, string(score) would return "300", and that can then be added onto the "SCORE: " string (adding strings like this, concatenates them).
We can now add this controller object into the room, so open up the room "rm_game" and drag an instance of this object up to the (0,0) position in the room (you'll see it is shown with a (?) symbol in the room editor - this is because we haven't assigned it a sprite):
Press play now and test the game! The score and lives values should be displayed in the top-left corner:
Fonts
Before we continue with the programming of the display we're going to add a new resource type to our game: a font resource, which is simply a collection of characters to use when drawing text. To create this, use the right mouse button on Fonts in the resource tree and select Create Font, which will open the Font Editor:
Call the font "fnt_text", and then select a font you like from the drop down Select Font menu. In the tutorial we'll use Consolas, and we will also switch off the anti-aliasing option to give a more pixelated and retro look to the font.
With that done, we need to tell GameMaker Studio 2 to use this font for the text, and for that you can call the function draw_set_font(). If your project uses multiple fonts, then you would need to call this function to set the font in the Draw Event before the lines you want to write using the different fonts, but in our small game we only want to use one font for all text, so we'll add it to the Create Event of the object "obj_game", like this:
draw_set_font(fnt_text);
After calling this function, all text in our game will be written using "fnt_text".
Setting Score and Lives
We can now look at updating the score and lives values as we play. For that, open up the object "obj_bullet" and go into the Collision Event with the object "obj_asteroid"
At the top of the code block, before everything else we're simply going to add in the following:
score += 10;
Since the score variable is global in scope, if we add to it from any instance then it will be updated for all instances, meaning that if we run the game now, we'd see the score value being drawn to the screen go up as we destroyed the asteroids. However, before testing this, let's deal with the player lives too.
Open the player ship object "obj_ship", and go to the Collision Event with the object "obj_asteroid":
Here we're going to deduct 1 from the lives, so go ahead and add the following code before the rest of the code in the event:
lives -= 1;
This will subtract 1 from the lives variable (and is the same as doing lives = lives - 1).
If you test the room now you'll see the score go up when you shoot the asteroids and if the player ship collides with an asteroid the lives will go down. There's still work to be done here, but we'll come back to it later after we've set up some more rooms.
Rooms
In order to add things like menu screens into our game we will create a few more rooms. Duplicate "rm_game" (use the right mouse button on the room and select Duplicate) and name this new room "rm_start". Open "rm_start" and delete everything apart from the "obj_game" instance:
You can hold down and then click and drag to select the instances to remove and then press to remove them.
This leaves an instance of our controller object "obj_game" in the room. We are now going to make this object persistent. Persistent objects are retained as you move from room to room; unlike regular objects, which are cleared from memory each time you leave a room. Note that a persistent object will therefore not trigger its Create or Destroy events when changing rooms, but it will trigger its Room Start and Room End events if it has them.
Open the object "obj_game" now and check the box marked Persistent:
We can go back and remove the instance of "obj_game" from the room "rm_game", as the instance created in the "rm_start" room will now persist into all subsequent rooms.
We also need to reorder the two rooms so "rm_start" is above "rm_game", as GameMaker Studio 2 will always start by loading the first room in the resource tree when your game is run:
Now we can add the rest of the rooms that our game requires. For that, duplicate the room "rm_start", rename the new room as "rm_win" and remove the instance of "obj_game" (so the room should have no instances in it).
We need one last room before we can go back to the programming, so duplicate the room "rm_win" and call it "rm_gameover". Your resource tree should now look like this:
Room Text
Open up the object "obj_game" (if it's not already) and go to the Draw Event. Since our object is persistent now, the code we have for drawing the score and lives will run in all the rooms the instance is persisted across and not just the main game room. Now, we could resolve this using a few "if... else if..." checks to see which room we are in and draw the text that's appropriate, but instead we'll use a switch() statement.
Using a switch() function we can check the room global variable, which holds the ID of the current room this instance is in, and add different cases for each of the possible values. In each case we can have the controller draw different things.
So, let's change the draw code to look like this:
switch (room)
{
case rm_game:
draw_text(20, 20, "SCORE: " + string(score));
draw_text(20, 40, "LIVES: " + string(lives));
break;
}
This code will only draw the text if the room ID is equal to "rm_game". Note that at the end of the case we add the keyword break. This is required to separate the different cases in the switch, and if you omit it then subsequent cases after the one that meets the switch condition will be run.
We'll set up the framework for the rest of the code now too, then go back and fill in the blanks, so continue to add cases into the Draw Event:
switch (room)
{
case rm_game:
draw_text(20, 20, "SCORE: " + string(score));
draw_text(20, 40, "LIVES: " + string(lives));
break;
case rm_start:
break;
case rm_win:
break;
case rm_gameover:
break;
}
We'll do the start room "rm_start" first, so in that case we want to add the following code:
var c = c_yellow;
draw_text_transformed_colour(room_width / 2, 100, "SPACE ROCKS", 3, 3, 0, c, c, c, c, 1);
This simply sets a local variable, c, to a colour constant which is then used in the draw_text_transformed_colour() to draw the game title scaled by 3 (see the manual for full information about the values this function takes).
Following that we add:
draw_text(room_width / 2, 200,
@"Score 1,000 points to win!
UP: move
LEFT/RIGHT: change direction
SPACE: shoot
>>PRESS ENTER TO START<<");
Note that we use the "@" to prefix the string we are wanting to draw, and we have the string split over multiple lines. Using "@" like this tells GameMaker Studio 2 that you are defining a string literal, which means that the string will be drawn with line breaks and other special characters, without the need for escape characters (see the section of the manual on Strings for more information).
Why not try running the game now and seeing how it looks?
That doesn't look quite right, does it? What's happened is that GameMaker Studio 2 has left justified all the text, so we need to tell it to center justify it using the function draw_set_halign() and set it to the constant fa_center. Our complete case should now look like this:
case rm_start:
draw_set_halign(fa_center);
var c = c_yellow;
draw_text_transformed_colour(room_width / 2, 100, "SPACE ROCKS", 3, 3, 0, c, c, c, c, 1);
draw_text(room_width / 2, 200,
@"Score 1,000 points to win!
UP: move
LEFT/RIGHT: change direction
SPACE: shoot
>>PRESS ENTER TO START<<");
draw_set_halign(fa_left);
break;
Notice that we call the function draw_set_halign() at the end of the case to reset the alignment for the text. If you test the game now, then the text should be in the center of the room and look much better:
The final thing to do now, is add in similar code for each of the other room cases, only changing the colour and position slightly to suit the different text. To start with we'll do the "rm_win" case, which should look like this:
case rm_win:
draw_set_halign(fa_center);
var c = c_lime;
draw_text_transformed_colour(room_width / 2, 200, "YOU WON!", 3, 3, 0, c, c, c, c, 1);
draw_text(room_width / 2, 300, "PRESS ENTER TO RESTART");
draw_set_halign(fa_left);
break;
And then for the room "rm_gameover" case:
case rm_gameover:
draw_set_halign(fa_center);
var c = c_red;
draw_text_transformed_colour(room_width / 2, 150, "GAME OVER", 3, 3, 0, c, c, c, c, 1);
draw_text(room_width / 2, 250, "FINAL SCORE: " + string(score));
draw_text(room_width / 2, 300, "PRESS ENTER TO RESTART");
draw_set_halign(fa_left);
break;
Game Control
Most of the text we've just added can't be seen, as we haven't actually coded anything into change between the different rooms. We'll do that now, starting with detecting the press of the key to start/restart the game, depending on the room the player is in.
In the object "obj_game", add a Step Event. This event will check for the keypress and then run a switch on the room variable to see what action should be taken using the following code:
if (keyboard_check_pressed(vk_enter))
{
switch(room)
{
case rm_start:
room_goto(rm_game);
break;
case rm_win:
case rm_gameover:
game_restart();
break;
}
}
Here we use two new functions which are pretty self-explanatory: room_goto() which will end the current room and then go to the room given as its argument, and then game_restart(), which takes no arguments and will simply restart the game again, as if the player was running it for the first time. Note how we have the two room cases together there for the win and gameover states. As was mentioned previously, omitting a break means that the case detected will run, and then subsequent cases will run too until the end of the switch or a break is met. Here we use this behavior to our advantage to detect two values and run a single code block.
We want to add in some more code now to detect the "win" and "lose" conditions, which in the case of our game is going to be 1000 points for the score to win, or 0 lives to lose. So, we'll want to first check that the current room is the game room (we don't want to perform these checks in any other room), and then we want to check the lives and score variables, like this:
if room == rm_game
{
if score >= 1000
{
room_goto(rm_win);
}
if lives <= 0
{
room_goto(rm_gameover);
}
}
We can quickly test this by opening up the Create Event of the "obj_game" and editing the score and lives to be 990 and 1 respectively:
And now if we test the game we will get the "Win" and "Game Over" screens depending on whether we shoot an asteroid or crash into it:
Chapter 4: Spawning Asteroids
In this final chapter we're going to be looking at making the game a bit more polished and interesting for the player.
To start with, we're going to change how the asteroids are created, so open the room "rm_game" and remove all the instances of the object "obj_asteroid":
You can remove an instance by clicking on it to select it and then using the key.
With that done, we go back to our controller "obj_game" and add a Room Start event:
This event will be run at the start of every room, so our persistent object will trigger this event each time a new room is entered. In this event we add the following code:
if (room == rm_game)
{
repeat(6)
{
var xx = choose(irandom_range(0, room_width * 0.3), irandom_range(room_width * 0.7, room_width));
var yy = choose(irandom_range(0, room_height * 0.3), irandom_range(room_height * 0.7, room_height));
instance_create_layer(xx, yy, "Instances", obj_asteroid);
}
alarm[0] = 60;
}
You should know at this point what each of the functions used here does individually, but together what they are doing is generating an x/y position within the room that is limited to only the corners of the room, as illustrated here:
This gives the player the best possible starting circumstances as there will be no asteroids created near them. We now need to continue to create asteroids as the player progresses and destroys them, otherwise there'll quickly be no asteroids left for them to shoot at, which is why we set the alarm instance variable. An alarm is an event that will be triggered some time after it is set, and it is set using the alarm variable. In this case we are setting the Alarm 0 Event to be triggered 60 steps after we set it.
Add the Alarm 0 Event to the object now:
In this event, we're going to spawn the asteroids not in the corner of the room, but at the boundaries of the room. This will make it a lot less obvious to the player when they are created. For this to work we need to choose either a random position along the x-axis and a value for y of either 0 or the room height, or a value of either 0 or the room width for x and a random value for y. The following code does just that, so add it into the Alarm 0 event:
if (choose(0,1) == 0)
{
var xx = choose(0, room_width);
var yy = irandom_range(0, room_height);
}
else
{
var xx = irandom_range(0, room_width);
var yy = choose(0, room_height);
}
We also need to add in the code to spawn the asteroid and also to reset the alarm so that it will lopp and continually create asteroids:
instance_create_layer(xx, yy, "instances", obj_asteroid);
alarm[0] = 4 * room_speed;
To set the alarm we have used the room_speed global variable. This variable holds the number of steps the room will perform in a second (the game speed), which is what we set right at the start of this tutorial: 60FPS. So, by setting the alarm to 4 * room_speed we are setting it to trigger again in 4 seconds.
There is one problem with this event, however... Because the object "obj_game" is persistent and the alarm is always reset, we would end up with asteroids in rooms other than the game room, since the alarm will be running even after the player has won or lost. To avoid this, add this code at the start of the code block, before the code given above:
if (room != rm_game)
{
exit;
}
Using "!=" in the above code is checking to see if something is not equal to the given value ("!" means "not"), so if the current room is not the game room, the rest of the event will be skipped (the exit statement will end the event that it is called in immediately, so any code after it will not be run).
If you run the game now and wait a few seconds you should see that asteroids are spawning constantly around the room edges.
Adding Sounds
The time has come to add sounds to our game, but before we get on with that, take a moment to reset the score and lives variables in the Create Event of "obj_game". These should be set to 0 and 3 respectively, as we no longer need them set to other values for testing:
We'll now need some sounds for our game to use...
The sounds can be *.wav, *.ogg or *.mp3 format and have a "retro" sound to them. There are a number of different programs available free online for making sound effects and music for you to make your own sounds with.
Once you have located the example sounds or created your own, we need to add them to our project. Create a Sound in the resource tree and this will open up the Sound Editor, ready for you to add your first sound:
The sounds we'll need are as follows:
- "msc_song" - Some kind of background music
- "snd_die" - A sound for the asteroid or the player exploding
- "snd_win" - A sound for the player winning
- "snd_lose" - A sound for the player losing
- "snd_zap" - A sound for the player shooting
Go ahead and create each of those sounds now (naming them as shown in the list above) and give them an appropriate sound to use. When you're finished your resource tree should look like this:
Playing Sounds
The first thing we'll do is add the music for the game when playing. For that, open the object "obj_game" and go to the Room Start Event. We want the music to play when we enter the room "rm_game", so we need to modify the code already in the event to look like this:
if (room == rm_game)
{
audio_play_sound(msc_song, 2, true);
repeat(6)
{
var xx = choose(irandom_range(0, room_width * 0.3), irandom_range(room_width * 0.7, room_width));
var yy = choose(irandom_range(0, room_height * 0.3), irandom_range(room_height * 0.7, room_height));
instance_create_layer(xx, yy, "Instances", obj_asteroid);
}
alarm[0] = 60;
}
Here we use the function audio_play_sound() to play the music resource that we added. We set the "priority" to 2 and also set the loop argument to true as we want the music to play constantly while the player is in the game room. Once you've added that we can then add in the win/lose sounds to this object. For that, go to the Step Event and modify the score and lives checks to look like this:
if room == rm_game
{
if score >= 1000
{
audio_play_sound(snd_win, 1, false);
room_goto(rm_win);
}
if lives <= 0
{
audio_play_sound(snd_lose, 1, false);
room_goto(rm_gameover);
}
}
Note that this time we set the priority argument to 1, as we want the sound effects to have less priority than the music, and we also set the loop argument to false, as we don't want these sounds to play more than once.
You need to go to the object "obj_ship" now, and open the Step Event. Here we want to modify the keyboard_check_pressed(vk_space) code block like this:
if (keyboard_check_pressed(vk_space))
{
audio_play_sound(snd_zap, 1, false);
var inst = instance_create_layer(x, y, "Instances", obj_bullet);
inst.direction = image_angle;
}
The player ship object also needs to have a sound for when it collides with an asteroid, so open the Collision Event with the object "obj_asteroid" and add this line to the top:
audio_play_sound(snd_die, 1, false);
We'll use the same sound in the object "obj_bullet" for when it hits an asteroid, so open that object too and in the Collision Event with the object "obj_asteroid" and add the same line:
audio_play_sound(snd_die, 1, false);
And that's it! You should run the game now and see how it sounds... it should feel a lot different playing!
Final Touches
Before we can call the game finished, there is one loose end that we need to fix up. Currently, when the player dies, a life is removed and nothing else happens. What we really want to happen is to have the room start again so the player can keep playing until the 3 lives are lost and the game ends. To achieve this we need to add another Alarm Event into the object "obj_game", and in that we'll restart the room, so that when the player dies there is a short pause, and then they can start to play again with a life less.
Open the object "obj_ship" now, and add an Alarm 1 event to it:
In this event we simply want to call the following code:
room_restart();
The room_restart() function does just what it says and restarts the room as if it had never been entered, so the player and asteroids are all created again and the player can keep playing.
To set this alarm, we need to open the object "obj_ship" again, and in the collision event with the object "obj_asteroid" add the following code:
with (obj_game)
{
alarm[1] = room_speed;
}
The last thing we are going to do is fix the music so it restarts when the room restarts too. As we have it now, we'll be playing the song again when the room is restarted, so we'll have two (out of sync) versions of the song playing. This is because sounds will not stop playing when a room is changed or restarted, so you must explicitly tell GameMaker Studio 2 to stop a sound if you don't want to hear it after a restart or change. To have our music restart and only play once, we need to open the Room Start Event of the object "obj_game", and add the following in just before the call to audio_play_sound():
if audio_is_playing(msc_song)
{
audio_stop_sound(msc_song);
}
All we're doing is checking to see if the sound "msc_song" is playing, and if it is then we stop it (the next line will restart it again).
Summary
With that, we come to the end of the "Space Rocks" tutorial! You can run the game now and test that all is working as it should, in which case you should have a start screen, hear music when the game starts, be able to shoot and destroy asteroids, and if you get hit by one the game should restart with one life less.
Congratulations, you've made your first game!
However, that's not the end of the story for "Space Rocks". We've set out the groundwork, but there is a lot here that you can build upon and make the game more advanced or more tailored to how you'd like it to be. Consider the following list of things that you could add to the game now:
- Give the game multiple levels.
- Have enemy ships that shoot at you.
- Maybe add in a boss fight?
- You could add in power ups, like shields or spreading bullets.
- Change the rate of spawn of the asteroids (produce more over time, perhaps?).
- Add some other kind of objective that isn't just getting a high score
You can add one or all of the above, or you can add anything else you can think of if you want! The important thing is to have fun and enjoy making games with GameMaker Studio 2.
Before we leave this tutorial, it's worth mentioning that if you are on any licence other than the Free licence, you can click the Compile Button to quickly make an executable file for testing or distributing to friends, etc...