In this tutorial we will add one of the most important features of a RPG: level advancement.
A level is a number that represents a character’s overall skill and power. Characters usually start at level 1 and increase their level by gaining experience points.
Experience points, also known as XP, are used in RPGs to quantify the player’s progression over the course of the game. Experience points are generally awarded for defeating enemies or completing quests.
When a sufficient amount of experience is obtained, characters “levels up”, and their stats (such as maximum health, magic and strength) increase. Leveling up may also give characters new powerful abilities. For our game, at every level up we will let the player choose whether to increase health or mana by 50 points.
Usually, in RPGs each subsequent level requires more XP to be reached, so leveling up becomes increasingly difficult. In our case, at each level up we will double the number of XP required to reach the next level.
Now that we have an idea of what we have to do, let’s get started!
XP and Level GUI
In the GUI tutorial we added a box in the GUI that shows us how many experience points we have and our level. We need to link this box to the player’s stats, so start adding these variables to Player’s script:
var xp = 0; var xp_next_level = 100; var level = 1;
These variables store how many xp we have, how many we need to reach the next level and what our current level is.
To update the GUI, attach a new script to the XP node. Call it XP.gd and save it in the GUI folder. Then, select the Player node and in the Node panel double click on the player_stats_changed() signal to connect it to XP.
The _on_Player_player_stats_changed() function will be automatically added to the XP.gd script. Change it this way:
func _on_Player_player_stats_changed(player): $ValueXP.text = str(player.xp) + "/" + str(player.xp_next_level) $ValueLevel.text = str(player.level)
This function simply takes the values from the player and inserts them into the GUI Labels.
Gaining XP when a skeleton die
As we saw in the introduction, defeating a monster is the main way to get experience points. So, when a skeleton dies, we’ll have to update the player’s XP amount.
First, in the Player.gd script add the function add_xp():
func add_xp(value): xp += value emit_signal("player_stats_changed", self)
The function receives as an argument the number of XP to be awarded to the player. This value is added to the current XP and then the function emit the signal to update the GUI.
Later we will return to this function to add level advancement.
Now let’s move on to the Skeleton.gd script. In the hit() function, when we handle the death of the skeleton, we must add the code that assigns the XP to the player:
func hit(damage): health -= damage if health > 0: $AnimationPlayer.play("Hit") else: ... death code ... # Add XP to player player.add_xp(25)
Try running the game now: every skeleton killed will increase the player’s XP by 25 points.
Level advancement popup
When we reach the number of XP to level up, we want the stop the game and show a popup that let the player choose whether they want to increase health or mana score.
So, add a Popup node to CanvasLayer and rename it LevelPopup. The popup node provides us with an easy way to show popup dialog and windows.
In the Scene panel, make LevelPopup visible by clicking on the icon next to it.
Next to LevelPopup, a warning will appear telling you that the panel will only be visible in the Editor but not in game. To show it during the game, we will have to call one of the popup() functions of the Popup class.
With LevelPopup selected, in the Inspector enable Exclusive (you can find it in the Popup section). When Exclusive is enabled, the popup will not be hidden when a click event occurs outside of it. Then, in Node → Pause, set Mode to Process. This will allow us to process the inputs of this panel even when the game is paused.
Still in the Inspector, in the Rect section, set panel Size to (100,50). Then, add a ColorRect node to LevelPopup and also set its size to (100,50).
Add a Label node to ColorRect. In the Inspector, set Text to New level! and Align to Center. Then, under Custom Font, load the Font.tres font we created in the GUI tutorial and set Font Color to (90,90,90,255). Finally, set Rect → Position to (0,1) and Rect → Size to (100,10).
To show the player the available stats upgrades, we will use two images, which you can download by pressing the button below:
Once downloaded, import them into the GUI folder (remember to re-import them disabling Filter in the Import panel).
Add a Sprite node to ColorRect and drag health_bonus.png to the Texture property in the Inspector. then, set Node2D → Transform → Position to (31,30). Add another Sprite for mana_bonus.png and set its position to (69,30).
To show the player what keys to press to choose the upgrade, add a Label node to ColorRect. In the Inspector, set Text to A. Then, under Custom Font, load the Font.tres font and set Font Color to (90,90,90,255). Finally, set Rect → Position to (7,25) and Rect → Size to (6,10). Duplicate this label, change Text to B and set Rect → Position to (88,25).
The result should be this:
Someone who already has experience with Godot may ask: why didn’t you use a PopupPanel node to create the popup? It would have been the simplest solution, because PopupPanel already integrates a configurable background. Unfortunately, for now there is a bug that makes it unusable.
Now that we have the upgrade selection popup, we can handle the player’s level advancement.
When the player levels up, we will emit a signal. When the popup receives this signal, it will show up and the game will be paused to give the player time to choose the upgrade.
So, to begin with, declare this new signal in Player.gd:
Then, go to the add_xp() function and change it like this:
func add_xp(value): xp += value # Has the player reached the next level? if xp >= xp_next_level: level += 1 xp_next_level *= 2 emit_signal("player_level_up") emit_signal("player_stats_changed", self)
The code we added checks if player’s XP is equal to or greater than the XP needed to reach the next level. If true, the level is increased by 1, and the next XP target is calculated by doubling the current level target. Finally, the player_level_up() signal is emitted.
To handle this signal, attach a new script to the LevelPopup node. Call it LevelPopup.gd and save it in the GUI folder. In this new script, add a variable to store a reference to Player node:
Then, add the _ready() function, where we will save the reference to Player and disable the input with the set_process_input() function (we don’t want to handle the input when the popup is not visible).
func _ready(): player = get_node("/root/Root/Player") set_process_input(false)
Now connect Player’s player_level_up() signal to LevelPopup and add this code to show the popup:
func _on_Player_player_level_up(): set_process_input(true) popup_centered() get_tree().paused = true
The first thing this function does is enabling input, to allow the player to choose the upgrade. Then, it calls the popup_centered() function. As the name suggests, this function shows the popup in the center of the screen. Finally, the game is paused setting the paused property of the current SceneTree to true.
The last thing to do is to handle player’s input. If the A key on the keyboard is pressed, the health is increased; if instead the B key is pressed, the mana is increased.
func _input(event): if event is InputEventKey: if event.scancode == KEY_A: player.health_max += 50 player.health += 50 player.emit_signal("player_stats_changed", player) hide() set_process_input(false) get_tree().paused = false elif event.scancode == KEY_B: player.mana_max += 50 player.mana += 50 player.emit_signal("player_stats_changed", player) hide() set_process_input(false) get_tree().paused = false
First of all, the function checks if the event is of type InputEventKey, i.e. if it’s an event generated by the keyboard. If so, it checks event‘s scancode property to know which key was pressed. If it’s one of the valid keys, the relative stat is incremented. After that, the popup is hidden by calling the hide() function. The input is then disabled again and finally the pause is removed.
A better alternative than directly checking which key was pressed would be to define input actions (as we saw in the player movement and following tutorials), but I wanted to show you another way to use input events.
In this tutorial we learned:
- how popups work
- how to pause the game, keeping only a few nodes running
- another way to use input events
If you want to try the game, click on the button below:
In the next tutorial we will introduce non-player characters (NPCs) and quests into the game.