How to Make an RPG in Godot 4 – Complete Guide

For anyone keen on designing their own RPG in Godot, you’ve come to the right Godot RPG tutorial.

Whether RPGs or strategy games, implementing strategic combat is a fundamental part of the process. This tutorial breaks down the creation of battle mechanisms, focusing on implementing various combat actions, creating a user interface (UI), and assigning different combat actions to individual characters.

By the end of this tutorial, you will have developed a simple yet effective combat system complete with player and enemy turns and actions. Note that this tutorial is intermediate level and expects familiarity with Godot engine and GDScript language. However, you can also review our full course, Build a Micro Turn-Based RPG with Godot 4 which covers these concepts more in-depth.

Without delay, let’s jump into it.

Project Files

Feel free to download the assets used in this Godot RPG tutorial to support your learning. You can modify these files as per your needs to try out different combat scenarios.

Download Project Files Here

CTA Small Image
FREE COURSES AT ZENVA
LEARN GAME DEVELOPMENT, PYTHON AND MORE
ACCESS FOR FREE
AVAILABLE FOR A LIMITED TIME ONLY

Combat Actions – Part 1

To start our Godot RPG tutorial, we will begin implementing the combat actions that will make up a major part of our game. In our game, a Combat Action will be an action the player or the enemy can do when it is their turn. These actions will be used to deal damage to the other Character or heal the current Character, however, we will be making the system in such a way you can expand it to add more features if you would like. Our Godot RPG tutorial game will include three basic combat actions:

  • Basic Attack, which deals damage.
  • Heavy Attack, which deals more damage than the basic attack.
  • Heal, which heals the character who is using the action.

All of these actions also need to be interchangeable so that we can give the player different actions to the enemy. The system we design will also be fairly modular – so when you’re done with this Godot RPG tutorial, you can makes up your own attacks as needed.

Using Resources

Because we want to have multiple, interchangeable combat actions, we don’t want to hard code each action into the Character script. This is where Resources come into play. A Resource in Godot is effectively a custom asset that will allow us to store the information needed for each combat action. To create a new custom Resource, we first need to create a New Script by right-clicking in the FileSystem window.

FileSystem window

We will call this “CombatAction”.

CombatAction script

This script also needs to inherit from the Resource class by selecting the tree structure button.

tree structure button

Select the Resource class as the class to inherit from.

Resource class

With these values selected, we are ready to choose Create and open up the script for editing. Before we can begin adding information to the new resource, we first need to give the script a class_name definition. This will make it possible to create the Resource instance in the FileSystem window along with identifying it in scripts. For this class name, we will use the name CombatAction.

class_name CombatAction

Creating the Variables

We are now ready to set up the properties for this Resource in our Godot RPG tutorial. Each of these variables will be exported so that we can edit them across each instance of the CombatAction resource. The first variable will be called display_name. This will define the name that is shown to the player, so we can put a placeholder of “Action (x DMG)” in place as a default value.

@export var display_name = "Action (x DMG)"

Next, we will create a variable called damage, which will have a type of int and have a default value of 0. This will be the damage that the Character applies to their opponent when the action is used.

@export var damage : int = 0

We can copy this variable for the heal property.

@export var heal : int = 0

Creating Combat Actions

With the variables in place, we can begin creating each instance of the new CombatAction resource. We will do this in a new folder inside of the FileSystem window to keep the project organized.

new folder

Inside this folder, we can create our Resources. To do this select the new -> Resource option in the right-click menu.

Resource option

Resource is just another name for an asset in our project. Although we often won’t access assets like scenes and sprites from this menu, you can create any type of Resource here. Because our CombatAction inherits from Resource, we are able to create them as resources within the FileSystem. This means that we don’t need to instantiate the CombatAction as a node inside the scene to use it. Instead, we can access it directly from the asset. To create the resource, simply select CombatAction from the resource tree.

CombatAction resource tree

We can name this “Slash.tres” as that will be the name for our base attack action.

Slash.tres

With the Slash.tres resource select in the FileSystem, you will notice the properties we have created are editable in the Inspector, similar to node instances in a scene. For our Slash resource, we want the following values:

  • Display Name: Slash (5 DMG)
  • Damage: 5
  • Heal: 0

Slash resource

We can now easily create a second CombatAction resource with different values. This can be called “Heal.tres”.

Slash resource

For the Heal resource, we will use the following values:

  • Display Name: Heal (5 HP)
  • Damage: 0
  • Heal: 5

Heal.tre

As a challenge, before the next section of our Godot RPG tutorial, try creating another combat action called Stomp. This action will deal 8 damage to the opponent.

Combat Actions – Part 2

Hopefully, you had a good go at the challenge set above for our Godot RPG tutorial! To create our Stomp combat action, we will begin by creating another CombatAction resource.

Stomp combat action

We can call this “Stomp.tres” to follow the naming scheme set up in the previous lesson. For the properties of this action, we will use the following:

  • Display Name: Stomp (8 DMG)
  • Damage: 8
  • Health0

Stomp.tres

Modifying the Character Script

Currently, we have no way to assign our combat actions to our Character. Luckily, as we have created them as Resources this will be no problem. Previously, we created a variable called combat_action in the Character script. This should be renamed to combat_actions to better show there are multiple elements in this variable.

@export var combat_actions : Array

The combat_actions variable is given a type of Array. This type allows us to store multiple items inside one variable in a list format. With this in mind, we can select the Player node and begin adding elements to our Combat Actions array from the Inpector window. Press the Add Element button to add room for new properties in the array.

Add Element button

By default, Godot does not know we want our Array to contain CombatActions. To fix this, select the pencil button to the right of the new element and select the Object type.

Object type

This will then allow us to drag our combat action into the new property.

Pasted 108

This process can then be repeated for both the Heal and the Stomp combat actions.

Object type

We can then repeat this process for the Enemy character. However, to make the game more playable, we don’t want to give our enemy the Stomp combat action, so we can simply not add this Resource.

Enemy character
This means we now have all the code in place to create the gameplay systems. For the next step of our Godot RPG tutorial, we will begin creating the Player’s UI for selecting the combat actions.

Player UI – Part 1

Our Godot RPG tutorial game now needs a user interface to allow our players to select the combat action they want to use. The idea is to create a list of buttons that appear below the Player character instance whenever it is their turn, when the button is clicked, this will trigger the combat action to be used.

Creating the UI

To begin with, we will create a new VBoxContainer node in the BattleScene. This will act as a container for our UI, that automatically arranges our buttons into a vertical list.

VBoxContainer

We can rename this node to “PlayerCombatActionsUI”.

PlayerCombatActionsUI

We can then add a Button as a child node to this container.

 Button child node

As you will see, because we use a VBoxContainer to handle the UI positioning, the button is automatically scaled to the width of the container.

UI positioning

We probably want to make this a bit taller. The best way to do that is to enter some default text on our button, such as “Slash (5 DMG)”.

Button text with Slash combat action entered

This will allow us to visualize the buttons in the scene. However, we will be changing this text through the code later on in our Godot RPG tutorial.

Button Script

We can now implement some of the functionality for our button. The best way to do this will be to add a new script directly to the Button node. We can name this CombatActionButton, ensure it inherits from Button and press Create.

CombatActionButton

Inside this script, we can remove the _ready and _process functions as we won’t be needing them. Our first variable will be called combat_action and have a type of CombatAction. This will allow us to assign the button with a specific CombatAction resource, allowing our script to know the effects that should be applied when the button is pressed. Our buttons will be passed the combat action when it is the player’s turn, so we don’t need to give a default value.

var combat_action : CombatAction

Before handling the click events on our button, we first want to set up a function in our Character script that can be called when the button is clicked. This will be called cast_combat_action and have a parameter of action, which will be the combat_action value of the button that was pressed.

func cast_combat_action (action):
    pass

We can now handle button clicks inside the CombatActionButton script. To listen for the button being clicked, we can add a signal to the node. We will use the pressed signal for this.

pressed signal

Make sure to assign the pressed signal to the Button node before clicking Connect.

Button node Connect

This will generate a function called _on_pressed inside our button script, which will be automatically called whenever the button is pressed by the player. Here, we can call the cast_combat_action function on the current character.

func _on_pressed():
    get_node("/root/BattleScene").cur_char.cast_combat_action(combat_action)

This is a relatively long line, however, it can easily be broken down into multiple steps:

  • Firstly, we access the BattleScene root node using the get_node method.
  • Then, we get the cur_char variable from the TurnManager script attached to the BattleScene node. Because the player’s buttons are visible, we know this is our Player character instance.
  • Finally, we call the cast_combat_action function on the current character, with a parameter of combat_action.

We can now rename the Button node to CombatAction so that it can be identified easily.

rename Button node

From here, duplicate (CTRL+D) the node a few times to create four buttons in total. These will automatically resize and position into a list, because of the VBoxContainer parent node.

duplicate nodes

PlayerUI Script

We now need to create a new script to handle connecting the Player character instance to the individual buttons in our Godot RPG tutorial game. This script can be attached to the PlayerCombatActionsUI node and will be called PlayerUI.

player script

In this script, we won’t need the _process function, so this can be removed. After this, we can create a new variable called buttons, with a type of Array. This can be exported so that we can assign the buttons in the Inspector window.

@export var buttons : Array

This script will make use of our character_begin_turn and character_end_turn signals that we created previously. To do this, we will need to create two functions that can listen to these signals. The first will be called _on_character_begin_turn, with a parameter of character.

func _on_character_begin_turn(character):
    pass

We can then copy this function format for the on_character_end_turn function.

func _on_character_end_turn(character):
    pass

Next, we can connect these functions to the relevant TurnManager signals. This can be done by accessing the TurnManager script from our BattleScene root node and assigning the individual signals, inside the _ready function.

func _ready():
    get_node("/root/BattleScene/").character_begin_turn.connect(_on_character_begin_turn)
    get_node("/root/BattleScene/").character_end_turn.connect(_on_character_end_turn)

The next step is to toggle the PlayerCombatActionUI’s visibility based on whether it is the player’s turn or not. This can be done in the _on_character_begin_turn function.

func _on_character_begin_turn(character):
    visible = character.is_player

Here we assign the visible variable, supplied by Godot on the VBoxContainer node, to the is_player value of the currently active Character. This means that if it is our player’s turn, the UI will be visible, and if it is not, the UI will be hidden. To make sure the player can’t run multiple actions, we will also disable the visibility in the _on_character_end_turn function, no matter which character it is.

func _on_character_end_turn(character):
    visible = false

In the part of our Godot RPG tutorial, we will look at populating the buttons and giving them the correct information.

Player UI – Part 2

In this part, we are going to continue creating our player’s combat UI (and finish the UI for this Godot RPG tutorial).

Previously, we set up our buttons and the scripts required to make them work. The next stage is to implement the systems to pass the combat action information from the Player character to the buttons in the UI. We will begin by creating a new _display_combat_actions function in the PlayerUI script, with a parameter of combat_actions, which will be the list of combat actions to apply to the buttons.

func _display_combat_actions(combat_actions):

In this function, we can loop through each button in our buttons list using a for loop.

func _display_combat_actions(combat_actions):
    for i in len(buttons):

From here, we can grab the button node instance using the get_node function.

func _display_combat_actions(combat_actions):
    for i in len(buttons):
        var button = get_node(buttons[i])

We can now check to see whether we need to give this button a combat action. We can do this by checking whether is larger than the combat_actions array’s length. If the button is within the combat_actions length, we can make the button visible.

func _display_combat_actions(combat_actions):
    for i in len(buttons):
        ...
        if i < len(combat_actions):
            button.visible = true

Then, we can update the text property of the button based on the display_name value of the CombatAction at this index (i), along with assigning the combat_action property of the button.

func _display_combat_actions(combat_actions):
    for i in len(buttons):
        ...
        if i < len(combat_actions):
            ...
            button.text = combat_actions[i].display_name
            button.combat_action = combat_actions[i]

If the button is outside the bounds of the combat_actions array, we want to hide it.

func _display_combat_actions(combat_actions):
    for i in len(buttons):
        ...
        if i < len(combat_actions):
            ...
        else:
            button.visible = false

We want to call the _display_combat_actions function from the _on_character_begin_turn. However, we only want to do this when it is the player’s turn, as we don’t want the player to be able to choose the enemy’s actions.

func _on_character_begin_turn(character):
    ...
    if character.is_player:
        _display_combat_actions(character.combat_actions)

Finally, we also need to fix a small error in the previous lesson. In the _on_pressed function of the CombatActionButton script, we use a variable called cur_character. This should be cur_char to allow our buttons to work properly, well done if you caught this!

func _on_pressed():
    get_node("/root/BattleScene").cur_char.cast_combat_action(combat_action)

We now need to fill the buttons property of the PlayerCombatActionsUI in the Inspector window. For this Array, the element type (selected using the pencil icon) will be a Node Path.

Node Path

We can drag each CombatAction button node into this Array.

CombatAction button node

We also need to assign the Player Char and Enemy Char properties of the BattleScene node, inside the Inspector window.

Player Char and Enemy Char properties

We can now press the play button in the editor to try the game. With everything set up, you will see the buttons appear as expected.

play game

You can also try clicking the buttons. However, nothing will happen as we haven’t yet set up the ability to cast the combat actions, so we will cover this next in our Godot RPG tutorial.

Casting Combat Actions

The final step to finishing the player’s UI and our Godot RPG tutorial game is to set up the systems to handle our Characters casting combat actions.

We will expand the cast_combat_action function, which is called both by our enemy AI (that we will create in the next lesson), and the player’s onscreen buttons. The first step is to find out whether the action property is a combat action that causes damage or a combat action that heals the character. This can be done by checking if the damage property is more than zero.

func cast_combat_action(action):
    if action. Damage > 0:

If the damage property is more than zero, this means the action property is aimed at causing damage to our opponent. Luckily, we have already set up the take_damage function on our Character script, so we can simply call this function on the opponent variable.

func cast_combat_action(action):
    if action.damage > 0:
        opponent.take_damage(action.damage)

If the damage property is zero, but the heal property is more than zero, this means the action property is aimed at healing the current character. Because we want to heal the current character instance, we can simply call the heal function.

func cast_combat_action(action):
    if action.damage > 0:
        ...
    else if action.heal > 0:
        heal(action.heal)

With the combat action processed, we then want to end the character’s turn. To do this we want to call the end_current_turn function on the BattleScene root node.

func cast_combat_action(action):
    ...
    get_node("/root/BattleScene").end_current_turn()

Make sure to assign the Opponent variable for both the Player and the Enemy characters, if you haven’t previously.

Opponent variable

We should now be able to press play and test the combat actions. If you try pressing the Slash or Stomp buttons, you will see the damage is applied to the enemy and the UI disappears as the turn changes, just as expected.

 play and test

Currently, our Enemy character can’t fight back, but enemy AI is a lesson for another day and another Godot RPG tutorial.

Godot RPG Tutorial Wrap Up

This Godot RPG tutorial is done! You’ve successfully created an interactive combat system into a game, adding weight and strategy to your gameplay. Feel free to further expand the concepts taught here – add more combat actions, create stronger enemies, or introduce magic and skills. The scope is endless!

You might also want to experiment with different UI designs and information prompts to make gameplay even more immersive. Remember, gaming is as much about experience as it is about winning!

Happy gaming!

Want to learn more? Try our complete BUILD A MICRO TURN-BASED RPG WITH GODOT 4 course.

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.