In this tutorial we will introduce some core elements of RPGs:

  • Non-Player Character: an NPC is a character which is not controlled by the player. In video games, NPC behavior is usually scripted and automatic, triggered by certain actions or dialogue with the player.
  • Quests: a quest is a task that the player may complete in order to gain a reward. Rewards include experience points, items, in-game currency, access to new locations or any combination of the above. Quests can be divided into four categories (kill quests, gather quests, delivery quests, and escort quests), which can be combined together to create more elaborate missions.
  • Dialogues: conversing with NPCs is one of the main ways to guide the player through story-driven game like RPGs. By talking to NPCs, the player can learn more about the game world, get hints and receive new quests to complete.

For our game we will create an NPC named Fiona. Fiona has lost her necklace, and will ask the player to find it. As a reward, she will give to the player a health potion (the player will get some experience points too).

Downloading and organizing files

Download the sprites for this tutorial by clicking on the button below:

Download “SimpleRPG Non-Player Character” non_player_character.zip – 2 KB

Then, in the FileSystem panel, inside the Entities folder create two new folders: Fiona and Necklace.

Import the images starting with npc_ in the Fiona folder and the necklage.png image in the Necklace folder. Remember to reimport the images turning off Filter.

Dialogue tree

To speak to NPCs, most games use a branching dialogue system. This gameplay mechanic is called a dialogue tree.

In this dialogue system, the player choose what to say from a list of pre-written options and the NPC reply to the player. The cycle continues until the conversation ends. External factors, like player stats or inventory items, may influence the answers of the NPC or unlock new dialogue options.

Quests and dialogues can be implemented as a finite state machine. For Fiona’s quest, it’s like this:

This quest can be in any of these states (rounded boxes):

  • Not started: the player has not yet accepted the quest;
  • Started: the player has accepted the quest from Fiona, but has not yet returned the necklace to her;
  • Completed: the player returned the necklace to Fiona.

In each of these states, there is a dialogue tree that represents the possible conversation exchanges between Fiona and the player.

Each Fiona’s sentence (rectangular boxes) is numbered; we’ll use this number in Fiona’s script to track the progress of the conversation. Every possible answers from the player is represented by an arrow leading to Fiona’s next sentence (or at the end of the conversation).

Some answers (red arrows) cause the Quest to change status. When the quest reaches a new state, the dialogues of the previous states are no longer reachable.

Creating Non-Player Character scene

Create a new scene for the NPC. Choose Custom Node as root node for the scene and set it to a StaticBody2D node. We choose this type of node instead of KinematicBody2D because we need to detect collisions but we don’t need the NPC to move.

Rename the root node to Fiona and then go to the Node → Groups panel. Add a new group called NPCs:

Groups in Godot work like tags that can be used to organize nodes in a scene. In this tutorial we will have only one NPC, but if you want to create others, grouping them will allow you to reuse some of the code we’ll write without modification.

In the Inspector, set Node → Pause property to Process. In this way, Fiona‘s animations will continue to play even when the game is paused.

Add an AnimatedSprite node to Fiona. In the Inspector, set the Frames property to New SpriteFrames.

Then, click on the newly created SpriteFrames to open the SpriteFrames Editor. Create these animations:

NameFramesSpeed (FPS)Loop
idlenpc_idle_1.png
npc_idle_2.png
1On
talknpc_idle_1.png
npc_talk.png
8On

If you don’t remember how to use the SpriteFrames Editor, check out the 2D Sprite animation tutorial.

When you’re done creating this animations, in the Inspector set the Animation property to idle and turn on the Playing property.

Add a CollisionShape2D node to Fiona and in the Inspector, set its Shape property to New RectangleShape2D. Then, resize the rectangle to just cover Fiona‘s sprite.

Now, attach a new script to Fiona. Save it as Fiona.gd in the Entities/Fiona folder. Then, add this code to the script:

enum QuestStatus { NOT_STARTED, STARTED, COMPLETED }
var quest_status = QuestStatus.NOT_STARTED
var dialogue_state = 0
var necklace_found = false

In the first two lines we declare an enumeration (QuestStatus) which lists all the states of the quest and the variable quest_status to store the current state.

The dialogue_state variable is used to store the current state of the dialogue (the numbers in the boxes that we have previously seen in the dialogue tree).

Finally, the variable necklace_found stores whether the player has found Fiona’s necklace or not.

Let’s leave this script for now (we’ll complete it later when we have created the dialog popup) and save the scene as Fiona.tscn inside the Entities/Fiona folder.

Now go back to the main scene and drag the Fiona.tscn scene onto the Root node to add her to the scene. I recommend to place her close enough to the player’s starting position to make testing easier.

Creating the necklace scene

Create a new scene. Choose Custom Node as root node for the scene and set it to an Area2D node. We will use Area2D to create a pickable object as in the Picking and Using Items tutorial.

Rename the root node to Necklace and add a Sprite node to it. Then drag necklace.png to the Texture property in the Inspector.

Add a CollisionShape2D node to Necklace and in the Inspector, set its Shape property to New RectangleShape2D. Then, resize the rectangle to just cover the necklace’s sprite.

Now attach a new script to Necklace and save it as Necklace.gd in the Entities/Necklace folder.

We need a variable to store a reference to Fiona, so add this code to the script:

var fiona

func _ready():
	fiona = get_tree().root.get_node("Root/Fiona")

Then, we want to connect the body_entered() signal of Necklace to this script to remove it from the game (as if the player had picked up it) and to set the necklace_found variable of Fiona to true.

Select Necklace and go to the Node panel. Choose the body_entered() signal and press Connect… (or double click on the signal). Choose Necklace as the node to connect and press the Connect button. The Necklace.gd script will open and the method that will handle the signal will be added automatically to it. Write this code for the _on_Necklace_body_entered() method:

func _on_Necklace_body_entered(body):
	if body.name == "Player":
		get_tree().queue_delete(self)
		fiona.necklace_found = true

If the body that entered Area2D is Player, the necklace is removed from the scene tree and Fiona‘s necklace_found variable is set to true to store that it has been found.

Finally, save the scene as Necklace.tscn inside the Entities/Necklace folder.

Now, go back to the main scene and drag the Necklace.tscn scene onto the Root node. For now, I recommend to place it close enough to the player’s start position to easily test dialogues. At the end of the tutorial you can move it to the position you prefer.

Dialog popup

Now, we want to create the popup where the dialogue with the NPCs will take place. We want it to look like this:

In the upper part of the panel we want to show the name of the character we are talking to. In the central part we will show the NPC’s sentences while at the bottom there will be the answers to choose from.

To start creating the dialogue popup, add a Popup node to CanvasLayer and rename it DialoguePopup, then make it visible to show it in the editor.

Turn on DialoguePopup‘s Popup → Exclusive property and set Node → Pause property to Process, so we can pause the game while the player talks to an NPC.

Add a ColorRect node to DialoguePopup. In the Inspector, set its Rect → Position to 10, 115 and Rect → Size to 300, 55.

Add a Label node to ColorRect and rename it NPCName. In the Inspector, in CustomFont load Font/Font.tres. Then, set Custom Colors → Font Color to black. Finally, set Rect → Position to 5, 2 and Rect → Size to 290, 10.

Add another Label to ColorRect and rename it Dialogue. In the Inspector, in CustomFont load Font/Font.tres. Then, set Custom Colors → Font Color to (128,128,128,255). Set Autowrap to On and finally, set Rect → Position to 5, 15 and Rect → Size to 290, 25.

Finally, let’s add one last Label to ColorRect and rename it Answers. In the Inspector, in CustomFont load Font/Font.tres. Then, set Custom Colors → Font Color to black. Set Align to Center and finally, set Rect → Position to 5, 43 and Rect → Size to 290, 10.

Animating dialogue text

To make the dialogue popup more interesting and give a sense of the progression of the conversation, we can animate the text of the dialogue, making it appear bit by bit.

First, let’s add some text to the labels, so we can see a preview of the animation. Set the Text properties of NPCName, Dialogue and Answers to:

  • Fiona
  • Hello adventurer! I lost my necklace, can you find it for me?
  • [A] Yes [B] No

Now, add a node of type AnimationPlayer to DialoguePopup. The Animation panel will open at the bottom of the editor.

Click on the Animation button to create a new animation and call it ShowDialogue.

Click on Add Track and add a Property Track. We want to animate the Dialogue label, so select it and then select the percent_visible property.

Next, we need to create the animation keyframes. Right-click on the percent_visible track at the 0 second position, and choose Insert Key.

A new keyframe will be added. Repeat the operation at 0.8 second to add a second keyframe.

You can use the zoom slider in the bottom-right of the panel to change the time scale and make it easier for you to work. If you place a keyframe in the wrong second, you can move it by dragging it along the track.

Now select the keyframe at position 0 and in the Inspector set Value to 0.

Since we want to show the answers only after Dialogue‘s text has completely appeared, add a Property Track also for the visible property of Answers.

In this new track, insert two keyframes, at positions 0 and 1 second. Select the keyframe at position 0 and in the Inspector disable Value.

If you preview the animation in the editor, you will get this:

Dialogue popup script

Attach a new script to DialoguePopup and save it as DialoguePopup.gd in the GUI folder. This script will allow Fiona‘s script to modify the dialogue popup labels and will handle user input.

Let’s start by entering the variables we will use for the labels:

var npc_name setget name_set
var dialogue setget dialogue_set
var answers setget answers_set

func name_set(new_value):
	npc_name = new_value
	$ColorRect/NPCName.text = new_value

func dialogue_set(new_value):
	dialogue = new_value
	$ColorRect/Dialogue.text = new_value

func answers_set(new_value):
	answers = new_value
	$ColorRect/Answers.text = new_value

The declaration of these variables makes use of the setget keyword, used to define setter/getter functions (in our case we only define the setter function). Whenever the value of a variable is modified by an external source (i.e. not from local usage in the class), the setter function will be called. In our case, we use the setter to modify the Text property of the relative label. Getter are similar, but are called when we access a variable.

To start and end the dialogues we’ll need two functions. Let’s start from the function to open the dialogue popup:

func open():
	get_tree().paused = true
	popup()
	$AnimationPlayer.playback_speed = 60.0 / dialogue.length()
	$AnimationPlayer.play("ShowDialogue")

When called, the function pauses the game. After that, it calls the popup() function to show the dialogue popup (if it isn’t already visible on the screen). Then, AnimationPlayer‘s playback_speed variable is set to make the characters appear on the screen at the same rate regardless of the length of the text. Lastly, the ShowDialogue animation is played using the play() function.

The function to end the dialogue simply resumes the game and hides the popup:

func close():
	get_tree().paused = false
	hide()

In its default state (hidden), this panel should not receive input. So, in the _ready() function, we must call the set_process_input() function to disable input handling:

func _ready():
	set_process_input(false)

We want to enable input only when the ShowDialogue animation is finished. So, connect AnimationPlayer‘s animation_finished() signal to DialoguePopup. The function _on_AnimationPlayer_animation_finished() will be automatically Added to the script. Edit the function like this:

func _on_AnimationPlayer_animation_finished(anim_name):
	set_process_input(true)

To reply to the NPC, we’ll need a variable to store a reference to it, so that we can send it the player’s answer:

var npc

Finally, write the function that handles the player’s input:

func _input(event):
	if event is InputEventKey:
		if event.scancode == KEY_A:
			set_process_input(false)
			npc.talk("A")
		elif event.scancode == KEY_B:
			set_process_input(false)
			npc.talk("B")

This function checks that the detected input is a keyboard event (InputEventKey). If one of the valid keys (A or B) is pressed, the input is disabled and the player’s choice is sent to the NPC via the talk() function (which we will write in the next section).

NPC script

Now that we have completed the dialogue popup, we can start implementing the dialogue tree in the NPC script.

Open the Fiona.gd script. Let’s start by adding to it two variables that will contain the references to DialoguePopup and Player:

var dialoguePopup
var player

func _ready():
	dialoguePopup = get_tree().root.get_node("Root/CanvasLayer/DialoguePopup")
	player = get_tree().root.get_node("Root/Player")

Since the player will be rewarded with a potion, for convenience copy the enumeration Potion into this script:

enum Potion { HEALTH, MANA }

Now add the talk() function. This function will allow the player to start a conversation and answer to NPCs:

func talk(answer = ""):
	# Set Fiona's animation to "talk"
	$AnimatedSprite.play("talk")
	
	# Set dialoguePopup npc to Fiona
	dialoguePopup.npc = self
	dialoguePopup.npc_name = "Fiona"
	
	# Show the current dialogue
	match quest_status:
		QuestStatus.NOT_STARTED:
			match dialogue_state:
				0:
					# Update dialogue tree state
					dialogue_state = 1
					# Show dialogue popup
					dialoguePopup.dialogue = "Hello adventurer! I lost my necklace, can you find it for me?"
					dialoguePopup.answers = "[A] Yes  [B] No"
					dialoguePopup.open()
				1:
					match answer:
						"A":
							# Update dialogue tree state
							dialogue_state = 2
							# Show dialogue popup
							dialoguePopup.dialogue = "Thank you!"
							dialoguePopup.answers = "[A] Bye"
							dialoguePopup.open()
						"B":
							# Update dialogue tree state
							dialogue_state = 3
							# Show dialogue popup
							dialoguePopup.dialogue = "If you change your mind, you'll find me here."
							dialoguePopup.answers = "[A] Bye"
							dialoguePopup.open()
				2:
					# Update dialogue tree state
					dialogue_state = 0
					quest_status = QuestStatus.STARTED
					# Close dialogue popup
					dialoguePopup.close()
					# Set Fiona's animation to "idle"
					$AnimatedSprite.play("idle")
				3:
					# Update dialogue tree state
					dialogue_state = 0
					# Close dialogue popup
					dialoguePopup.close()
					# Set Fiona's animation to "idle"
					$AnimatedSprite.play("idle")
		QuestStatus.STARTED:
			match dialogue_state:
				0:
					# Update dialogue tree state
					dialogue_state = 1
					# Show dialogue popup
					dialoguePopup.dialogue = "Did you find my necklace?"
					if necklace_found:
						dialoguePopup.answers = "[A] Yes  [B] No"
					else:
						dialoguePopup.answers = "[A] No"
					dialoguePopup.open()
				1:
					if necklace_found and answer == "A":
						# Update dialogue tree state
						dialogue_state = 2
						# Show dialogue popup
						dialoguePopup.dialogue = "You're my hero! Please take this potion as a sign of my gratitude!"
						dialoguePopup.answers = "[A] Thanks"
						dialoguePopup.open()
					else:
						# Update dialogue tree state
						dialogue_state = 3
						# Show dialogue popup
						dialoguePopup.dialogue = "Please, find it!"
						dialoguePopup.answers = "[A] I will!"
						dialoguePopup.open()
				2:
					# Update dialogue tree state
					dialogue_state = 0
					quest_status = QuestStatus.COMPLETED
					# Close dialogue popup
					dialoguePopup.close()
					# Set Fiona's animation to "idle"
					$AnimatedSprite.play("idle")
					# Add potion and XP to the player. 
					yield(get_tree().create_timer(0.5), "timeout") #I added a little delay in case the level advancement panel appears.
					player.add_potion(Potion.HEALTH)
					player.add_xp(50)
				3:
					# Update dialogue tree state
					dialogue_state = 0
					# Close dialogue popup
					dialoguePopup.close()
					# Set Fiona's animation to "idle"
					$AnimatedSprite.play("idle")
		QuestStatus.COMPLETED:
			match dialogue_state:
				0:
					# Update dialogue tree state
					dialogue_state = 1
					# Show dialogue popup
					dialoguePopup.dialogue = "Thanks again for your help!"
					dialoguePopup.answers = "[A] Bye"
					dialoguePopup.open()
				1:
					# Update dialogue tree state
					dialogue_state = 0
					# Close dialogue popup
					dialoguePopup.close()
					# Set Fiona's animation to "idle"
					$AnimatedSprite.play("idle")

The talk() function:

  • play Fiona‘s talk animation when starting a conversation and the idle animation when the conversation ends.
  • setup the dialogue popup with Fiona‘s name and reference.
  • implements the status changes in the dialogue tree, based on the status of the quest, the player’s answers and whether the player has found the necklace or not
  • show the current line of dialogue in the dialogue popup.
  • rewards the player when the quest is completed.

This function makes heavy use of the match statement. The match statement is used to branch execution of a program. It’s the equivalent of the switch statement found in many other languages. It has the following syntax:

match [expression]:
    [pattern](s):
        [block]
    [pattern](s):
        [block]
    [pattern](s):
        [block]

The expression after the match keyword is compared to the patterns in the following lines. If a pattern matches, the corresponding block will be executed. After that, the execution continues below the match statement.

Player script

The last thing to do is to insert in Player‘s script the code to start a conversation when you press the attack key (the space bar or the relative joypad key). So, in the _input() function, edit the code that handle the attack input event like this:

if event.is_action_pressed("attack"):
	# Check if player can attack
	var now = OS.get_ticks_msec()
	if now >= next_attack_time:
		# What's the target?
		var target = $RayCast2D.get_collider()
		if target != null:
			if target.name.find("Skeleton") >= 0:
				# Skeleton hit!
				target.hit(attack_damage)
			# NEW CODE - START
			if target.is_in_group("NPCs"):
				# Talk to NPC
				target.talk()
				return
			# NEW CODE - END
		# Play attack animation
		attack_playing = true
		var animation = get_animation_direction(last_direction) + "_attack"
		$Sprite.play(animation)
		# Add cooldown time to current time
		next_attack_time = now + attack_cooldown_time

If the “attack” target is part of the NPCs group, its talk() function is called to start the dialogue.

Testing quest and dialogues

Try running the game and check that all dialogue options are working properly.

When you have checked that everything works, move the necklace to its final position (I recommend placing it on the other side of the map).

Conclusions

The quest and dialogue system we have created for our game is a good starting point to understand how such a system works. However, there are many things that can be improved. For example:

  • All dialogues are embedded in the NPC script. It’s desirable that dialogues are written in a easily-editable human-readable external file that is loaded at Runtime.
  • The quest code is inside the NPC. The status of a quest may depend on multiple NPCs and many other factors, so it is preferable to have some kind of “centralized” system that manages quests.
  • The NPC is static and only reacts to player input. In many games, NPCs have their own movement and actions routine, which requires implementing a more complex scripting system.
  • At presents, monsters ignore NPCs and viceversa.

In addition to having learned how to implement the quest and dialogue system, in this tutorial we learned a few other new things:

  • how to animate Label’s text in an interesting way;
  • what groups are and how to use them;
  • what are setters/getters;
  • how to use the match statement.

You can try what we’ve done so far by clicking here:

In the next post we will see how to implement sounds and music in the game.


8 Comments

James · February 5, 2020 at 1:05 pm

Wow!
This is so great Davide Pesce. I’m loving these tutorials.
I’m always learning something new in each and every tutorial I’m going through. Keep it up.
It is becoming more interesting stage after stage. However I’m having some problems switching scenes. Like for example we created the necklace in a different scene so did we with NPC. The main problem comes when you said we connect the scene to our main scene etc. I actually don’t know if I’m supposed to drag it or copy it somehow.

Once again thank you for these tutorials they are really helping me.
I’ll be glad to hear from you as soon as possible.

    Davide Pesce · February 5, 2020 at 8:28 pm

    Hi James!

    To instantiate the necklace, drag the Necklace.tscn file from the FileSystem panel and drop it on the Root node of the main scene. The necklace is added as the last child of Root at position 0,0 (then you can move it). Alternatively, first select the Root node of the main scene and then drag the Necklace.tscn file on the map where you want to place it.

      James · February 6, 2020 at 11:35 am

      Hey Davide Pesce!

      I actually figured it out through your comment. It is really helpful doing it this way. Now i’m just waiting for more quest and challenges.

      Thanks.

James · February 5, 2020 at 1:40 pm

I Hope soon you will add a soundtrack or some type of music. : )

    Davide Pesce · February 5, 2020 at 8:29 pm

    Next tutorial! 😉

      James · February 6, 2020 at 11:43 am

      I can’t wait for that!
      >MORE IDEAS
      . more quest of course
      . Options for choosing languages( if players comments are written in English and French)
      . Lol maybe a little cottage too 🙂
      Just some ideas for the game if they can be any help to make the game more exciting or if not , any links you have that can help me to gain that knowledge.

      Thank You!

        James · February 6, 2020 at 11:52 am

        Almost forgot one thing. What if the player gets missions to protect other players from the skeletons.

        Davide Pesce · February 6, 2020 at 7:30 pm

        These are all good suggestions, I’ll think about it and see what I can do!

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