Setting up an autoload for persistent data
Before we set up the Level scene to handle multiple levels, we'll make an Autoload function where we'll store the current level number since that would be a piece of data that we want to have persist when the scene changes. And we'll also want to save the player's progress, which in this case would be the last level that the player has reached. So this will be a good point to talk about saving data.
I've found the most convenient way to save data is to think about all the data that will need to be saved, and put all of that data in a variable of type Dictionary in an Autoload script. I haven't talked about the Dictionary data type yet, and there's a pretty good explanation of it in the Godot API reference at https://docs.godotengine.org/en/stable/classes/class_dictionary.html
But long story short, if you make a dictionary like
var my_dict:Dictionary
Then you can store key/value pairs which are very similar to having properties of a Node. You could say
my_dict.hp_max = 40.0
my_dict.name = "Sigfried"
my_dict.an_array = [12, 16, 27]
func pick_up_potion():
my_hp = my_dict.hp_max
func talk_to_npc():
speech_bubble.text = "Hello there, " + my_dict.name
However I more commonly set up initial values for a Dictionary using curly brace notation
var my_dict:Dictionary = {
"hp_max": 40.0,
"name": "Sigfried",
"an_array": [12, 16, 27],
}
And instead of using a dot followed by key name to access a Dictionary element, I usually use bracket syntax.
func pick_up_heart_container():
my_dict["hp_max"] += 5.0
Because I occasionally find it useful to have a variable with the string of the key that I want to look for, and you can do this
var key_name:String = "hp_max"
print("HP max is: ", my_dict[key_name])
But this won't work
print("HP max is: ", my_dict.key_name)
It's nice to store all the data that needs to be saved in a single Dictionary because the saving and loading process will involve serialization and deserialization, which is a fancy way of saying "converting a complex object like a Dictionary into a String and converting it back again". If you can serialize and deserialize all the data that needs to be saved or loaded in one command, then that makes things easier. And since I do this in practically every game I make, I wrote an Autoload script and use it in all my projects where the only thing I change is the structure of the Dictionary named "data" that will be set up to carry all the relevant data for that game. Save this as res://scripts/save_handler.gd.
extends Node
var key = "3p0ch_all_the_kings_men"
var data = {
"last_level_unlocked": 0,
}
var level:int = 0
func _ready():
pause_mode = Node.PAUSE_MODE_PROCESS
func read_save():
if OS.has_feature('JavaScript'):
var JSONstr = JavaScript.eval("window.localStorage.getItem('" + key + "');")
if (JSONstr):
return parse_json(JSONstr)
else:
return null
else:
var file = File.new()
if not file.file_exists("user://" + key + ".save"):
return null
file.open("user://" + key + ".save", File.READ)
return parse_json(file.get_line())
func write_save():
if OS.has_feature('JavaScript'):
JavaScript.eval("window.localStorage.setItem(\"" + key + "\", \'" + to_json(data) + "\');")
else:
var file = File.new()
file.open("user://" + key + ".save", File.WRITE)
file.store_line(to_json(data))
file.close()
func open_site(url):
if OS.has_feature('JavaScript'):
JavaScript.eval("window.open(\"" + url + "\");")
else:
print("Could not open site " + url + " without an HTML5 build")
func switchToSite(url):
if OS.has_feature('JavaScript'):
JavaScript.eval("window.open(\"" + url + "\", \"_parent\");")
else:
print("Could not switch to site " + url + " without an HTML5 build")
The variable named data is the Dictionary of data that we want to save, which in this case is just the last level that the player reached, but in your own games could be much more complicated and include any permanent upgrades the player collected or number of battleships they amassed in their fleet. It can even include an array or even another dictionary if you want. The variable named level is outside of the data Dictionary, it's a standalone variable for the particular run that the player is playing that we'll use to keep track of what level they're on. We don't want to save it to disk because if the player plays through then game once and has their progress saved, and then decides to start a new game from the beginning, then we don't need to unlock any more levels. But we will want to keep track of where the player is in their current run by using a variable in an Autoload that won't disappear when the scene changes, so we'll keep the current level number in this Autoload but not in the data Dictionary that will be saved.
The _ready() function is worth talking about even though it's not really necessary for this game because I didn't implement a Pause feature. If you want your game to be pauseable, then you'll probably go to the top menu bar in Godot and choose Project – Project Settings – pick the Input Map tab, and make a new action called pause and assign a key like P to it. Then in one of your scripts if you have a command like this
if Input.is_action_just_pressed("pause"):
get_tree().paused = not get_tree().paused
Then it looks like the SceneTree will toggle its property called paused whenever you press the pause button. But there's a problem – when the game is paused the nodes will stop running their scripts, so when the player tries to press P again to unpause this script won't be running and the game won't restart.
When you pause the game, you want to stop things like the player and enemies but you want to have other things that keep running, such as a script that will handle unpausing the game. Nodes have a property that you can set in the Inspector at the bottom called PauseMode, which normally is "Inherit" which means it will do whatever its parent node does, but can be set to "Process" if you want that node to keep running its scripts even when the SceneTree is paused (which is what you would want to do if you make a full pause screen pop up), or can be set to Stop if you want to explicitly tell that node to stop running its code when the SceneTree is paused. That code above to toggle the pause state should work fine if you put it in the script of a node where PauseMode is set to Process.
In this case, the save_handler.gd script is something that I want to keep running even when the game is paused, especially if a pause menu has a button like "Save Game" or if we want to save any options that are set while the player is in the pause screen. So I put that in its _ready() function and have left it there since.
The other functions of read_save(), write_save(), open_site(), and switch_to_site() pretty much do what they say, but I'll point out a few things. The read_save() function doesn't just take the saved data and put it directly into the data variable, it passes it as a return value for the function which will be null if no saved data was successfully read (so you can use that as a condition in an "if" statement and it will evaluate to true only if some data was actually read). That allows you to do things in your code like
data_from_disk = SaveHandler.read_save()
if not data_from_disk:
continue_button.disabled = true
if player_clicked_continue_button:
SaveHandler.data = data_from_disk
start_the_game()
elif player_clicked_new_game_button:
# Just start with the default data in SaveHandler.data and don't assign data_from_disk
start_the_game()
I'll also point out that the functions use
if OS.has_feature('JavaScript'):
Which will check if the game is being run from a platform that supports JavaScript (e.g. an HTML5 export that's being played from a browser) and allows you to run different code in a web build than when you're testing things out in the Godot editor. In this case, for web builds I want to save the data to the browser's LocalStorage under a key that I set at the top to be 3p0ch_all_the_kings_men. This gets a little complicated and outside the scope of what you need to know to make Godot games so don't worry too much about it if you're not familiar with it, just be aware that you should change that key at the very top to be something unique for your game, such as your_user_name_your_game_name.
After you've changed the var key = "" line to be something unique for your game, save the script and add it to the list of Autoloads like you did earlier: go to Project – Project Settings – Autoload tab, pick save_handler.gd, and click the Add button. Now all of your other scripts should be able to read and write to
SaveHandler.level
SaveHandler.data["last_level_unlocked"]
(or alternatively SaveHandler.data.last_level_unlocked, but like I said I prefer bracket notation)
And run the methods
SaveHandler.read_save()
SaveHandler.write_save()
Making things change with different levels
Now that we have a place to keep track of the current level, we can start setting things up to make the difficulty progress and have more "allies" around the further you get into the game. Just like I mentioned with Enemies, there are a couple of ways of doing that with Levels.
1) Make a completely different scene for each level in the game. The different scenes might all have the same script attached to them if they all use the same logic and check for the same win or lose conditions and the only difference between them is the level layout.
2) Make one scene and just change the number of enemies that get spawned depending on what level we're on, which is what we'll do in this game.
3) Use class inheritance which I talked about with the Enemies earlier but still haven't shown you yet – I promise I'll get around to it though. This might be useful if you want to write some code that every level will use, similar to how our current Level code handles pausing and making the scene fade out after a win or lose condition has been met, but have custom code for each level so they can have different win or lose conditions like killing all the enemies in some levels or reaching a goal in other levels.
The level scene
We're going with approach #2, so open the Level scene and get ready for a moderate redesign because we're going to take a different approach to adding enemies and allies to the scene. If you have enemies and archers and mages and hounds added to the hierarchy, delete them all and just leave the Ground, Borders, Player, and HUD with its children. Then add the following exported variables which will hold references to the scenes for the enemies, archers, mages, and hounds that we'll instance.
extends YSort
export var enemy_scene:PackedScene
export var archer_scene:PackedScene
export var mage_scene:PackedScene
export var hound_scene:PackedScene
Save the scene so those variables appear in the Inspector when you click on the root Level node, and drag the appropriate .tscn files into those variables. I should mention that it is possible to do this
export var enemy_scene:PackedScene = preload("res://enemy/enemy.tscn")
But the problem is that if you ever decide to change the game's file structure or scene names so the scene you want is no longer at res://enemy/enemy.tscn then you'll have to go back and fix that, whereas if you make an exported variable and drag the scene to it in the Inspector then Godot will keep track of things even if you move files around, so I usually do it that way.
After you've added the scenes to the variables in the Inspector, here's the code for the level with all of the changes that we'll make up to the _ready() function (we won't mess with any of the other functions, so leave your current physics_process and lose functions as they are).
extends YSort
export var enemy_scene:PackedScene
export var archer_scene:PackedScene
export var mage_scene:PackedScene
export var hound_scene:PackedScene
var level_enemies:Dictionary = {
0: {"enemy": 5, "archer": 0, "mage": 0, "hound": 0},
1: {"enemy": 5, "archer": 2, "mage": 0, "hound": 0},
2: {"enemy": 5, "archer": 6, "mage": 0, "hound": 0},
3: {"enemy": 5, "archer": 2, "mage": 2, "hound": 0},
4: {"enemy": 5, "archer": 2, "mage": 4, "hound": 0},
5: {"enemy": 5, "archer": 2, "mage": 2, "hound": 1},
6: {"enemy": 5, "archer": 4, "mage": 4, "hound": 1},
}
var level_won:bool = false
var level_lost:bool = false
var pause_until_fade:float = 2.0
var fade_duration_max:float = 1.0
var fade_duration:float = 0.0
var enemy_instance:KinematicBody2D
onready var outcome_node = get_node("HUD/Outcome")
onready var overlay_node = get_node("HUD/Overlay")
func _ready():
for i in level_enemies[SaveHandler.level]["enemy"]:
enemy_instance = enemy_scene.instance()
add_child(enemy_instance)
enemy_instance.position.x = 1050.0 + 100.0 * i
enemy_instance.position.y = rand_range(100.0, 550.0)
for enemy in get_tree().get_nodes_in_group("enemy"):
enemy.connect("reached_end", self, "lose")
for i in level_enemies[SaveHandler.level]["archer"]:
add_child(archer_scene.instance())
for i in level_enemies[SaveHandler.level]["mage"]:
add_child(mage_scene.instance())
for i in level_enemies[SaveHandler.level]["hound"]:
add_child(hound_scene.instance())
get_node("Player").connect("died", self, "lose")
We added a variable called level_enemies which is a Dictionary, and it's a Dictionary of Dictionaries so is an example of a relatively complex data structure that we'll walk through. A Dictionary is a set of key / value pairs, and in this case the first entry in the level_enemies dictionary has a key of 0 and the value is {"enemy": 5, "archer": 0, "mage": 0, "hound": 0}. When you see things like that enclosed in curly brackets, that tells you you're looking at a Dictionary, so the value in level_enemies[0] is that Dictionary which four elements – for enemy, archer, mage, and hound – which are the numbers of each thing that should be present in the level. And you can access those values by typing level_enemies[0]["enemy"] and that would give you the value of 5 which is the number of enemies in level 0. And level_enemies[1]["archer"] is 2 which is the number of archers in level 1.
And now the _ready() function has "for" loops that will make the appropriate number of instances of enemies, archers, mages, and hounds. If the line setting up each "for" loop seems difficult to decipher, let's look at the one for Enemies.
for i in level_enemies[SaveHandler.level]["enemy"]:
Since SaveHandler.level has an integer telling you the current level, then if the player were say on level 2 that would be the same as
for i in level_enemies[2]["enemy"]:
And that should make sense now that we discussed it in the previous paragraph. Since level_enemies[2]["enemy"] is 5, that's the same as
for i in 5:
Which will run the loop while having the variable i take value 0 for the first iteration, then 1, then 2, then 3, and then 4 (so the loop runs 5 times). In the loop for the enemies, we make an instance of the Enemy scene and add it to the SceneTree by adding it as a child of Level, and then we position it with an x coordinate of 1050 + 100 * i (which spreads out the enemies – remember that for the first enemy i=0, for the second i=1, etc.) and a random y coordinate. I didn't worry about giving the archers or mages a position like that since they reposition themselves randomly anyway, and I just left the hound with its default position of (0, 0) instead of bothering to give it a random position.
If you playtest it now it should run level 0 all right and spawn five enemies, but you'll notice that the enemies are either all slimes or all goblins and not a mix. Remember that when we were placing enemies in the Level manually we were clicking a box to set its is_goblin variable to true or false, but now that the enemies are being spawned from code we can't do that. But we can make the Enemy code randomly select whether to be a slime or a goblin when it spawns. Go to the Enemy code and add this line where you're declaring variables with parameters
var odds_of_being_a_goblin:float = 0.3
Then erase the export var is_goblin:bool line (since we don't want it to be an exported variable any more) and add this
var is_goblin:bool = false
And add this line to the _ready() function
if randf() < odds_of_being_a_goblin:
is_goblin = true
The randf() function is a built-in function in Godot that generates a random floating point number from 0 to 1. And now if you playtest it you should (probably) get some slimes and some goblins. While we're in the Enemy script, you might notice during playtesting that if enemies are coming in from the right edge of the screen and you're impatient about pouncing on them and slashing them, then you can knock them back off the right edge of the screen and have to wait for them to come back into play. I didn't like that, and wanted to make the enemies stay in play after they entered and not get knocked back out. It might be possible to do that by messing with those invisible borders in the Level scene, but I've had enough of dealing with the necromancy of collision layers and masks so instead I added this to the end of _physics_process() and for the tutorial it's worth making the point that just because you know one way of solving a problem doesn't mean you need to or should stick with the same approach all the time.
if movement_vector.x > 0.0 and global_position.x > 1024.0:
global_position.x = 1024.0
So Level number 0 works, and if you want to test out the other levels you could go to the SaveHandler script and change the value of the variable named level there, but we'll need to make the game increment the level when you win and if applicable increment the last_level_unlocked and save your progress. Fortunately that's pretty easy, just make this modification to the Level script.
func _physics_process(delta):
if (not level_lost) and (not level_won):
if (get_tree().get_nodes_in_group("enemy").size() < 1):
level_won = true
outcome_node.visible = true
SaveHandler.level += 1
if SaveHandler.level > SaveHandler.data["last_level_unlocked"]:
SaveHandler.data["last_level_unlocked"] = SaveHandler.level
SaveHandler.write_save()
else:
if pause_until_fade > 0.0:
pause_until_fade -= delta
else:
fade_duration += delta
if fade_duration < fade_duration_max:
overlay_node.modulate.a = fade_duration / fade_duration_max
else:
get_tree().change_scene("res://story/story.tscn")
The story scene
We'll also do a similar thing to the Story scene so it shows appropriate text depending on which level the player is currently on. Here's the updated code.
extends Node2D
var level_text:Array = [
"Sir Knight, the goblins of the East are laying seige.\n\n" + \
"Pray thee, vanquish the vile invaders lest they reach our farmlands.\n\n" + \
"Move with WASD / arrows\nand use thy mouse to smite them!",
"Alas, more fiends are upon us. But have no fear, thou needst not fight alone. I shall dispatch the archers to aid you!",
"Yet more goblins approach.\n\n" + \
"Thou shalt have the aid of all of our archers!",
"They continue to press on our borders.\n\n" + \
"Mages, gather at once to assist!",
"Their ranks still swell...\n\n" + \
"Muster every mage in the wizard tower for our defense!",
"Even against every man in the kingdom, they continue their assault.\n\n" + \
"If men aren't enough, so be it. Release the hounds!",
"The last of the enemy approaches!\n\n" + \
"Everyone in the kingdom who still draws breath, throw all that you have at them!",
"Thank you for your service, sir Knight.\n\n" + \
"But everyone in the kingdom was fighting, so I guess you didn't really need to do much yourself now did you?\n\n" + \
"Well, give them your thanks for helping and bugger off now.",
]
var button_text:Array = [
" A'ight ",
" A'ight ",
" Um, it's really not necessary m'lord... ",
" I hope they're better than the archers ",
" I really can handle this on my own, sire ",
" Oh for the love of... ",
" Something something shadow of death ",
]
onready var label_node = get_node("Label")
onready var button_node = get_node("CenterContainer/Button")
func _ready():
label_node.text = level_text[SaveHandler.level]
if SaveHandler.level < button_text.size():
button_node.text = button_text[SaveHandler.level]
button_node.grab_focus()
else:
button_node.visible = false
button_node.connect("pressed", self, "start_level")
func start_level():
get_tree().change_scene("res://level/level.tscn")
It sets up an array named level_text that has the text for each level. It might look a little strange, but there are actually seven values in the array (for levels 0 through 6) plus another value to be shown after the player has finished all the levels, even though the block for that array takes up a lot of lines. The \ character at the end of a line tells Godot that the next line should be considered part of the same line as the previous line but you're breaking them up for clarity to make all the code fit on your screen, and the combination of + \ at the end of the line does that while the + will concatenate the two strings from that line and the next line. The commas will actually separate the different elements in the array. The button_text array is easier to read and is essentially the same thing. In _ready() it sets the label to show the level_text for the current level (the number in SaveHandler.level) which might be 7 if the player has finished the last level. So when it tries to set the text for the button, first it sees if SaveHandler.level is less than the size of the button_text array: if so then it sets the button text appropriately, and if SaveHandler.level isn't less than the size of the button_text array (remember that if the last element is index number 6 then the size of the array is 7) then it makes the button_node invisible. You might wonder: why is the "if" statement checking whether SaveHandler.level < button_text.size() instead of simply saying if SaveHandler.level < 7? The answer is that while I was programming the game I wasn't sure how many levels I was going to make, so writing it that way would make it work even if the number of levels changes. You might also have noticed the button_node.grab_focus() line; it allows the player to just press Enter or Space to quickly restart the level since the button will have focus for keyboard input.
Main menu node hierarchy and dealing with containers
Now that we're saving progress, we'll need to have a Main Menu where you can choose New Game or Continue, and if you continue then have a level selection. Make a new folder of res://main and a new scene with a root node of type Sprite and name the root node Main, and from my assets get title.png for the Sprite. We're about to work with Container nodes, and I will give you fair warning that even if you follow this tutorial and read all the Godot docs you can find on UI design, in practice they will probably mess you up good. Don't consider yourself any less of a programmer if they do. And if you just plan to make web games where you can manually control the size and position of everything and don't need to worry about having things resize properly if people play it with different size viewports and different aspect ratios, then you're free to not use them at all. But for this tutorial we'll do some simple ones.
When the game starts we'll want to show the New Game and Continue buttons, and have some text saying this is for Brackeys Game Jam. And if the player chooses Continue, we'll want to make that go away and have a level selection come up. So we'll make one child of Main that has New Game and Continue and another child of Main that has the Level Section and we'll adjust which one of them is visible based on what the player does.
First give the Main node a child VBoxContainer node and name it NewContinue. Then give NewContinue a child HBoxContainer node and it can keep its the default name. Give the HBoxContainer node two children that are each Button nodes with one named New and one named Continue, and give the NewContinue node another child that's a Label node with the following text (including an Enter so it's on two lines).
Made in a week for Brackeys Game Jam
Jam theme: "You're not alone"
Since you've set up the Story scene already, you should be able to set up the Button nodes and Label node to look how you want and to respond when the mouse is initially clicked instead of waiting for a click and release (if that's how you want it to work), and I'm mostly going to focus on how the Container nodes (VBox and HBox) behave. The HBox container with the two Button children will keep them aligned in a row with the first child in the hierarchy on the left and the second child in the hierarchy on the right. Try going to one of the buttons nodes and manually changing its Control-Rect-Position so it's no longer nicely aligned in the row and then run the scene using that icon in the top right of the arrow in the director's thingy. How do you like that? Godot will let you know who the boss is, and now you're starting to see why container nodes can make a novice pull their hair out.
With Container nodes, you wont be controlling things like you're used to any more. You can set the position and size of the outermost Container node but it will control the placement of all of its children. In this case set the NewContinue node's Control-Rect-Position to be (440, 425). Set the HBoxContainer's Alignment to be Center and its Control-ThemeOverrides-Constants-Separation to be 100. You can have a little bit of control by changing the child nodes' properties, and for the Button nodes you could try changing their SizeFlags (under Control) and seeing what happens. If your scene looks like mine in the actual game, then adjusting the Horizontal flags and having them on in different combinations will have the most noticeable effects.
Once those look like they're set up nicely, you can hide the NewContinue node (click they eye icon in the hierarchy) and give Main a new child of type HBoxContainer and name it Levels. Give the Levels node a child Label node with the text Level, and another child node of type Button named 0 with text 0. After you set the Control-ThemeOverrides-Font on the 0 node (and its Colors if desired), you can copy/paste nodes in the hierarchy, so copy the 0 node and paste it to the Levels node to give it another Button child, and keep doing that until you have Button nodes for levels 0 through 6 and adjust their Text appropriately. In this case I thought it looked nicest if I set the Levels node's Control-Rect-Position to (440, 425) and its MinSize-x to 550, Control-ThemeOverrides-Constants-Separation to 20, and turned on each of the children's Control-SizeFlags-Horizontal Fill and Expand flags. When you're done, make the Levels node be hidden and show the NewContinue node again.
Main menu code
When you're done, save the scene as res://main/main.tscn and add this code.
extends Sprite
func _ready():
randomize()
if SaveHandler.read_save():
SaveHandler.data = SaveHandler.read_save()
if SaveHandler.data["last_level_unlocked"] == 0:
get_node("NewContinue/HBoxContainer/Continue").disabled = true
get_node("NewContinue/HBoxContainer/New").connect("pressed", self, "new_game")
get_node("NewContinue/HBoxContainer/Continue").connect("pressed", self, "continue_game")
func new_game():
SaveHandler.level = 0
get_tree().change_scene("res://story/story.tscn")
func continue_game():
for level in 7:
get_node("Levels/" + str(level)).disabled = (level > SaveHandler.data["last_level_unlocked"])
get_node("Levels/" + str(level)).connect("pressed", self, "start_at_level", [level])
get_node("NewContinue").visible = false
get_node("Levels").visible = true
func start_at_level(level):
SaveHandler.level = level
get_tree().change_scene("res://story/story.tscn")
The randomize() function should be run once when the game starts because if you don't use it then when Godot generates random numbers it will always generate the same series of random numbers each time you run the game, but after it executes that randomize() function it will start making its random numbers randomly. You only need to run it once, so the _ready() function of the main scene is a good place for it.
Next it checks for saved data. I wrote the SaveHandler.read_save() function so it will return null if there's no save data present, so that first "if" statement will only execute the next line if there was data saved (meaning that SaveHandler.read_save() is not null since logic statements like "if" consider null to be false). If there's not any save data found, then SaveHandler.data will remain the default value that we set it to when we made the save_handler script. Then if the player hasn't unlocked any levels after level 0 it will make the Continue button be disabled.
It connects the Continue button's "pressed" event to continue_game, and let's look at that function closely. It runs a "for" loop that will make the variable named level take on each value from 0 to 6. Then it has get_node() statements that might look weird. Normally you would expect to see something like get_node("Levels/0") to get the button for level 0, but here we have the expression get_node("Levels/" + str(level)). The str(level) will give you the level number in String format, and the plus symbol when used with strings will concatenate them or join them together. So the first time it goes through the loop with level = 0 that will be get_node("Levels/" + str(0)) which is get_node("Levels/0"), and the second time through the loop it will be get_node("Levels/1"), etc, so this loop will end up acting on each of the level buttons. That's one slick way to do it; another way to loop through nodes would be if you have a parent_node and want to do something to each of its children in a loop, you could say
for child_node in parent_node.get_children():
# Do something with child_node
But in this case I want to have the variable named level with the level number to make things easier for the next steps. First, the loop will go through all the level buttons and make them disabled if they're above the last_level_unlocked, and will connect each level button to the start_at_level() function. Next it will connect the buttons' "pressed" signals to start_at_level().
This is also an example of making a signal pass a value when it runs a function. In this case, we're going to write a single start_at_level() script that will start at a different level depending on which of the level buttons have been pressed, so the start_at_level() script will need to know which level to start at. Some signals will automatically pass parameters when they're connected to functions – for example when we were making the player's swipe work by connecting an on_body_entered signal to the swipe, we connected it to a function and that function took a parameter which was a reference to the body that entered. For the "pressed" signal, Godot doesn't automatically pass any parameters like that. But you can tell it to pass parameters to the function that the signal is connected to by adding a fourth parameter in the .connected() command, and that fourth parameter should be an Array. In this case we made the fourth parameter in the .connect() be [level], so the start_at_level function will receive a parameter with the number for the level that we want to start.
In the start_at_level() function itself, it sets the value of the SaveHandler script's level variable to be whichever level number the button passed to it when it called the function, and then goes to the Story scene which will read that level number from SaveHandler and show the appropriate story for that level.
Now that we have a proper Main scene, you can go to Project – Project Settings – General tab – Application – Run and make that be the Main scene that runs when the game starts. And we're getting there, this is starting to look almost like a finished game! Except there's still no sound, so let's handle that next.