Congratulations on making it to the end of our 2D RPG series! It might have taken you a long to get here, but you persisted and hopefully by now you have a working game and you understand all of the concepts that we went over throughout this tutorial. With our game created, you need to go back and test it to make sure that it is as bug-free as possible.
WHAT YOU WILL LEARN IN THIS PART:
· How to install Export Templates.
· How to export projects as Windows Executables.
· How to test and debug your game
For this, you need to delve into the world of gameplay testing. Since this is a small-scale game, we’ll focus on the aspects of manual testing, which includes testing the game’s mechanics by playing it and trying to break it. Please note, that everybody’s testing approach is different, and the following section just contains basic guidelines.
Here’s a general guideline for manual testing:
Gameplay Mechanics: This involves testing all of the game’s mechanics to ensure they work correctly. In our game, the mechanics to test would include moving, shooting, interacting, damaging entities, consuming pickups, and the ability to progress and end the game. The interaction of the player’s character with enemie should also be tested. Are they colliding properly? Do the animations play correctly? Make sure to separate each factor of the game into a checklist which you can then test individually.
Levels: Each level should be tested thoroughly. This includes testing the map generation to make sure that the tiles are generating correctly. Spawn locations of entities and other obstacles should also be tested to see if they are not out of bounds/unreachable or spawning beyond the game map.
User Interface (UI) and Controls: You should test that the game’s controls work correctly and that the UI displays the right information. For example, you might check that the stamina and health progress bar regenerates correctly.
Difficulty and Progression: Check that the game gets harder as you progress through the levels. For example, the payer’s health increase each time the level progresses — but the enemies health remains the same. Make sure that this progression feels fair and balanced.
Audio and Visuals: Test the game’s sound effects, music, and graphics. This would involve testing things like the sounds of picking up items, shooting, and taking damage, as well as the animations for these actions.
Performance: Check the game’s performance. It should run smoothly without lagging or stuttering, even when there are lots of entities on the screen.
Bugs and Glitches: Play the game while actively trying to cause bugs and glitches. This might involve things like trying to move into walls, pausing and unpausing the game, or trying to interact with objects in unexpected ways.
Edge Cases: Test unusual or extreme situations. For example, what happens if the player doesn’t move at all? What if they try to run in place whilst shooting?
Player Experience: Lastly, test the overall player experience. Is the game fun to play? Are there any frustrating parts? This will be subjective, so it can be useful to get multiple people to playtest the game.
You can use the debugger to see which functions or methods are returning errors. My debug console returned a lot of warnings (yellow errors) which won’t affect my game in any way but instead are giving us suggestions for code optimizations. It also returned a lot of red errors, which might affect our game. Let’s go ahead and fix these.
This error will occur whenever our game tries to update our health and stamina via the signals. Just like the error above, this is due to a mismatch in argument numbers. When we emit the ‘health_updated’ signal in the Player.gd script, we are passing in an argument. Then, in the Health.gd script, the ‘update_health’ method is connected to this signal, and is expecting to receive this argument. However, our method ‘update_health’ is defined without any parameters, hence the mismatch and the error.
To fix this, we just need to emit both our x and max_x variables in our health and stamina signals.
### Player.gd # ----------------- Level & XP ------------------------------ #updates player xp func update_xp(value): xp += value #check if player leveled up after reaching xp requirements if xp >= xp_requirements: #stop background music background_music.stop() level_up_music.play() #allows input set_process_input(true) Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) #pause the game get_tree().paused = true #make popup visible level_popup.visible = true #reset xp to 0 xp = 0 #increase the level and xp requirements level += 1 xp_requirements *= 2 #update their max health and stamina max_health += 10 max_stamina += 10 #give the player some ammo and pickups ammo_pickup += 10 health_pickup += 5 stamina_pickup += 3 #update signals for Label values health_updated.emit(health, max_health) stamina_updated.emit(stamina, max_stamina) ammo_pickups_updated.emit(ammo_pickup) health_pickups_updated.emit(health_pickup) stamina_pickups_updated.emit(stamina_pickup) xp_updated.emit(xp) level_updated.emit(level)
If you load your game, this error might occur. This error typically occurs when we’re trying to set a current scene in Godot, and the scene we’re trying to set already has a parent node, which is not the scene tree root. When changing scenes, we usually want to add the new scene as a child of the root node and then set it as the current scene. However, if the scene already has a parent that isn’t the root, Godot won’t allow us to set it as the current scene, causing this error.
We can fix this via the call_deferred object, which will allow us to set our current scene and change our scene when we load our game both in the same frame, avoiding the error we’ve encountered.
### Global.gd func load_game(): if loading and FileAccess.file_exists(save_path): #older code # Change to the loaded scene get_tree().root.call_deferred("add_child", game) get_tree().call_deferred("set_current_scene", game) current_scene_name = game.name #older code
If you load your game, this error might occur. This error is happening because we’re trying to access the “Player” node from the scene “MainScene”. It seems like the “ MainScene “ scene does not have a “Player” node.
In our load_data() function, we’re trying to find the “Player” node from the current_scene, which in this case is “MainMenu”. Here is the problematic line:
var player = current_scene.get_node("Player")
If there’s no “Player” node in the “MainMenu” scene, we’ll get a “Node not found” error. To avoid this error, we can first check if the “Player” node exists in the current_scene before trying to access it:
### Global.gd #player data to load when changing scenes func load_data(): var current_scene = get_tree().get_current_scene() if current_scene and FileAccess.file_exists(save_path): print("Save file found!") var file = FileAccess.open(save_path, FileAccess.READ) var data = JSON.parse_string(file.get_as_text()) file.close() # Now you can load data into the nodes if current_scene.has_node("Player"): var player = current_scene.get_node("Player") if player and data.has("player"): player.values_to_load(data["player"]) else: print("Save file not found!")
This error happens when we’re trying to change a scene while some physics queries are still being resolved. Godot doesn’t allow direct change in some physics properties or change scenes during a physics process as it could lead to crashes or unexpected behavior.
One solution is to use the call_deferred() function which schedules the method to be called during the idle time of the next frame. This can prevent race conditions that may arise if physics queries are being flushed while we’re trying to change the scene.
# Change scene func change_scene(scene_path): save() # Get the current scene current_scene_name = scene_path.get_file().get_basename() var current_scene = get_tree().get_root().get_child(get_tree().get_root().get_child_count() - 1) # Free it for the new scene current_scene.queue_free() # Change the scene var new_scene = load(scene_path).instantiate() get_tree().get_root().call_deferred("add_child", new_scene) get_tree().call_deferred("set_current_scene", new_scene) call_deferred("post_scene_change_initialization") func post_scene_change_initialization(): load_data() scene_changed.emit()
With these issues, the red errors from our log should be removed. The remaining yellow warnings are of no concern, as removing some of the “shadowed” variables will result in us receiving actual errors. You can however go ahead and fix the warnings that say variable x is never used by adding the indentations to the unused parameters.
After testing, I realized that I want our level progression to be more fair. Each time we level up, the enemies’ health should also increase — making them harder to kill.
We can do this by increasing our enemy’s health in their ready function. We can then use the player’s level to adjust the enemy’s health. For example, for each level the player gains, the enemy’s health could increase by a certain percentage or a fixed amount.
### Enemy.gd func _ready(): rng.randomize() # Reset color animation_sprite.modulate = Color(1,1,1,1) # Adjust enemy health based on player's level health += player.level * 10 # Increase health by 10 for each player level max_health = health print("Enemy health:" , health)
Now if we level up, our enemy’s health should increase by 10.
If the player levels up, we can also increase the max amount of enemies that can spawn. To do this, we need to define a new signal that we will emit when the player levels up.
### Player.gd # Custom signals signal health_updated signal stamina_updated signal ammo_pickups_updated signal health_pickups_updated signal stamina_pickups_updated signal xp_updated signal level_updated signal xp_requirements_updated signal coins_updated signal leveled_up # ----------------- Level & XP ------------------------------ #updates player xp func update_xp(value): xp += value #check if player leveled up after reaching xp requirements if xp >= xp_requirements: # older code #update signals for Label values health_updated.emit(health, max_health) stamina_updated.emit(stamina, max_stamina) ammo_pickups_updated.emit(ammo_pickup) health_pickups_updated.emit(health_pickup) stamina_pickups_updated.emit(stamina_pickup) xp_updated.emit(xp) level_updated.emit(level) leveled_up.emit()
Then, in our EnemySpawner, we will create a new function that will update our max_enemies + the current level if the signal is emitted.
###EnemySpawner.gd extends Node2D # Node refs @onready var player = get_tree().root.get_node("%s/Player" % Global.current_scene_name) @onready var spawned_enemies = $SpawnedEnemies @onready var tilemap = get_tree().root.get_node("%s/Map" % Global.current_scene_name) # Audio nodes @onready var death_sfx = $GameMusic/DeathMusic # Enemy stats @export var max_enemies = 9 var enemy_count = 0 var rng = RandomNumberGenerator.new() # Inside the _ready() function func _ready(): player.leveled_up.connect(_on_player_leveled_up) # The function that adjusts max_enemies based on player's level func _on_player_leveled_up(): max_enemies += player.level * 1 print("Max enemies adjusted to:", max_enemies)
With these changes, the EnemySpawner will spawn more enemies and at a faster rate as the player levels up and the enemies’ health will increase, making the game progressively more challenging!
I also want our game to have the functionality to autosave. We can do this with the help of a Timer node. We want our game to autosave every 5 minutes. In your Main and Main_2 scenes, add a new Timer node and set it’s wait-time to 300 (300 seconds = 5 minutes). Also, ensure that you enable autostart on load.
Connect your timer node’s timeout() signal to your Main script. In this function, we will simply just call our Global.save() function. Every five minutes, the timer will timeout, and save our game.
### Main_2.gd extends Node2D @onready var background_music = $Player/GameMusic/BackgroundMusic #connect signal to function func _ready(): background_music.stream = load("res://Assets/FX/Music/Free Retro SFX by @inertsongs/Imposter Syndrome (theme).wav") background_music.play() # Change scene func _on_trigger_area_body_entered(body): if body.is_in_group("player"): Global.change_scene("res://Scenes/Main.tscn") Global.scene_changed.connect(_on_scene_changed) #only after scene has been changed, do we free our resource func _on_scene_changed(): queue_free() # Autosave func _on_timer_timeout(): Global.save() print("Game saved.")
### Main.gd extends Node2D # Change scene func _on_trigger_area_body_entered(body): if body.is_in_group("player"): Global.change_scene("res://Scenes/Main_2.tscn") Global.scene_changed.connect(_on_scene_changed) #only after scene has been changed, do we free our resource func _on_scene_changed(): queue_free() # Autosave func _on_timer_timeout(): Global.save() print("Game saved.")
Now after five minutes have passed, your game should autosave!
The final source code for this part should look like this.
If you’ve gotten to the point in your game where you have fixed all of your major bugs and you have smoothened out your gameplay, then it might be time for you to export your game so that others can enjoy it as well! We’re going to be exporting our project for PC today. When we export our project, it will be compiled into an executable (.exe) program that we can launch at the click of a button!
Before we can export our project, we need to choose an export template. These templates will compile our binary files into a program file for the platform that we choose.
To choose a project template, we need to open our Export menu via our Project > Export property.
This will open the Export window, and by default, you should have no export preset available to you because you do not have an export template installed.
We can add new presets by clicking on the “Add” option next to our Presets menu. This will open a dropdown to which we have to choose the platform that we want to export our project to. We want to export our project as a Windows Desktop application, so select that preset option.
This will display a bunch of text that is written in red. This is because we still do not have our project template installed. Let’s install one by clicking on “Manage Export Templates”.
From here you can download the most recent export template available to you. Click “Download and Install” to download the most recent one for your Godot version, or you can download one from the Godot website and install it from the file.
Once it’s finished installing, you can close the window and go back to your Exports window. The red warning messages will now be gone. In this window, you can change your game’s export name, save the path, and you can even set a password to it — amongst other things.
In your resources menu, you can even set which assets/resources you want to export with the project. For this project, we will export all of our resources. You can read more about the properties in the Export window here.
The default properties should be fine for our project — just change its name and save location. When you’ve added all your properties, you can click on “Export All” to export your executable program to your designated save location.
Now you can navigate to where you exported your project, and voila, your game runs! When you’ve created your own game and you are confident that you’ve created a smooth, engaging, and bug-free game, you’ll probably export your project so that it can be hosted on online marketplaces such as Steam, GOTM.io or itch.io.
Congratulations, you’ve made a game in Godot 4 from start to finish. Hopefully, you’ve learned a lot on this journey, but you’ll only learn as much as you allow yourself to. A good thing to remember is that you’ll only really get good at Godot and game development in general if you practice.
Do as many **tutorials **as possible on game development in Godot for beginners until you get bored. What I like to do, or what works for me when I’m stuck in this phase is that instead of following an entire course, I consume articles or videos that focus on singular topics. I’ll list some links to other tutorials at the end of this part.
Practicing implementing simple mechanics that you’ve already learned to make. Let’s take our player’s movement code as an example. Try and implement it by yourself, and if you struggle, look back at how you did it and try again. This will help you to memorize simple things without getting overwhelmed by more complex problems.
Move on to more complex mechanics. After you’ve done a few tutorials and practiced what you’ve learned, you can then challenge yourself to try out more advanced concepts, meaning try to implement things that scare or intimidate you. For example, try and implement a simple slot-based inventory system. If you struggle with it, try something else and come back to it later.
Try making a basic game. Don’t shoot for an AAA game yet. Your first game will never meet the expectations that you’ve set for it in your head, so don’t shoot for the stars with this one. Instead, do something basic that is within your skill range. You can find the steps to a simple challenge Mario clone game below.
Focus on one thing at a time. When you start out with game dev, don’t jump too quickly into other aspects of game dev such as character creation, art, music, etc. You’ll get lost if you focus on too many things at once. So, make sure you spend your time where it matters first — learning how to make games before you learn how to make games look and sound good. Once you know how to make your game, then go and learn the other aspects of game dev.
If you’re looking for something to do next, I’d recommend my other PDF’s in my “Learn Godot” series.
You can view them here.
*Mega List of Godot Courses (Free): *https://www.reddit.com/r/godot/comments/an0iq5/godot_tutorials_list_of_video_and_written
GDQuest Tutorials (Free): https://www.gdquest.com/tutorial/godot/
Learn To Code From Zero by GDQuest (Free): https://gdquest.github.io/learn-gdscript/
Unity Learning from Unity (Free): https://learn.unity.com/
*GameDevTV Courses (Paid): *https://www.gamedev.tv/courses/category/unity
Brackeys (Free): https://www.youtube.com/@Brackeys/playlists
*UE Learning from UE (Free): *https://www.unrealengine.com/en-US/students
GameDevTV Courses (Paid): https://www.gamedev.tv/courses/category/Unreal
Ryan Laley (Free): https://www.youtube.com/@RyanLaley
Reids Chanel (Free): https://www.youtube.com/@ReidsChannel/videos
Code Like Me (Free): https://www.youtube.com/@CodeLikeMe
Good luck with the rest of your journey. Remember, this is not supposed to be a linear journey, so feel free to reach out to me if you need some help or advice. I have another tutorial series planned that I will be releasing soon, so once that is out you will be able to see it in my Gitbook updates. Remember to save your project, and I’ll see you in the next series!
The tutorial series has 23 chapters. I’ll be releasing all of the chapters in sectional daily parts over the next couple of weeks.
If you like this series or want to skip the wait and access the offline, full version of the tutorial series, you can support me by buying the offline booklet for just $4 on Ko-fi!😊
You can find the updated list of the tutorial links for all 23 parts in this series here.