In this tutorial, we will:

  • create the start menu – here, you can find the menu items to start a new a game, load the saved one, and quit the game;
  • develop the save and load functionality (to keep things simple, we’ll only have one save slot);
  • edit the in-game menu and the game over screen to bring us back to the start screen.

Creating the start menu scene

The following image shows the final result we will get. It’s very similar to the in-game menu we built in Part 16: Pause menu. Restart and quit game:

Graphically, it’s a very basic menu – feel free to add your game logo and any graphics you want.

To start, let’s create a new scene for the start menu by clicking on Scene → New Scene. Then, in the Scene panel, click the 2D Scene button to create a Node2D root node. Rename it StartScreen.

Add a ColorRect node to StartScreen and resize it to 320×180 pixels: it will be our menu background. Change its Color property to black (0,0,0,255).

Now, add another ColorRect to StartScreen and rename it NewGame. Set Rect → Position to (70, 50) and Rect → Size to (180, 20). Now add a Label to NewGame. In the Custom Fonts property load Font.tres (you’ll find it in the Fonts folder) and in Custom Colors set Font Color to black. Write NEW GAME in the Text property of the label, then set both Align and Valign to Center. Finally, set the Rect → Size of the label to (180, 20).

Now, duplicate NewGame two times. Rename one copy to LoadGame and the other to Quit.

Set LoadGame‘s Rect → Position to (70, 80), then change its label’s text to LOAD GAME. Select the Quit node and set its Rect → Position to (70, 110). Finally, change its label’s text to QUIT.

Finally, let’s add some background music to our menu. Add an AudioStreamPlayer node to StartScreen and in the Stream property load the night-chip.ogg file (you can find it in the Sounds folder). Enable the Autoplay property to play music when the game starts.

You’ll end up with this node structure for the StartScreen scene:

Save the scene in the Scenes folder and call it StartScreen.tscn.

Start menu script

Attach a new script to the StartScreen node, call it StartScreen.gd and save it in the GUI folder. Copy this code inside the script:

extends Node2D

var selected_menu = 0

func change_menu_color():
	$NewGame.color = Color.gray
	$LoadGame.color = Color.gray
	$Quit.color = Color.gray
	
	match selected_menu:
		0:
			$NewGame.color = Color.greenyellow
		1:
			$LoadGame.color = Color.greenyellow
		2:
			$Quit.color = Color.greenyellow

func _ready():
	change_menu_color()

func _input(event):
	if Input.is_action_just_pressed("ui_down"):
		selected_menu = (selected_menu + 1) % 3;
		change_menu_color()
	elif Input.is_action_just_pressed("ui_up"):
		if selected_menu > 0:
			selected_menu = selected_menu - 1
		else:
			selected_menu = 2
		change_menu_color()
	elif Input.is_action_just_pressed("attack"):
		match selected_menu:
			0:
				# New game
				get_tree().change_scene("res://Scenes/Main.tscn")
			1:
				# Load game
				var next_level_resource = load("res://Scenes/Main.tscn");
				var next_level = next_level_resource.instance()
				next_level.load_saved_game = true
				get_tree().root.call_deferred("add_child", next_level)
				queue_free()
			2:
				# Quit game
				get_tree().quit()

The inner workings of the menu are the same as the in-game menu that we built in Part 16. Let’s take a closer look at what happens when we select one of the menu items.

When we select NEW GAME, the script calls the change_scene() function of the current SceneTree to load the main scene of the game.

Instead, when we select LOAD GAME, we use a different method to load the main scene:

  • First, we use the load() statement to load the scene in memory as a PackedScene resource.
  • The scene is now in memory, but it’s not yet a node. To create the actual node, we must call the instance() function of PackedScene.
  • Now that the scene is an actual node, we set the load_saved_game variable to true. This variable (which we will declare shortly) will be used by the Root node of the main scene to decide whether to load the data from the saved game or not.
  • At this point, we add the newly created node to the current scene. We must call add_child() via call_deferred() because we have to wait for the node to be completely initialized (i.e., all child nodes loaded).
  • Finally, we tell Godot to remove the start menu from the node tree by calling the queue_free() function.

The last menu item is QUIT. To exit the game, we simply call the quit() function of the current SceneTree.

As seen above, the script we just created refers to the load_saved_game variable that doesn’t exist yet. To declare it, go to the Main.tscn scene and attach a script to the Root node. Call it SaveLoadGame.gd and put it in a new folder named Scripts. For now, simply enter this code to declare the load_saved_game variable and to add a new function called save (we’ll implement it later when we save the game):

var load_saved_game = false

func save():
	pass

Now click the Project → Project Settings menu and go to the Application → Run section. Change the Main Scene setting to run the StartScreen.tscn scene at game start. Run the game to test the menu.

In-game menu

Before seeing how to save the game, we need to update the in-game menu to add the save and the return to start menu items. In the main scene, go to the MenuPopup node. Rename the Restart node to SaveGame and the Quit node to MainMenu, and then change the text of their labels to SAVE GAME and MAIN MENU. Then, open the MenuPopup.gd script and in the change_menu_color() function replace any reference to $Restart with $SaveGame, and those to $Quit with $MainMenu:

func change_menu_color():
	$Resume.color = Color.gray
	$SaveGame.color = Color.gray
	$MainMenu.color = Color.gray
	
	match selected_menu:
		0:
			$Resume.color = Color.greenyellow
		1:
			$SaveGame.color = Color.greenyellow
		2:
			$MainMenu.color = Color.greenyellow

Now, in the _input() function, we need to replace the code of the updated menu items (index 1 and 2). The new code is as follows:

...

1:
	# Save Game
	get_node("/root/Root").save()
	get_tree().paused = false
	hide()
2:
	# Back to start screen
	get_node("/root/Root").queue_free()
	get_tree().change_scene("res://Scenes/StartScreen.tscn")
	get_tree().paused = false

The code for the save game menu item calls the save() function of the Root node than cancel the pause and hide the in-game menu. To return to the main menu, we free the current scene, loads the start screen scene, and cancels the pause.

Save file data

Saving a game is the action of creating or updating a file that contains all the progress made by the players. This allows them to interrupt the game and resume it later, or even to retry the game several times in case of defeat, restarting from a previous moment in time.

To save the game, we need to:

  1. identify all the information necessary to rebuild the current state of the game;
  2. choose the most effective method to store this information.

Let’s start from point 1. The entities that change during the game, and therefore must be stored, are the following:

  • The player: to rebuild its state, we must save its position, its health, its mana, the experience points, the level, and its inventory.
  • Fiona: to store the state of Fiona, it’s enough to know the status of her quest and if her necklace has been found or not.
  • The skeletons: we have to store a list of skeletons, saving each one’s position and health.
  • Pickable objects: at the beginning of the game, on the map there are two potions that the player can pick up; we have to store whether they have already been taken or not.

As for point 2, I decided to save the state of the game in JSON format. JSON (JavaScript Object Notation) is a data format that uses human-readable text to store data in attribute-value pairs.

A significant advantage of using JSON to save data is the remarkable resemblance to Godot’s dictionaries. A dictionary is a data type that contains values referenced by unique keys (so it is composed of pairs of keys and values like JSON). You can define a dictionary by placing a comma-separated list of key: value pairs in curly braces {}, as we will see shortly.

In Godot, some very convenient functions can be used to convert dictionaries directly into JSON strings and vice versa. Saving the game requires building a dictionary that contains information of all the entities in the game and then converting it to JSON format and saving it as a file.

The most convenient way to build this dictionary is for each object to be able to create a “translation” of itself as a dictionary. This dictionary can, in turn, be used as a value within another dictionary, thus creating a tree structure that can be converted into a single JSON file. So, we will provide each node that needs to be saved with a to_dictionary() function that will handle this translation.

Listed below are all the scripts to be modified and their respective functions to be added:

Player.gd

func to_dictionary():
	return {
		"position" : [position.x, position.y],
		"health" : health,
		"health_max" : health_max,
		"mana" : mana,
		"mana_max" : mana_max,
		"xp" : xp,
		"xp_next_level" : xp_next_level,
		"level" : level,
		"health_potions" : health_potions,
		"mana_potions" : mana_potions
	}

The variable position is a Vector2, which is a Godot type that cannot be converted directly to JSON. So, we will store it as an array containing 2 elements (the x and y coordinates).

Fiona.gd

func to_dictionary():
	return {
		"quest_status" : quest_status,
		"necklace_found" : necklace_found
	}

Skeleton.gd

func to_dictionary():
	return {
		"position" : [position.x, position.y],
		"health" : health
	}

SkeletonSpawner.gd

func to_dictionary():
	var skeletons = []
	for node in get_children():
		if node.name.find("Skeleton") >= 0:
			skeletons.append(node.to_dictionary())
	return skeletons

This function returns an array of dictionaries, each containing the data of a skeleton.

The last piece of information we want to save is if the two potions present at the beginning of the game have already been taken or not. In this case, we don’t need to create the function to_dictionary(), but we will simply check that the potions are present or not in the scene tree. But to distinguish these potions from those instantiated later, we must first rename them respectively to StartPotion1 and StartPotion2.

At this point, we are ready to write the code that actually saves the game.

Saving the game

Let’s go to the SaveLoadGame.gd script and edit the save() function in this way:

func save():
	var data = {
		"player" : $Player.to_dictionary(),
		"fiona" : $Fiona.to_dictionary(),
		"skeletons" : $SkeletonSpawner.to_dictionary(),
		"potion1" : is_instance_valid(get_node("/root/Root/StartPotion1")),
		"potion2" : is_instance_valid(get_node("/root/Root/StartPotion2"))
	}
	
	var file = File.new()
	file.open("user://savegame.json", File.WRITE)
	var json = to_json(data)
	file.store_line(json)
	file.close()

First, the function creates a dictionary called data, which contains some properties whose values are the dictionaries generated by the to_dictionary() functions we saw earlier.

For the two potions, we use the is_instance_valid() function to find out if they are still valid nodes (and therefore still present in the game).

To save the file, we first create a new object of type File. Then we call the open() method, which requires two parameters: the path to the file to be opened and the opening mode (in this case, we want to write to the file).

The file path uses the special path user://, which represents, in a platform-independent way, the location available to the user to save files. The actual location of this folder depends on the operating system; for example, in Windows, it’s %APPDATA%\Godot\app_userdata\Project-Name, while in Linux is $HOME/.godot/app_userdata/Project-Name.

The next line uses the to_json() function to convert the given dictionary into a string, which is saved in the json variable. Then the file store_line() function is called to save the string inside the file.

Finally, we call the close() function to close the file and end the operation.

If you want to know more about how to use all the file system features in Godot, you can consult my Essential Guide to Godot Filesystem API.

Below you can see an example of saved game in JSON format. I formatted the JSON file for reading convenience; it is actually saved as a single line.

{
   "fiona":{
      "necklace_found":true,
      "quest_status":2
   },
   "player":{
      "health":100,
      "health_max":100,
      "health_potions":5,
      "level":3,
      "mana":104.221565,
      "mana_max":200,
      "mana_potions":3,
      "position":[
         256.913422,
         90.034714
      ],
      "xp":250,
      "xp_next_level":400
   },
   "potion1":false,
   "potion2":false,
   "skeletons":[
      {
         "health":100,
         "position":[
            43.824348,
            353.437714
         ]
      },
      {
         "health":100,
         "position":[
            346.069458,
            453.682861
         ]
      },
      {
         "health":100,
         "position":[
            300.874878,
            483.28363
         ]
      },
      {
         "health":100,
         "position":[
            621.517029,
            735.957458
         ]
      },
      {
         "health":100,
         "position":[
            579.305176,
            753.475708
         ]
      },
      {
         "health":100,
         "position":[
            55.37022,
            306.615692
         ]
      },
      {
         "health":100,
         "position":[
            614.484924,
            237.107681
         ]
      },
      {
         "health":100,
         "position":[
            331.521759,
            776.349609
         ]
      },
      {
         "health":100,
         "position":[
            685.134583,
            241.07724
         ]
      },
      {
         "health":100,
         "position":[
            754.878662,
            417.91452
         ]
      },
      {
         "health":100,
         "position":[
            323.712311,
            456.731567
         ]
      },
      {
         "health":100,
         "position":[
            691.614624,
            501.519104
         ]
      },
      {
         "health":100,
         "position":[
            747.091858,
            733.033936
         ]
      },
      {
         "health":100,
         "position":[
            189.58287,
            764.28302
         ]
      },
      {
         "health":100,
         "position":[
            641.547668,
            570.008911
         ]
      }
   ]
}

Loading the game from save file

To load the game from the save file, we’ll have to do the opposite procedure to that seen previously, converting the JSON file into a dictionary, and using the latter to restore the values of the various nodes. To do this, we will create the from_dictionary() functions for each node that needs to be loaded.

Listed below are all the scripts to be modified and their respective functions to be added:

Player.gd

func from_dictionary(data):
	position = Vector2(data.position[0], data.position[1])
	health = data.health
	health_max = data.health_max
	mana = data.mana
	mana_max = data.mana_max
	xp = data.xp
	xp_next_level = data.xp_next_level
	level = data.level
	health_potions = data.health_potions
	mana_potions = data.mana_potions

Fiona.gd

func from_dictionary(data):
	necklace_found = data.necklace_found
	quest_status = int(data.quest_status)

Skeleton.gd

func from_dictionary(data):
	position = Vector2(data.position[0], data.position[1])
	health = data.health

SkeletonSpawner.gd

func from_dictionary(data):
	skeleton_count = data.size()
	for skeleton_data in data:
		var skeleton = skeleton_scene.instance()
		skeleton.from_dictionary(skeleton_data)
		add_child(skeleton)
		skeleton.get_node("Timer").start()
		
		# Connect Skeleton's death signal to the spawner
		skeleton.connect("death", self, "_on_Skeleton_death")

When loading the game from a save file, skeletons do not have to be instantiated in the _ready() function like when starting a new game. So, in SkeletonSpawner.gd, you have to change the _ready() function like this:

...

# Create skeletons
if not get_parent().load_saved_game:
	for i in range(start_skeletons):
		instance_skeleton()
	skeleton_count = start_skeletons

...

The actual loading of the game will take place in the _ready() function of SaveLoadGame.gd. Open the script and add the function:

func _ready():
	var file = File.new()
	if load_saved_game and file.file_exists("user://savegame.json"):
		file.open("user://savegame.json", File.READ)
		var data = parse_json(file.get_as_text())
		file.close()
		
		$Player.from_dictionary(data.player)
		$Fiona.from_dictionary(data.fiona)
		$SkeletonSpawner.from_dictionary(data.skeletons)
		if($Fiona.necklace_found):
			$Necklace.queue_free()
		if(not data.potion1):
			$StartPotion1.queue_free()
		if(not data.potion2):
			$StartPotion2.queue_free()

To load the game, the save file is opened for reading, the JSON string is read with the get_as_text() method and converted into a dictionary using the parse_json function. Next, we use the from_dictionary() functions to rebuild the state of the various nodes. Finally, we check some dictionary values to decide whether to remove the potions and Fiona’s necklace from the scene tree.

And with this, we have completed the saving and loading of the game!

Game Over screen

he last thing to do is to change the Game Over screen to take us back to the start screen when we press the Escape key.

Find the GameOver node and temporarily set its Modulate property to (255,255,255,255) to make it visible. Duplicate its label and move it down, then change the text to PRESS ESC TO RETURN TO MAIN MENU and center it horizontally.

Set Modulate to (255,255,255,0) to hide it again.

Now open the MenuPopop.gd script and edit the _input() function like this:

func _input(event):
	if not visible:
		if Input.is_action_just_pressed("menu"):
			# If player is dead, go to start screen
			if player.health <= 0:
				get_node("/root/Root").queue_free()
				get_tree().change_scene("res://Scenes/StartScreen.tscn")
				get_tree().paused = false
				return
			# Pause game
			already_paused = get_tree().paused
			get_tree().paused = true
			# Reset the popup
			selected_menu = 0
			change_menu_color()
			# Show popup
			player.set_process_input(false)
			popup()

			...

In this way, when the player is dead, pressing the Escape key will bring us back to the start screen.

Conclusions

In this tutorial we learned:

  • how to switch from one scene to another, using both change_scene() and load();
  • how to prepare data for saving, using dictionaries and JSON format;
  • how to save and load data to and from the file system, using the File class, to_json(), and parse_json().

You can try what we’ve done so far by clicking here:

Did you enjoy this article? Then consider buying me a coffee! Your support will help me cover site expenses and pay writers to create new blog content.


6 Comments

Avatar

Dzandaa · August 2, 2020 at 2:20 pm

in:
func from_dictionary(data):
position = Vector2(data.position[0], data.position[1])
health = data.health

I think forg0t to include the position in skeleton to_dictionary():

func to_dictionary():
return {
“position” : [position.x, position.y],
“health” : health,
“health_max” : health_max
}

    Avatar

    Davide Pesce · August 2, 2020 at 6:06 pm

    Thank you, I lost a piece in the copy-paste!

Avatar

Phigiro · August 2, 2020 at 3:58 pm

😀 😀 😀 😀 😀 😀

thank you !!!!

Avatar

Phigiro · August 5, 2020 at 6:16 pm

Hi Davide!

Could it be, that there is another error in Fiona.gd?

Her Quest-Status wasn´t taken over.
I think that depends on that we have to cast data.quest_status into int like int(data.quest_status).

that way:

func from_dictionary(data):
necklace_found = data.necklace_found
var quest_status_number = int(data.quest_status)
match quest_status_number:
0:
print(“not started”)
quest_status = QuestStatus.NOT_STARTED
1:
print(“started”)
quest_status = QuestStatus.STARTED
2:
print(“complete”)
quest_status = QuestStatus.COMPLETED

after that the status was taken over after loading.

    Avatar

    Davide Pesce · August 5, 2020 at 6:44 pm

    Hi Phigiro! You’re right, there is a problem in Fiona’s script. In fact it can be solved even more simply:

    func from_dictionary(data):
    	necklace_found = data.necklace_found
    	quest_status = int(data.quest_status)

    I’d swear I wrote it in this way at first and it didn’t work… 🤷‍♂️

Avatar

Ken · October 7, 2020 at 8:19 am

This is my comment to thank you very much for this tutorial series. I finally completed this series and learned really valuable skills.
I will dig into your site for more Godot related articles.
Again, thank you very much.

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.

Close