14.creating-the-main-scene

Creating the main scene

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 scene structure

The Main scene will use multiple nodes, which we have prepared for you.

Create a new scene, and pick a YSort as the root node. Call it Main.

  1. Drag res://props/sky/BackgroundBlueSky.tscn. That scene is the parallax background. We provide it already set for you.
  2. Add a CanvasLayer node and call it UILayer. Drag into it:
    1. res://interface/OnScreenUI.tscn: this has the health, score, and current weapon indicator.
    2. res://interface/Pause.tscn: the pause screen.
  3. Drag 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.

Main scene code: planning

Let’s think about what we want to do.

  1. We want to spawn a certain number of rooms from an array and place them, as we did in Random Rocks.

  2. We want to set up each room in a different way:

    1. The first room should not spawn enemies, but it should spawn the robot.
    2. The last room should spawn the teleporter.
    3. Rooms on the border of the grid should hide the bridge that leads nowhere.

The variables we will need:

  1. The array of rooms: an exported array of scenes, like we used in the spawner.
  2. The size of the grid: determines how many rooms we spawn on the x and y axes.
  3. The room size. We need this to know how far from each other each room should be.
# 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:

  1. Spawn a room.
  2. Set the room’s position at room_size * (x,y).
  3. We decide what to spawn. If:
    1. The room is the first room: spawn the robot and pickups.
    2. Else, if the room is the last room, spawn the teleporter and mobs.
    3. Else, spawn pickups and mobs.
  4. We also need to hide the bridges. So, if:
    1. x is 0 (so the room is on the left side of the grid): hide the left bridge.
      1. But if x is the last x (so the room is on the right side of the grid): hide the right bridge.
    2. Also, if y is 0 (so the room is at the top of the grid): hide the top bridge.
      1. But, if y is the last y (so the room is at the bottom of the grid): hide the bottom 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.

What information are we missing?

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
        current_room_index += 1

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!

Setting up rooms

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.global_position = room_size * room_position
add_child(room)

At this point, rooms are generated and set in a grid. Now, we need to check the following:

  1. If this is the first room, spawn a robot and pickups.
  2. Else, if this is the last room, spawn the teleporter and mob.
  3. Else, spawn pickups and mobs.

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:
    room.spawn_robot()
    room.spawn_items()
elif current_room_index == last_room_index:
    room.spawn_teleporter()
    room.spawn_mobs()
else:
    room.spawn_mobs()
    room.spawn_items()

Finally, we need to hide the relevant bridges depending on where the room is.

  1. If x is 0, we need to hide the left bridge.
  2. If x is grid_width - 1, we need to hide the right bridge.
  3. If y is 0, we need to hide the top bridge.
  4. If y is 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.

In this grid, the first square should have both top and left bridge hidden

The code for doing this is:

if x == 0:
    room.hide_left_bridge()
elif x == grid_width - 1:
    room.hide_right_bridge()
if y == 0:
    room.hide_top_bridge()
elif y == grid_height - 1:
    room.hide_bottom_bridge()

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.

Wrapping up

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!