We’re finally ready to build our main scene!
Congratulations, you’ve come a long way.
You’ve managed to go through an incredible amount of material, write a ton of code, and understand many concepts! With this final step, you’ll have a small but playable game that you’ll be able to build upon.
The main scene will do something basic: it will just place all the rooms in a grid and spawn the pickups and mobs.
Piece of cake!
This chapter will explore how to spawn rooms in a grid.
The Main scene will use multiple nodes, which we have prepared for you.
Create a new scene, and pick a
as the root node. Call it Main.res://props/sky/BackgroundBlueSky.tscn
. That scene
is the parallax background. We provide it already set for you.res://interface/OnScreenUI.tscn
: this has the health,
score, and current weapon indicator.res://interface/Pause.tscn
: the pause screen.res://music/MusicPlayer.tscn
: A background music
player that uses random tracks from an array.Attach a script to the top node, and save the scene.
Let’s think about what we want to do.
We want to spawn a certain number of rooms from an array and place them, as we did in Random Rocks.
We want to set up each room in a different way:
The variables we will need:
# The list of possible rooms to spawn
export(Array, PackedScene) var rooms := []
# The number of rooms on the x-axis
export var grid_width := 2
# The number of rooms on the y-axis
export var grid_height := 2
# The size of each individual room. It's assumed that all the rooms in the
# rooms array have that size.
export var room_size := Vector2(13, 13) * 128
You might be wondering about Vector2(13, 13) * 128
. What
is that?
If you count the tiles of one room, there are 13
tiles
from one side to the end of the bridge. That’s where the next room will
be appended.
Yes! That means the room’s bridges will overlap.
That is not an issue, and the player will not see it. As for
128
, it’s the tile size.
So one room’s size is 13
tiles times 128
pixels. We could write it as:
export var room_size := Vector2(1664, 1664)
But by leaving it written the way we did, we can more easily remember what it means and change it if we make bigger or smaller rooms.
Now that we have all the variables we need let’s write the function that generates a level.
Write:
func generate_level() -> void:
What should this function do? Let’s write some pseudo-code. Pseudo-code is used to write something that looks like code but isn’t functional. That can help us reflect on what we need or what we’re missing.
Here’s the ideal code we would like to write:
for each (x,y) in the grid:
room_size * (x,y)
.0
(so the room is on the left side of the grid):
hide the left bridge.
0
(so the room is at the top of the
grid): hide the top bridge.
You have all the tools to write this code, even if writing this entire algorithm might seem daunting.
The following steps will all help you along the way, with increasing levels of information until the complete solution at the end. Don’t hesitate to peek ahead, and then try on your own. If you don’t manage, you can try reading a bit more and try again. Don’t be discouraged if you can’t do it at all! It doesn’t matter. The act of attempting it is a form of practice, and is already progress by itself.
Let’s observe the steps we outlined, one by one. Try to think about what information we do not have.
We know how to spawn random scenes from an array. That is no issue. But we have things like “if the room is the last room.” How do we know the total amount of rooms? How do we know which number is the current room?
It’s easy to know how many cells are in a grid: multiply the height
by the width. If a grid is 10x10
, then it has
100
cells. If a grid is 4x4
, then it has
16
cells, and so on.
The last index is the size of the grid, minus 1
.
Therefore, we can write:
var last_room_index := (grid_width * grid_height) - 1
And to count the rooms, we’ll have an index we increment for every room:
var current_room_index := 0
How do we write for each (x,y) in the grid
in GDScript?
We cannot loop over two values in one loop. We need a nested loop. We
should write:
for x in grid_width:
for y in grid_height:
var room_position := Vector2(x, y)
# ... all the code will be between those two lines
+= 1 current_room_index
Be sure to keep current_room_index += 1
at the bottom of
the loop! We don’t want to increment the index before we’re done.
And with that, we resolved all the unknowns. All that’s left is to transform the pseudo-code into actual code!
The following is code we’ve written multiple times. We will pick a random room from the array, instance it, and add it to the scene.
var RoomScene: PackedScene = rooms[randi() % rooms.size()]
var room: BaseRoom = RoomScene.instance()
= room_size * room_position
room.global_position add_child(room)
At this point, rooms are generated and set in a grid. Now, we need to check the following:
Try to write this code! How do you know which room is the first? How do you know the last?
Here’s how the series of if
statements looks:
if current_room_index == 0:
pass
# In the last room, we don't spawn items not to overcrowd it.
elif current_room_index == last_room_index:
pass
# For all other rooms, we spawn both items and mobs.
else:
pass
We wrote all the spawn_...()
functions before in the
BaseRoom.gd
file, so we should be able to create all the
necessary nodes without too much trouble.
If you’re stuck, here’s the entire series of lines:
if current_room_index == 0:
spawn_robot()
room.spawn_items()
room.elif current_room_index == last_room_index:
spawn_teleporter()
room.spawn_mobs()
room.else:
spawn_mobs()
room.spawn_items() room.
Finally, we need to hide the relevant bridges depending on where the room is.
0
, we need to hide the left bridge.grid_width - 1
, we need to hide the right
bridge.0
, we need to hide the top bridge.grid_height -1
, we need to hide the bottom
bridge.Try on your own! If you don’t manage, scroll down, we provide the complete solution below.
The code for doing this is:
if x == 0:
hide_left_bridge()
room.elif x == grid_width - 1:
hide_right_bridge()
room.if y == 0:
hide_top_bridge()
room.elif y == grid_height - 1:
hide_bottom_bridge() room.
And with this, we finished the generate_level()
function. If you managed on your own, amazing! If you didn’t, no
worries! Sometime in the future, when you’ve progressed, come back and
try again. Repetition is critical, don’t hesitate to redo the same
exercises many times.
The rest of what we have to do is just some menial tasks: hide the pause screen, run the music player, and run the level generation function we wrote.
Reference the nodes we’ll need:
onready var _pause_screen := $UILayer/PauseScreen
onready var _music_player := $MusicPlayer
And then finally, write the _ready()
:
func _ready() -> void:
randomize() # without this, we would get the same random numbers on each run
_pause_screen.hide()
_music_player.play()
generate_level()
Whew! We’re almost there! Go back to the scene view, and add a few
rooms to the rooms
array:
You are… ready. Run the game with F5.
Godot will ask you for the main scene for your game. Select the current scene, and you’re off to the races.
Have fun. Feel free to add as many rooms, weapons, and mobs as you have the patience to write.
Challenge: How would you abstract away the level generation so you can create levels of varying difficulty? Would you use Level scenes? A resource? A mix of both? Try implementing this!
We’re almost there! We only need to wire the user interface, and we’ll have a complete game. Hang in there!