Introduction

Welcome back everyone! It’s time to finish our 2D RPG in Godot!

For Part 1 of this tutorial series, we started setting up our 2D RPG project in the open-source and free Godot game engine. Some of the things we learned about include: tilemaps, player controllers, raycasting, sprite animations, and more! All in all, we have a great base to work with and expand on!

However, what’s an RPG without some enemies or items to loot? In this second part of our 2D RPG tutorial for Godot, we’ll finish by adding these elements, as well as adding the ability for our camera to follow the player.

So sit back, and prepare yourself to finish your first RPG in Godot!

Project Files

For this project, we’ll be needing some assets such as sprites and a font. These will be sourced from kenney.nl and Google Fonts. You can, of course, choose to use your own assets, but for this course we’ll be using these.

Download the sprite and font assets here .

. Download the complete Godot project here .

Don't miss out! Offer ends in Access all 200+ courses

Access all 200+ courses New courses added monthly

New courses added monthly Cancel anytime

Cancel anytime Certificates of completion ACCESS NOW

Camera Follow

With our big scene, there’s one problem which is the fact that the camera doesn’t move. To fix this, let’s create a new Camera2D node in the MainScene.

Then we can create a new script attached to the camera node. Call it CameraFollow. All we’re going to do here is find the player and move towards them every frame.

onready var target = get_node("/root/MainScene/Player") func _process (delta): position = target.position 1 2 3 4 onready var target = get_node ( "/root/MainScene/Player" ) func _process ( delta ) : position = target . position

Finally, in the inspector make sure to enable Current so that the camera will be the one we look through.

Now if we press play, you’ll see that the camera follows the player.

Creating an Enemy

Let’s now create an enemy scene. These will chase after the player and attack them when in range.

Create a new scene with a root node of KinematicBody2D Rename it to Enemy and save the scene Drag in the player_s_0.png sprite to create a sprite node Set the Modulate to red Create a CollisionShape2D node with a capsule shape and resize it Create a Timer node – this will be used for checking for when we can attack

Scripting the Enemy

On the enemy node, create a new script called Enemy. We can start with the variables.

var curHp : int = 5 var maxHp : int = 5 var moveSpeed : int = 150 var xpToGive : int = 30 var damage : int = 1 var attackRate : float = 1.0 var attackDist : int = 80 var chaseDist : int = 400 onready var timer = $Timer onready var target = get_node("/root/MainScene/Player") 1 2 3 4 5 6 7 8 9 10 11 12 13 var curHp : int = 5 var maxHp : int = 5 var moveSpeed : int = 150 var xpToGive : int = 30 var damage : int = 1 var attackRate : float = 1.0 var attackDist : int = 80 var chaseDist : int = 400 onready var timer = $ Timer onready var target = get_node ( "/root/MainScene/Player" )

Inside of the _physics_process function (gets called 60 times per second) we want to move towards the target if we’re further than the attack distance but closer than the chase distance.

func _physics_process (delta): var dist = position.distance_to(target.position) if dist > attackDist and dist < chaseDist: var vel = (target.position - position).normalized() move_and_slide(vel * moveSpeed) 1 2 3 4 5 6 7 8 func _physics_process ( delta ) : var dist = position . distance_to ( target . position ) if dist > attackDist and dist < chaseDist : var vel = ( target . position - position ) . normalized ( ) move_and_slide ( vel * moveSpeed )

Back in the MainScene let’s drag in the Enemy scene to create a new instance. Now we can press play and test it out. The enemy should move towards us.

Now that the enemy moves towards us, let’s have it attack us.

Go to the Enemy scene Select the Timer node In the Inspector panel, click on the Node tab Double click on the timeout() signal Click Connect

This will create the _on_Timer_timeout function. Here, we want to check if we’re within the attack distance and then attack the target. We’ll be creating the take_damage function on the player soon.

func _on_Timer_timeout (): if position.distance_to(target.position) <= attackDist: target.take_damage(damage) 1 2 3 4 func _on_Timer_timeout ( ) : if position . distance_to ( target . position ) < = attackDist : target . take_damage ( damage )

In the _ready function which gets called once the node is initialized, let’s setup the timer node.

func _ready (): timer.wait_time = attackRate timer.start() 1 2 3 4 func _ready ( ) : timer . wait_time = attackRate timer . start ( )

Finally for the enemy, let’s create the take_damage and die functions. The target.give_xp function will be called over on the player script later on.

func take_damage (dmgToTake): curHp -= dmgToTake if curHp <= 0: die() func die (): target.give_xp(xpToGive) queue_free() 1 2 3 4 5 6 7 8 9 10 11 func take_damage ( dmgToTake ) : curHp -= dmgToTake if curHp < = 0 : die ( ) func die ( ) : target . give_xp ( xpToGive ) queue_free ( )

Player Functions

Now we want to go over to the Player script and add in some new functions to work with the enemies. Let’s start with the give_gold function which will add gold to the player.

func give_gold (amount): gold += amount 1 2 3 func give_gold ( amount ) : gold += amount

For the levelling system, let’s create the give_xp and level_up functions.

func give_xp (amount): curXp += amount if curXp >= xpToNextLevel: level_up() func level_up (): var overflowXp = curXp - xpToNextLevel xpToNextLevel *= xpToLevelIncreaseRate curXp = overflowXp curLevel += 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func give_xp ( amount ) : curXp += amount if curXp > = xpToNextLevel : level_up ( ) func level_up ( ) : var overflowXp = curXp - xpToNextLevel xpToNextLevel *= xpToLevelIncreaseRate curXp = overflowXp curLevel += 1

The take_damage and die functions will be called when an enemy attacks us.

func take_damage (dmgToTake): curHp -= dmgToTake if curHp <= 0: die() func die (): get_tree().reload_current_scene() 1 2 3 4 5 6 7 8 9 10 func take_damage ( dmgToTake ) : curHp -= dmgToTake if curHp < = 0 : die ( ) func die ( ) : get_tree ( ) . reload_current_scene ( )

Now if we press play, you’ll see that the enemy can attack and kill us – which will reload the scene.

Player Interaction

With our player, we want the ability to interact with chests and enemies. In the Player script, let’s create the _process function (called every frame) and check for when we’re pressing the interact button.

func _process (delta): if Input.is_action_just_pressed("interact"): try_interact() 1 2 3 4 func _process ( delta ) : if Input . is_action_just_pressed ( "interact" ) : try_interact ( )

The try_interact function will check to see if the raycast is hitting anything. If it’s an enemy, damage them but if it’s not – call the on_interact function if they have it.

func try_interact (): rayCast.cast_to = facingDir * interactDist if rayCast.is_colliding(): if rayCast.get_collider() is KinematicBody2D: rayCast.get_collider().take_damage(damage) elif rayCast.get_collider().has_method("on_interact"): rayCast.get_collider().on_interact(self) 1 2 3 4 5 6 7 8 9 func try_interact ( ) : rayCast . cast_to = facingDir * interactDist if rayCast . is_colliding ( ) : if rayCast . get_collider ( ) is KinematicBody2D : rayCast . get_collider ( ) . take_damage ( damage ) elif rayCast . get_collider ( ) . has_method ( "on_interact" ) : rayCast . get_collider ( ) . on_interact ( self )

Chest Interactable

Now we need something to interact with. This is going to be a chest which will give the player some gold. Create a new scene with a root node of Area2D.

Rename the node to Chest Save the node Drag in the rpgTile163.png to create a Sprite node Create a CollisionShape2D node Set the shape to a Rectangle and resize it to fit the sprite

Next, create a new script called Chest attached to the Area2D node. All we’re going to do here, is have the on_interact function which gives the player gold then destroys the node.

export var goldToGive : int = 5 func on_interact (player): player.give_gold(goldToGive) queue_free() 1 2 3 4 5 6 export var goldToGive : int = 5 func on_interact ( player ) : player . give_gold ( goldToGive ) queue_free ( )

You might notice that interacting with the chest wont work but attacking the enemy does work. So in the Player scene, select the ray cast node and enable Collide With Areas.

Creating the UI

Create a new scene with a root node of Control.

Rename the node to UI Save the scene Create a new TextureRect node and call it BG Set the Texure to UI_Square.png Enable Expand Drag the 4 anchor points down to the bottom center Set the Position to 386, 520 Set the Size to 250, 60 Set the Visibility > Self Modulate to dark grey

As a child of this BG node, we want to create three new nodes.

LevelBG – TextureRect

HealthBar – TextureProgress

XpBar – TextureProgress

For the TextureProgress nodes…

Enable Nine Patch Stretch Set the Under and Progress textures to UI_Square.png Set the Under tint to dark grey Set the Progress tint to red or green

Now we need to setup our fonts. With the assets we imported into the project, we have two font files. Right now, these are just .ttf files which need to be converted in a format that Godot can read. For each font file…

Right click it and select New Resource… Select the DynamicFont resource Save this to the Font folder Double click on the new resource and drag the .ttf file into the Font > Font Data property

As a child of the LevelBG node, create a new Label node and rename it to LevelText.

Resize the rect box to fit the level BG square

Set Align and Valign to Center

and to Center Drag the bold dynamic font resource into the Custom Fonts property

property Set the font Size to 35

to 35 Set the Visibility > Self Modulate to grey

Create a new Label node called GoldText.

Move and resize it to look like below Set Align to Center Set the Custom Font to the regular dynamic font resource

Scripting the UI

Next up, let’s create a new script attached to the UI node, called UI. We’ll start with the variables to reference the children nodes.

onready var levelText : Label = get_node("BG/LevelBG/LevelText") onready var healthBar : TextureProgress = get_node("BG/HealthBar") onready var xpBar : TextureProgress = get_node("BG/XpBar") onready var goldText : Label = get_node("BG/GoldText") 1 2 3 4 onready var levelText : Label = get_node ( "BG/LevelBG/LevelText" ) onready var healthBar : TextureProgress = get_node ( "BG/HealthBar" ) onready var xpBar : TextureProgress = get_node ( "BG/XpBar" ) onready var goldText : Label = get_node ( "BG/GoldText" )

Then we have our functions which update each of these nodes.

# updates the level text Label node func update_level_text (level): levelText.text = str(level) # updates the health bar TextureProgress value func update_health_bar (curHp, maxHp): healthBar.value = (100 / maxHp) * curHp # updates the xp bar TextureProgress value func update_xp_bar (curXp, xpToNextLevel): xpBar.value = (100 / xpToNextLevel) * curXp # updates the gold text Label node func update_gold_text (gold): goldText.text = "Gold: " + str(gold) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # updates the level text Label node func update_level_text ( level ) : levelText . text = str ( level ) # updates the health bar TextureProgress value func update_health_bar ( curHp , maxHp ) : healthBar . value = ( 100 / maxHp ) * curHp # updates the xp bar TextureProgress value func update_xp_bar ( curXp , xpToNextLevel ) : xpBar . value = ( 100 / xpToNextLevel ) * curXp # updates the gold text Label node func update_gold_text ( gold ) : goldText . text = "Gold: " + str ( gold )

Now that we have the UI script, let’s go over to the MainScene and drag the UI scene in. To make it render to the screen, we need to create a CanvasLayer node and make UI a child of it.

In order to be able to access the UI script for the player, we need to move the canvas layer up to the top of the hierarchy.

Over in the Player script, let’s create a variable to reference the UI script.

onready var ui = get_node("/root/MainScene/CanvasLayer/UI") 1 onready var ui = get_node ( "/root/MainScene/CanvasLayer/UI" )

Create the _ready function which gets called once the node is initialized. In there, we’ll be initializing the UI.

func _ready (): ui.update_level_text(curLevel) ui.update_health_bar(curHp, maxHp) ui.update_xp_bar(curXp, xpToNextLevel) ui.update_gold_text(gold) 1 2 3 4 5 6 func _ready ( ) : ui . update_level_text ( curLevel ) ui . update_health_bar ( curHp , maxHp ) ui . update_xp_bar ( curXp , xpToNextLevel ) ui . update_gold_text ( gold )

Now we need to go through a few functions and call the UI functions at the end of them. They go as following:

# give_gold function ui.update_gold_text(gold) # give_xp function ui.update_xp_bar(curXp, xpToNextLevel) # level_up function ui.update_level_text(curLevel) ui.update_xp_bar(curXp, xpToNextLevel) # take_damage function ui.update_health_bar(curHp, maxHp) 1 2 3 4 5 6 7 8 9 10 11 12 # give_gold function ui . update_gold_text ( gold ) # give_xp function ui . update_xp_bar ( curXp , xpToNextLevel ) # level_up function ui . update_level_text ( curLevel ) ui . update_xp_bar ( curXp , xpToNextLevel ) # take_damage function ui . update_health_bar ( curHp , maxHp )

We can now press play and see that the UI is set at the start of the game and changes as we get gold, take damage, get xp and level up.

With all of this, we’re finished with our RPG game in Godot. Let’s now go back to the MainScene and place some enemies and chests around the place.

Conclusion

Congratulations on completing the tutorial! Through perseverance, hard work, and a little bit of know-how, we just created a 2D RPG in Godot with a number of different features. Over the course of Part 1 and Part 2, we’ve shown you how to set up:

A top-down 2D player controller

Enemies who can attack and chase the player

Chests we can interact with to get gold

A UI to show our health, XP, level, and gold

And with that, you should now possess the fundamental skills to create even more RPGs in the future! Of course, you can expand upon the game created here – adding in new features, fleshing out current ones, or even just improving certain aspects you felt were lacking! There are endless directions to go from here, but with this solid foundation, you’ll have quite the headstart!

Thank you very much for following along, and I wish you the best of luck with your future games.