In this tutorial we will learn how to create objects that can be picked up and used by the player. To keep things simple, in our little game there will be only two types of objects, i.e. two potions that will allow the player to recover health and mana.

Potion scene

To begin, download the image we will use for potions by clicking the button below.

Download “SimpleRPG Small Potions Sprite” small_potions.png – 202 B

To keep the project organized, create a new Potion folder under Entities. Within this folder, import the image you just downloaded (remember to re-import the image with Filter disabled).

Now, create a new scene. We will use this scene for both types of potions. Use an Area2D node as the root node, and rename it Potion.

Add a Sprite node to Potion. In the Inspector, drag small_potions.png to the Texture property. Since the image contains both potions, we need to enable the Region section and set the w and h properties to 8. In this way, only one potion is used.

Then, add a CollisionShape2D node to Potion. In the Inspector, set Shape to new RectangleShape2D and resize the rectangle to have the same size of the potion.

Potion script

Attach a new script to Potion and save it as Potion.gd in the Entities/Potion folder.

In the script, before extends Area2D, add this line:

tool

When you add at the top of a script the tool keyword, it will be executed not only during the game, but also in the editor. Running a script in the editor can be useful for doing many things, but it’s mostly used in level design to show things that would otherwise be visible only during game play.

In our case, it will allow us to see in the editor what kind of potion we instantiated. Without using the keyword tool, both types of potions would be shown in the editor with the same sprite.

We will see later that you can also decide which parts of the script execute in the editor, which in game, and which in both.

First, we need a variable to store the type of potion:

enum Potion { HEALTH, MANA }
export(Potion) var type = Potion.HEALTH

The first line declares an enumeration. Enums are a way of declaring a group of related constants. We have declared an enum called Potion, which contains two constants HEALTH and MANA, to which the values 0 and 1 are automatically assigned.

In the second line we declare the variable type, which has the constant Potion.HEALTH as default value. As we have already seen in the previous tutorials, the keyword export makes a variable visible in the Inspector. If after export, inside brackets, we put the name of an enum, this variable will be shown in the Inspector as a drop down list.

Save the scene as Potion.tscn in Entities/Potion.

If you instantiate a potion by dragging the Potion.tscn scene into the main scene, and then you try to change the type of potion by changing the type variable, you’ll see that the sprite of the potion does not change.

To change the sprite in the editor based on the type variable, we can check its value inside the _process() function. Since we used the keyword tool at the beginning of the script, the _process() function will be executed in the editor, so the sprite will change as soon as we change the value of Type in the Inspector.

So, let’s add the _process() function to the script:

func _process(delta):
	if Engine.editor_hint:
		if type == Potion.MANA:
			$Sprite.region_rect.position.x = 8
		else:
			$Sprite.region_rect.position.x = 0

The first line of the function checks if we are in the editor, using the editor_hint property of the Engine class. We want to execute this code only in the editor, because during the game the potion type does not change. So, it would be a waste of processing power to repeat the type check at each frame. If we are in the editor, we check the potion type and based on that, we choose the region of the image to use as the potion’s sprite.

Now, place a potion in main scene and move it near the player. Then, place a second potion and in Inspector change type to Mana. You’ll see the sprite change.

When running the game, we need to set potion’s sprite at the beginning. So, we must put the type check in the _ready() function:

func _ready():
	if type == Potion.MANA:
			$Sprite.region_rect.position.x = 8
		else:
			$Sprite.region_rect.position.x = 0

If the player walk over a potion, we want it to be picked up. We must therefore detect if the player has entered the potion area. To do so, we connect the body_entered() signal of Potion to itself. The function _on_Potion_body_entered() will be automatically added to the script. Add this code to the function:

func _on_Potion_body_entered(body):
	if body.name == "Player":
		body.add_potion(type)
		get_tree().queue_delete(self)

First, the function checks if the body that entered the area is Player. If so, it does two things:

  • it calls Player‘s add_potion() function, which will update the player’s inventory and emits the signal to update the GUI (we will write this function shortly)
  • it removes the potion from the scene tree.

Since there is nothing else to do in the Potion scene, save it and return to the main scene.

Player script

Open Player script (player.gd) and at the beginning add this code to store player’s inventory:

# Player inventory
enum Potion { HEALTH, MANA }
var health_potions = 0
var mana_potions = 0

We simply declared two variables to store how many potions we have. We also copied here the enum used in Potions.gd, so we can use the same constants.

Now, we must add the add_potion() function to the script. We called this function in the Potion script when the player pick up the potion.

func add_potion(type):
	if type == Potion.HEALTH:
		health_potions = health_potions + 1
	else:
		mana_potions = mana_potions + 1
	emit_signal("player_stats_changed", self)

This function simply updates the inventory based on the type of potion picked up and then emits the signal to update the GUI.

Updating potions count in GUI

In the GUI tutorial, we had added the interface nodes to show the inventory, but we hadn’t written the code to update the values, so let’s do it now.

Attach a new script to the HealthPotions node (it’s located under CanvasLayer), call it HealthPotions.gd and save it in the GUI folder.

Select Player and go to the Node panel to connect the player_stats_changed() signal to HealthPotions. The function _on_Player_player_stats_changed() is automatically added to the HealthPotions.gd script. Replace the function with this code:

func _on_Player_player_stats_changed(var player):
	$Label.text = str(player.health_potions)

The function simply assigns the amount of potions currently present in the inventory to the text property of the Label node. To assign the value, it must first be converted to a string, using the str() function.

Repeat the procedure for the ManaPotions node. The function in this case will be:

func _on_Player_player_stats_changed(var player):
	$Label.text = str(player.mana_potions)

Run the game and try moving the player on the potions to see if they are removed from the scene and if the GUI is updated correctly.

Dropping a potion after enemy death

In RPGs, when an enemy dies, usually players get a loot meant to reward them for progressing in the game. In our game, when the player defeats a skeleton, there will be an 80% chance of getting a random potion.

To implement loot dropping, open the Skeleton.gd script. First of all, we need a reference to the Potion.tscn scene to be able to instantiate it when the skeleton die:

# Reference to potion scene
var potion_scene = preload("res://Entities/Potion/Potion.tscn")

In the _ready() function, we must add a call to the randomize() function to randomize the seed of the random number generator:

func _ready():
	player = get_tree().root.get_node("Root/Player")
	rng.randomize()

In the hit() function, when the skeleton dies, we must randomly decide whether to instantiate a potion. Edit the function as follows:

func hit(damage):
	health -= damage
	if health > 0:
		$AnimationPlayer.play("Hit")
	else:
		... death code ...
		
		# 80% probability to drop a potion on death
		if rng.randf() <= 0.8:
			var potion = potion_scene.instance()
			potion.type = rng.randi() % 2
			get_tree().root.get_node("Root").call_deferred("add_child", potion)
			potion.position = position

Immediately after executing the existing death code, a random number between 0 and 1 is generated using the randf() function. If this number is less than or equal to 0.8 (80%), a new potion is instantiated.

Then, an integer random number is generated using the randi() function. With a modulo (%) 2 operation, you get the reminder of the division by 2 of this number. Since the rest can only be 0 or 1, one of the two types of potion is randomly chosen (0 for health, 1 for mana).

Lastly, the potion is added to the scene tree and is placed where the skeleton died.

Note for more advanced readers: we cannot directly call the add_child() function to add the potion, because it isn’t safe to add a new Area2D to the node tree inside a function that handles the on_area_entered() signal of another Area2D node. We must therefore use call_deferred() to defer the call to add_child() during idle time.

Drinking potions

The last thing left to do is handling the input to drink potions.

First, click on the Project → Project Settings menu to open the Project Settings window and go to the Input Map tab.

We need to create two actions:

  • drink_health, to which we map the Comma key (,) and the joypad button 0 (equivalent to the PlayStation Cross button and the XBox button A)
  • drink_mana, to which we map the Period key (.) and the joypad button 1 (equivalent to the PlayStation Circle button and the XBox button B)

If you don’t remember how to create input actions, take a look at the Player Movement tutorial.

Close the window and go to the _input() function in Player‘s script. Add to the function the code to handle this two new actions:

elif event.is_action_pressed("drink_health"):
	if health_potions > 0:
		health_potions = health_potions - 1
		health = min(health + 50, health_max)
		emit_signal("player_stats_changed", self)
elif event.is_action_pressed("drink_mana"):
	if mana_potions > 0:
		mana_potions = mana_potions - 1
		mana = min(mana + 50, mana_max)
		emit_signal("player_stats_changed", self)

When the input action is detected, the function checks if the player has any potions left to use. If so, the quantity of the potion is reduced by 1 and 50 points are added to the corresponding stat (the min() function is used to not exceed the maximum value). Finally, it emits the player_stats_changed() signal to update the GUI.

Run the game and, when you have lost health points or consumed your mana, try to drink a potion to recover some points.

Conclusions

In this tutorial we have seen how to create objects that can be collected and used by the player. Furthermore, we learned how to use the tool keyword to add interactivity to the editor, and we expanded our knowledge of GDScript a little bit.

If you want to try the game, click on the button below:

In the next tutorial, we will add another fundamental piece of RPGs: Experience Points and Level advancement.




6 Comments

Jorge · December 15, 2019 at 3:43 pm

Hello, please finish this rpg Godot series! I’m starting with this engine and these tutorials are being really helpful to me. Thank so much for your work

    Davide Pesce · December 15, 2019 at 9:20 pm

    Thanks Jorge, I’m glad you found these tutorials useful! If everything goes as planned, by the end of January I should finish the series.

Akilen · December 16, 2019 at 9:46 pm

Hi David

Thanks for series. It gave me a lot of idea for my game

    Davide Pesce · December 17, 2019 at 8:58 am

    Happy to hear that!

Novu · February 2, 2020 at 11:37 am

Thanks for the tutorials, very helpful!

One small note, don’t know if it’s because I have not followed correctly all the previous lessons, but I think that when you say “In the _ready() function, we must add a call to the randomize()…” you show the Player.gd source, and it should be in Skeleton.gd. Also, Skeleton.gd already has a random number generator ready (rng), so this step is not needed.

    Davide Pesce · February 2, 2020 at 10:33 pm

    You’re right, I accidentally copied the Player.gd code. Thanks for the feedback!

    Calling the randomize function of RandomNumberGenerator is still required in Skeleton.gd to initialize the generator with a random seed.

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