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 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 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 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 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:
		... death code ...
		# Add XP to player

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:

Download “SimpleRPG Stats Upgrade Sprites” – 1 KB

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.

Leveling up

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

signal player_level_up

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_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 and save it in the GUI folder. In this new script, add a variable to store a reference to Player node:

var player

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")

Now connect Player’s player_level_up() signal to LevelPopup and add this code to show the popup:

func _on_Player_player_level_up():
	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 += 50
			player.emit_signal("player_stats_changed", player)
			get_tree().paused = false
		elif event.scancode == KEY_B:
			player.mana_max += 50
			player.mana += 50
			player.emit_signal("player_stats_changed", player)
			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.



Ben · December 24, 2019 at 3:55 pm

I wanted to show you another way to use input events

Thanks for this. I’ve seen multiple ways of handling input but didn’t fully understand this one. One more time seeing it in action was what I needed.


MaggoFFM · December 28, 2019 at 12:17 pm

Just wanted to let you know till here a very nice tutorial.
And i am sure the next ones will also be good! Keep going!


    Davide Pesce · January 1, 2020 at 4:30 pm

    Thank you!


Jef · June 7, 2020 at 9:21 pm

Thanks for this tutorial, I was learning alot of stuff myself.
And didn’t really like youtube tutorials, but this written one showed me a lot of things, I learned alot.

Thanks for your time put into this 🙂


    Davide Pesce · June 8, 2020 at 11:45 am

    Glad you like it Jef!


Ken · September 27, 2020 at 3:25 pm

Hi David
Thank you very much for this great tutorial. I learned a lot from this tutorial series.
I have a couple of issues with your example and wonder if you can provide feedback.

In the _input(event) function of the, I believe you should have put a code to add health/mana potion once the play select A or B. Such as:

player.health_potions += 1
player.manah_potions += 1

Or do you put this code in a later tutorial?

In the add_xp(value) function of the, the last emit_signal(“player_stats_changed”, self) statement has no effect.
I initially thought the code below emits the “player_level_up” signal, show popup and pause until the user select A or B. And then, it emits the “player-stats_changed” to update the HUD. But, I found that the popup with SceneTree paused does not prevent the last emit_signal(“player_stats_changed”, self) from performing. As the result, the HUD updates before the levelup stat change.

if xp >= xp_next_level:
level += 1
xp_next_level *= 2
emit_signal(“player_stats_changed”, self)

I tested this by setting both health_regeneration and mana_regeneration to 0 (e.g. the player is under NO_REGEN spell).

var health_regeneration = 0
var mana_regeneration = 0

As, now no HUD update is required, the levelup stat is not updated until any other stat changes.

Thank you again, David for this excellent tutorial. Any feedback will be greatly appreciated.


    Davide Pesce · September 27, 2020 at 7:31 pm

    Hi Ken!

    1. As you level up, your character’s stats are increased, not the number of potions. If you want the player to get potions as well, you can of course add the instructions you suggested.

    2. emit_signal(“player_stats_changed”, self) is used to update the UI when you gain experience points, it’s not related to level advancement (it’s outside the if block and could be placed before it without changing the behavior of the function). So it has an effect, but not when you level up.
    Indeed the problem you describe is there, and I didn’t notice it before (the regeneration actually made it difficult to find), so thank you for the feedback! Luckily it’s easy to fix it, just add player.emit_signal(“player_stats_changed”, player) to the script when changing player stats.


      Ken · September 28, 2020 at 3:41 am

      Thank you for your quick reply, David.

      Yes, I misunderstood your design. I wrongly thought A or B adds extra potion. 😉

      As you suggested, I put the emit_signal(“player_stats_changed”, player) at end of each A or B if block as below. It works!!!

      if event.scancode == KEY_A:
      player.health_max += 50 += 50
      player.health_potions += 1
      get_tree().paused = false
      emit_signal(“player_stats_changed”, player)
      elif event.scancode == KEY_B:
      player.mana_max += 50
      player.mana += 50
      player.mana_potions += 1
      get_tree().paused = false
      emit_signal(“player_stats_changed”, player)

      But, it caused me another confusion. I thought that in order to emit a signal, the signal MUST exist in the node the script is attached, but this code works while the Popup node doesn’t have such signal in it. The signal is in the Player node and script. I am confused why it works???
      The Player node and the LevelPopup node (in the CanvasLayer) are totally separated independent child nodes of the Root. Player defines the signal but the CanvasLayer doesn’t.
      Is it that if a signal is declared in a node, the signal is global and any other nodes can emit the signal from anywhere? Am I missing something in GDScript?


        Ken · September 28, 2020 at 4:00 am

        After researching, I found the answer of my own question asked above.

        The program itself doesn’t freeze or stop but the Debugger section generates an Error as below:
        emit_signal: Can’t emit non-existing signal “player_stats_changed”.

        It seems obvious that my code actually does not work. The auto re-generation updates the HUD. I still need to find a way how to emit the signal “player_stats_changed” from the script. 😉


          Davide Pesce · September 28, 2020 at 9:45 am

          Hi Ken, if you look again at my answer you will see that I call the emit_signal function of player: player.emit_signal(“player_stats_changed”, player)


          Ken · September 28, 2020 at 1:55 pm

          Thank you David for pointing that out. I didn’t know I could emit a signal of other node.
          By the way, there is no more reply click to your last comment, so this reply is to your last comment.


          Davide Pesce · September 28, 2020 at 2:31 pm

          You can do it, even if it’s not the approach I usually recommend. The best solution would be to create setter methods for all variables, and to emit the signal within them.


          Ken · September 29, 2020 at 3:22 am

          Yes, that’s a brilliant idea. I learned a lot. Thank you.


Mike Frost · December 17, 2020 at 1:55 pm

Hi Davide,
just want to say:

“Thank you!”

Your tutorial once again has been such a source of help to me.
Only thing I changed was the input to choose between mana or healthbonus to the attack and magic keys of the controller.
The fact that you wrote down this tutorial makes it great to work with in one’s own speed 🙂

Stay healthy & rock on!

Comments are closed.

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.