DEV Community

Cover image for Let’s Learn Godot 4 by Making an RPG — Part 17: Basic NPC & Quest🤠
christine
christine

Posted on • Updated on

Let’s Learn Godot 4 by Making an RPG — Part 17: Basic NPC & Quest🤠

Now that we have most of our game set up, we can go ahead and create an NPC with a Quest. Please note that this quest system will not be a dynamic system with quest chains. No, unfortunately, this quest system only contains a simple collection quest with a dialog and state tree. I left it to be this simple system because a dialog, quest, and even inventory system is quite complex, and therefore I will make a separate tutorial series that will focus on those advanced concepts in isolation!


WHAT YOU WILL LEARN IN THIS PART:

· How to work with game states.
· How to add dialog trees.
· Further practice with popups and animations.
· How to work with the InputEventKey object.
· How to add Groups to nodes.
· How to work with the RayCast2D node.


If you are curious about how these systems might work, here are some written resources that could give you an idea of how to implement this:

NPC SETUP

Let’s create our NPC scene. In your project, create a new scene with a CharacterBody2D node as the scene root, and an AnimatedSprite2D and CollisionBody2D node as its children. Make the collision shape for your collision node to be a RectangleShape. Save this scene underneath your Scenes folder.

Godot RPG

Let’s add two new animations to the AnimatedSprite2D node: idle_down and talk_down. You can find the animations spritesheet that I used for this node underneath Assets > Mobs > Coyote.

idle_down:

Godot RPG

Godot RPG

talk_down:

Godot RPG

Godot RPG

For now, our NPC will be anchored in one place, and they won’t attack our enemies. Let me know if you want me to show you how to make them roam around the map and attack enemies and vice versa. Leave the FPS and Looping as their default values.

Please make sure that your sprite is in the middle of your CollisionShape.

Godot RPG

Attach a script to your NPC scene and save it underneath your Scripts folder.

Godot RPG

We also want to add a Group to this node so that our players can check for the special “NPC” group when they interact with it. We will have to also add a RayCast2D node to our player so that we can check its “target”, and if that target is part of the NPC group, we’ll launch the function to talk to the NPC.

Godot RPG

Godot RPG

DIALOG POPUP & PLAYER SETUP

Now, in your Player scene, we need to create another popup node that will be made visible when the player interacts with the NPC. Add a new CanvasLayer node and rename it to “DialogPopup”.

Godot RPG

In this DialogPopup node, let’s add a ColorRect with three Label nodes as its children. Rename it as follows:

Godot RPG

Godot RPG

Select your Dialog node and change its Color property to #581929. Change its size (x: 310, y: 70), and its anchor_preset (center_bottom).

Godot RPG

Select your NPC Label node and change its text to “Name”. Change its size (x: 290, y: 26); position (x: 5, y: 2).

Godot RPG

Now change its font to “Scrondinger”, and its font size to 10. We also want to change its font color, so underneath Color > Font Color, change the color to #B26245.

Godot RPG

Select your Message Label node and change its text to “Text here…”. Change its size (x: 290, y: 30); position (x: 5, y: 15). Also change its font to “Scrondinger”, and its font size to 10. The AutoWrap property should also be set to “Word”.

Godot RPG

Select your Response Label node and change its text to “Answer”. Change its size (x: 290, y: 16); position (x: 5, y: 60); horizontal and vertical alignment (center). Also, change its font to “Scrondinger”, and its font size to 10. Underneath Color > Font Color, change the color to #D6c376.

Godot RPG

Change the DialogPopup’s visibility to be hidden and add a RayCast2D node before your UI layer.

Godot RPG

In your Player script, we have to do the same as we did in our Enemy script when we set the direction of our raycast node to be the same as the direction of our character. In your _physics_process() function, let’s turn the RayCast2D node to follow our player’s movement direction.

    ### Player.gd

    extends CharacterBody2D

    # Node references
    @onready var animation_sprite = $AnimatedSprite2D
    @onready var health_bar = $UI/HealthBar
    @onready var stamina_bar = $UI/StaminaBar
    @onready var ammo_amount = $UI/AmmoAmount
    @onready var stamina_amount = $UI/StaminaAmount
    @onready var health_amount = $UI/HealthAmount
    @onready var xp_amount = $UI/XP
    @onready var level_amount = $UI/Level
    @onready var animation_player = $AnimationPlayer
    @onready var level_popup = $UI/LevelPopup
    @onready var ray_cast = $RayCast2D

    # --------------------------------- Movement & Animations -----------------------------------
    func _physics_process(delta):
        # Get player input (left, right, up/down)
        var direction: Vector2
        direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
        direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
        # Normalize movement
        if abs(direction.x) == 1 and abs(direction.y) == 1:
            direction = direction.normalized()
        # Sprinting             
        if Input.is_action_pressed("ui_sprint"):
            if stamina >= 25:
                speed = 100
                stamina = stamina - 5
                stamina_updated.emit(stamina, max_stamina)
        elif Input.is_action_just_released("ui_sprint"):
            speed = 50  
        # Apply movement if the player is not attacking
        var movement = speed * direction * delta
        if is_attacking == false:
            move_and_collide(movement)
            player_animations(direction)
        # If no input is pressed, idle
        if !Input.is_anything_pressed():
            if is_attacking == false:
                animation  = "idle_" + returned_direction(new_direction)    
        # Turn RayCast2D toward movement direction  
        if direction != Vector2.ZERO:
            ray_cast.target_position = direction.normalized() * 50
Enter fullscreen mode Exit fullscreen mode

This raycast will hit any node that has a collision body assigned to it. We want it to hit our NPC, and if it does and we press our interaction button, it will launch the dialog popup and run the dialog tree. For this, we need to first add a new input action that we can press to interact with NPCs.

In your Input Actions menu in your Project Settings, add a new input called “ui_interact” and assign a key to it. I assigned the TAB key on my keyboard to this action.

Godot RPG

Now, in our input() function, we will expand on it to add an input event for our ui_interact action. If our player presses this button, our raycast will capture the colliders it’s hitting, and if one of those colliders is part of the “NPC” group, it will launch the NPC dialog. We’ve done something similar to this in our Enemy scene.


    ### Player.gd

    func _input(event):
        #input event for our attacking, i.e. our shooting
        if event.is_action_pressed("ui_attack"):
            #checks the current time as the amount of time passed in milliseconds since the engine started
            var now = Time.get_ticks_msec()
            #check if player can shoot if the reload time has passed and we have ammo
            if now >= bullet_fired_time and ammo_pickup > 0:
                #shooting anim
                is_attacking = true
                var animation  = "attack_" + returned_direction(new_direction)
                animation_sprite.play(animation)
                #bullet fired time to current time
                bullet_fired_time = now + bullet_reload_time
                #reduce and signal ammo change
                ammo_pickup = ammo_pickup - 1
                ammo_pickups_updated.emit(ammo_pickup)
        #using health consumables
        elif event.is_action_pressed("ui_consume_health"):
            if health > 0 && health_pickup > 0:
                health_pickup = health_pickup - 1
                health = min(health + 50, max_health)
                health_updated.emit(health, max_health)
                health_pickups_updated.emit(health_pickup) 
        #using stamina consumables      
        elif event.is_action_pressed("ui_consume_stamina"):
            if stamina > 0 && stamina_pickup > 0:
                stamina_pickup = stamina_pickup - 1
                stamina = min(stamina + 50, max_stamina)
                stamina_updated.emit(stamina, max_stamina)      
                stamina_pickups_updated.emit(stamina_pickup)
        #interact with world        
        elif event.is_action_pressed("ui_interact"):
            var target = ray_cast.get_collider()
            if target != null:
                if target.is_in_group("NPC"):
                    # Talk to NPC
                    #todo: add dialog function to npc
                    return
Enter fullscreen mode Exit fullscreen mode

Next up we want our NPC script to update the DialogPopup node in our Player scene’s labels (npc, message, and response). We also want it to handle the player’s input for the dialog’s response, so if the player says “A” or “B” for yes/no the dialog popup will update the message values according to the dialog tree. We’ll get more into this when we set up our dialog tree later on.

Attach a new script to your DialogPopup node and save it underneath your GUI folder.

Godot RPG

Sometimes, you want a class’s member variable to do more than just hold data and perform some validation or computation whenever its value changes. It may also be desired to encapsulate its access in some way. For this, GDScript provides a special syntax to define properties using the set and get keywords after a variable declaration. Then you can define a code block that will be executed when the variable is accessed or assigned.

At the top of this script, we need to declare a few variables. We will define these variables using our set/get properties, which will allow us to set our Label values from the data that we get from our NPC script. We also need to define a variable for our NPC reference.

    ### DialogPopup.gd

    extends CanvasLayer

    #gets the values of our npc from our NPC scene and sets it in the label values
    var npc_name : set = npc_name_set
    var message: set = message_set
    var response: set = response_set

    #reference to NPC
    var npc
Enter fullscreen mode Exit fullscreen mode

Next, we need to create three functions to set the values of our set/get variables. This will capture the value passed from our NPC and assign the value to our Labels.

    ### DialogPopup.gd

    extends CanvasLayer

    #gets the values of our npc from our NPC scene and sets it in the label values
    var npc_name : set = npc_name_set
    var message: set = message_set
    var response: set = response_set

    #reference to NPC
    var npc

    #sets the npc name with the value received from NPC
    func npc_name_set(new_value):
        npc_name = new_value
        $Dialog/NPC.text = new_value

    #sets the message with the value received from NPC
    func message_set(new_value):
        message = new_value
        $Dialog/Message.text = new_value

    #sets the response with the value received from NPC
    func response_set(new_value):
        response = new_value
        $Dialog/Response.text = new_value
Enter fullscreen mode Exit fullscreen mode

Before we continue with this script, let’s add an animation to our Message node so that it has a typewriter effect. This is a classic feature in RPG-type games, so we will also implement this. In Godot 4, we can do this by changing our text’s visible ratio in the AnimationPlayer. This will transition our text visibility slowly from invisible to visible.

In your AnimationPlayer node in your Player scene, add a new animation called “typewriter”.

Godot RPG

Add a new Property Track to your animation and assign it to your Message node.

Godot RPG

The property we want to change is the visible ratio of our dialog text.

Godot RPG

This visible ratio will make the dialog ratio visible from 0 to 1. In your visible_ratio track, add two new keys, one at keyframe 0 and the other one at keyframe 1.

Godot RPG

Change the Value of your keyframe 0 to 0, and the Value of your keyframe 1 to 1.

Godot RPG

Godot RPG

Now if you play your animation, your typewriter effect should work!

Godot RPG

Back in your DialogPopup code, let’s create two functions that will be called by our NPC script. The first function should pause the game and show the dialog popup, and play the typewriter animation. The other function should hide the dialog and unpause the game.

    ### DialogPopup.gd

    extends CanvasLayer

    # Node refs
    @onready var animation_player = $"../../AnimationPlayer"

    #gets the values of our npc from our NPC scene and sets it in the label values
    var npc_name : set = npc_name_set
    var message: set = message_set
    var response: set = response_set

    #reference to NPC
    var npc

    #sets the npc name with the value received from NPC
    func npc_name_set(new_value):
        npc_name = new_value
        $Dialog/NPC.text = new_value

    #sets the message with the value received from NPC
    func message_set(new_value):
        message = new_value
        $Dialog/Message.text = new_value

    #sets the response with the value received from NPC
    func response_set(new_value):
        response = new_value
        $Dialog/Response.text = new_value

    #opens the dialog
    func open():
        get_tree().paused = true
        self.visible = true
        animation_player.play("typewriter")

    #closes the dialog  
    func close():
        get_tree().paused = false
        self.visible = false
Enter fullscreen mode Exit fullscreen mode

If this node is hidden and if the message text has not been completed yet, this node should not receive input. So, in the _ready() function, we must call the set_process_input() function to disable input handling. This will disable the input function and Input singleton in our Player script from processing any input.

    ### DialogPopup.gd

    # older code

    # ------------------- Processing ---------------------------------
    #no input on hidden
    func _ready():
        set_process_input(false)

    #opens the dialog
    func open():
        get_tree().paused = true
        self.visible = true
        animation_player.play("typewriter")

    #closes the dialog  
    func close():
        get_tree().paused = false
        self.visible = false
Enter fullscreen mode Exit fullscreen mode

We only want the player to be able to insert inputs if our “typewriter” animation has finished animating our message text. Therefore, we can connect the AnimationPlayer node’s animation_finished() signal to our DialogPopup script.

Godot RPG

    ### DialogPopup.gd

    # older code

    # ------------------- Processing ---------------------------------
    #no input on hidden
    func _ready():
        set_process_input(false)

    #opens the dialog
    func open():
        get_tree().paused = true
        self.visible = true
        animation_player.play("typewriter")

    #closes the dialog  
    func close():
        get_tree().paused = false
        self.visible = false

    #input after animation plays
    func _on_animation_player_animation_finished(anim_name):
        set_process_input(true)
Enter fullscreen mode Exit fullscreen mode

Finally, we can write our code that will accept the player’s input in response to our dialog options. We don’t have to create unique input actions for this, as we can just use the InputEventKey object. This object stores key presses on the keyboard. So if we press “A” or “B”, the object will capture those keys as input and trigger the dialog tree to respond to these inputs.

Finally, we can write our code that will accept the player’s input in response to our dialog options. We don’t have to create unique input actions for this, as we can just use the InputEventKey object. This object stores key presses on the keyboard. So if we press “A” or “B”, the object will capture those keys as input and trigger the dialog tree to respond to these inputs.

Here’s a visual representation to help you understand what we want to achieve:

Godot RPG

    ### DialogPopup.gd

    #older code

    # ------------------- Dialog -------------------------------------
    func _input(event):
        if event is InputEventKey:
            if event.is_pressed():
                if event.keycode == KEY_A:  
                    #todo: add dialog function to npc
                    return
                elif event.keycode == KEY_B:
                    #todo: add dialog function to npc
                    return
Enter fullscreen mode Exit fullscreen mode

NPC Dialog Tree


What is a Dialog Tree?
A dialog tree, often referred to as a conversation tree or branching dialog, is a form of interactive narrative. It represents a series of branching choices in character dialogues or interactions, allowing players or users to navigate through various conversation paths based on their choices.


We’ll come back to this _input function once we have the dialog function in our NPC script. We’re going to start working on that now, so let’s open up our NPC script and define some variables that will store our quest and dialog states. We’ll create an enum that will hold the states of our Quest — which is when it’s not started, started, and completed. then we will instance that enum to set its initial state to be NOT_STARTED. We then need to store the state of our dialog as an integer.

We’ll define our dialog state as an integer so that we can increment it throughout our dialog tree, and then we can select our dialog based on our number value in a match statement. A match statement is used to branch the execution of a program. It’s the equivalent of the switch statement found in many other languages, and it works in the way that an expression is compared to a pattern, and if that pattern matches, the corresponding block will be executed. After that, the execution continues below the match statement.

Our dialog and quest states will be executed in this match-case pattern:

    match (quest_status):
     NOT_STARTED: 
      match (dialog_state):
       0:
        //dialog message
        //increase dialog state
        match (answer):
         //dialog message
         //increase dialog state
       1:
        //dialog message
        //increase dialog state
        match (answer):
         //dialog message
         //increase dialog state
        STARTED: 
      match (dialog_state):
       0:
        //dialog message
        //increase dialog state
        match (answer):
         //dialog message
         //increase dialog state
       1:
        //dialog message
        //increase dialog state
        match (answer):
         //dialog message
         //increase dialog state
        COMPLETED: 
      match (dialog_state):
       0:
        //dialog message
        //increase dialog state
        match (answer):
         //dialog message
Enter fullscreen mode Exit fullscreen mode

Let’s define our variables.

    ### NPC

    extends CharacterBody2D

    #quest and dialog states
    enum QuestStatus { NOT_STARTED, STARTED, COMPLETED }
    var quest_status = QuestStatus.NOT_STARTED
    var dialog_state = 0
    var quest_complete = false
Enter fullscreen mode Exit fullscreen mode

We also need to define some variables that will reference our DialogPopup node in our Player scene, as well as our Player itself — because our player is the one that will initiate the interaction with our NPC.

    ### NPC

    extends CharacterBody2D

    # Node refs
    @onready var dialog_popup = get_tree().root.get_node("Main/Player/UI/DialogPopup")
    @onready var player = get_tree().root.get_node("Main/Player")
    @onready var animation_sprite = $AnimatedSprite2D

    #quest and dialog states
    enum QuestStatus { NOT_STARTED, STARTED, COMPLETED }
    var quest_status = QuestStatus.NOT_STARTED
    var dialog_state = 0
    var quest_complete = false
Enter fullscreen mode Exit fullscreen mode

I also want us to type in the NPC’s name in the Inspector panel, instead of giving them a constant value like “Joe”. To do this, we can export our variable.

    ### NPC

    extends CharacterBody2D

    # Node refs
    @onready var dialog_popup = get_tree().root.get_node("Main/Player/UI/DialogPopup")
    @onready var player = get_tree().root.get_node("Main/Player")
    @onready var animation_sprite = $AnimatedSprite2D

    #quest and dialog states
    enum QuestStatus { NOT_STARTED, STARTED, COMPLETED }
    var quest_status = QuestStatus.NOT_STARTED
    var dialog_state = 0
    var quest_complete = false

    #npc name
    @export var npc_name = ""
Enter fullscreen mode Exit fullscreen mode

Godot RPG

In our ready() function we need to set the default animation of our NPC to be “idle_down”.

    ### NPC
    extends CharacterBody2D

    # Node refs
    @onready var dialog_popup = get_tree().root.get_node("Main/Player/UI/DialogPopup")
    @onready var player = get_tree().root.get_node("Main/Player")
    @onready var animation_sprite = $AnimatedSprite2D

    #quest and dialog states
    enum QuestStatus { NOT_STARTED, STARTED, COMPLETED }
    var quest_status = QuestStatus.NOT_STARTED
    var dialog_state = 0
    var quest_complete = false

    #npc name
    @export var npc_name = ""

    #initialize variables
    func _ready():
        animation_sprite.play("idle_down")
Enter fullscreen mode Exit fullscreen mode

Now we can create our dialog() function. This dialog tree is quite long, so I recommend you go ahead and just copy and paste it from below. A dialog tree, or conversation tree, is a gameplay mechanic that runs when a player character interacts with an NPC. In this tree, the player is given a choice of what to say and makes subsequent choices until the conversation ends.

In our dialog tree, our NPC runs our Player through a quest to go and find a recipe book. We have not yet created this recipe book, or quest item, which will call our NPC to notify them that the quest has been completed. If we haven’t gotten this quest item yet, the NPC will remind us to get it. If we’ve gotten this quest item, the NPC will thank us and reward us, and complete the quest.

    ### NPC
    extends CharacterBody2D

    # Node refs
    @onready var dialog_popup = get_tree().root.get_node("Main/Player/UI/DialogPopup")
    @onready var player = get_tree().root.get_node("Main/Player")
    @onready var animation_sprite = $AnimatedSprite2D

    #quest and dialog states
    enum QuestStatus { NOT_STARTED, STARTED, COMPLETED }
    var quest_status = QuestStatus.NOT_STARTED
    var dialog_state = 0
    var quest_complete = false

    #npc name
    @export var npc_name = ""

    #initialize variables
    func _ready():
        animation_sprite.play("idle_down")

    #dialog tree    
    func dialog(response = ""):
        # Set our NPC's animation to "talk"
        animation_sprite.play("talk_down")   
        # Set dialog_popup npc to be referencing this npc
        dialog_popup.npc = self
        dialog_popup.npc_name = str(npc_name)   
        # dialog tree
        match quest_status:
            QuestStatus.NOT_STARTED:
                match dialog_state:
                    0:
                        # Update dialog tree state
                        dialog_state = 1
                        # Show dialog popup
                        dialog_popup.message = "Howdy Partner. I haven't seen anybody round these parts in quite a while. That reminds me, I recently lost my momma's secret recipe book, can you help me find it?"
                        dialog_popup.response = "[A] Yes  [B] No"
                        dialog_popup.open() #re-open to show next dialog
                    1:
                        match response:
                            "A":
                                # Update dialog tree state
                                dialog_state = 2
                                # Show dialog popup
                                dialog_popup.message = "That's mighty kind of you, thanks."
                                dialog_popup.response = "[A] Bye"
                                dialog_popup.open() #re-open to show next dialog
                            "B":
                                # Update dialog tree state
                                dialog_state = 3
                                # Show dialog popup
                                dialog_popup.message = "Well, I'll be waiting like a tumbleweed 'till you come back."
                                dialog_popup.response = "[A] Bye"
                                dialog_popup.open() #re-open to show next dialog
                    2:
                        # Update dialog tree state
                        dialog_state = 0
                        quest_status = QuestStatus.STARTED
                        # Close dialog popup
                        dialog_popup.close()
                        # Set NPC's animation back to "idle"
                        animation_sprite.play("idle_down")
                    3:
                        # Update dialog tree state
                        dialog_state = 0
                        # Close dialog popup
                        dialog_popup.close()
                        # Set NPC's animation back to "idle"
                        animation_sprite.play("idle_down")
            QuestStatus.STARTED:
                match dialog_state:
                    0:
                        # Update dialog tree state
                        dialog_state = 1
                        # Show dialog popup
                        dialog_popup.message = "Found that book yet?"
                        if quest_complete:
                            dialog_popup.response = "[A] Yes  [B] No"
                        else:
                            dialog_popup.response = "[A] No"
                        dialog_popup.open()
                    1:
                        if quest_complete and response == "A":
                            # Update dialog tree state
                            dialog_state = 2
                            # Show dialog popup
                            dialog_popup.message = "Yeehaw! Now I can make cat-eye soup. Here, take this."
                            dialog_popup.response = "[A] Bye"
                            dialog_popup.open()
                        else:
                            # Update dialog tree state
                            dialog_state = 3
                            # Show dialog popup
                            dialog_popup.message = "I'm so hungry, please hurry..."
                            dialog_popup.response = "[A] Bye"
                            dialog_popup.open()
                    2:
                        # Update dialog tree state
                        dialog_state = 0
                        quest_status = QuestStatus.COMPLETED
                        # Close dialog popup
                        dialog_popup.close()
                        # Set NPC's animation back to "idle"
                        animation_sprite.play("idle_down")
                        # Add pickups and XP to the player. 
                        player.add_pickup(Global.Pickups.AMMO)
                        player.update_xp(50)
                    3:
                        # Update dialog tree state
                        dialog_state = 0
                        # Close dialog popup
                        dialog_popup.close()
                        # Set NPC's animation back to "idle"
                        animation_sprite.play("idle_down")
            QuestStatus.COMPLETED:
                match dialog_state:
                    0:
                        # Update dialog tree state
                        dialog_state = 1
                        # Show dialog popup
                        dialog_popup.message = "Nice seeing you again partner!"
                        dialog_popup.response = "[A] Bye"
                        dialog_popup.open()
                    1:
                        # Update dialog tree state
                        dialog_state = 0
                        # Close dialog popup
                        dialog_popup.close()
                        # Set NPC's animation back to "idle"
                        animation_sprite.play("idle_down")
Enter fullscreen mode Exit fullscreen mode

Figure 1: Visual Representation of our Dialog Tree.

Figure 1: Visual Representation of our Dialog Tree.

QUEST ITEM SETUP

For this quest item, we can duplicate our Pickups scene and rename it to “QuestItem”. Detach the Pickups script and signal from the newly created scene and rename its root to “QuestItem”. Delete the script and signal from the root node.

Godot RPG

Change its sprite to be anything you want. Since our NPC is looking for a recipe book, I’m going to change my sprite to “book_02d.png”, which can be found under the Assets > Icons directory.

Godot RPG

Attach a new script to the root of this scene and save it underneath your Scripts folder. Also, connect its body_entered() signal to this new script.

Godot RPG

Godot RPG

In this script, we need to get a reference to our NPC scene which we will instance in our Main scene. From this, we’ll need to see if the Player has entered the body of this scene, and if true, we can call our to our NPC to change its quest status to complete — since we’ve found the requirements to complete the quest.

    ### QuestItem.gd

    extends Area2D

    #npc node reference
    @onready var npc = get_tree().root.get_node("Main/SpawnedNPC/NPC")

    #if the player enters the collision body, destroy item and update quest
    func _on_body_entered(body):
        if body.name == "Player":
            print("Quest item obtained!")
            get_tree().queue_delete(self)
            npc.quest_complete = true
Enter fullscreen mode Exit fullscreen mode

Let’s instance our NPC and our Quest Item in our Main scene underneath two new nodes called SpawnedQuestItems and SpawnedNPC (both should be Node2D nodes).

Godot RPG

Godot RPG

Here is an example of my map’s layout for my quest:

Godot RPG

Now we need to go back to our Player scene so that we can update our interact input to call the dialog function in our NPC script.

    ### Player.gd

    # older code

    func _input(event):
        #input event for our attacking, i.e. our shooting
        if event.is_action_pressed("ui_attack"):
            #checks the current time as the amount of time passed 
            var now = Time.get_ticks_msec()
            #check if player can shoot if the reload time has passed and we have ammo
            if now >= bullet_fired_time and ammo_pickup > 0:
                #shooting anim
                is_attacking = true
                var animation  = "attack_" + returned_direction(new_direction)
                animation_sprite.play(animation)
                #bullet fired time to current time
                bullet_fired_time = now + bullet_reload_time
                #reduce and signal ammo change
                ammo_pickup = ammo_pickup - 1
                ammo_pickups_updated.emit(ammo_pickup)
        #using health consumables
        elif event.is_action_pressed("ui_consume_health"):
            if health > 0 && health_pickup > 0:
                health_pickup = health_pickup - 1
                health = min(health + 50, max_health)
                health_updated.emit(health, max_health)
                health_pickups_updated.emit(health_pickup) 
        #using stamina consumables      
        elif event.is_action_pressed("ui_consume_stamina"):
            if stamina > 0 && stamina_pickup > 0:
                stamina_pickup = stamina_pickup - 1
                stamina = min(stamina + 50, max_stamina)
                stamina_updated.emit(stamina, max_stamina)      
                stamina_pickups_updated.emit(stamina_pickup)
        #interact with world        
        #interact with world        
        elif event.is_action_pressed("ui_interact"):
            var target = ray_cast.get_collider()
            if target != null:
                if target.is_in_group("NPC"):
                    # Talk to NPC
                    target.dialog()
                    return
Enter fullscreen mode Exit fullscreen mode

We also need to add our dialog functions to our DialogPopup’s input code to capture our A and B responses.

    ### DialogPopup.gd

    #older code

    # ------------------- Dialog -------------------------------------
    func _input(event):
        if event is InputEventKey:
            if event.is_pressed():
                if event.keycode == KEY_A:  
                    npc.dialog("A")
                elif event.keycode == KEY_B:
                    npc.dialog("B")
Enter fullscreen mode Exit fullscreen mode

So now if our player were to run into our NPC and press TAB, the dialog popup should be made visible. We can then navigate through the conversation with our NPC by responding with our “A” for yes and “B” for no keys. If we get the quest item and we go back to the NPC, the dialog options should update, and we should receive our Pickups as a reward. We’ve then reached the end of our dialog tree, so if we return to our NPC, the last line will play.

Before we can test this out, we need to change our node’s process mode, because the DialogPopup will pause the game. Change the NPC’s process mode to “Always” because we need their animations to play even if the game is paused — which is when the dialog is playing.

Godot RPG

Change the DialogPopup’s process mode to “When Paused”, because we need to be able to run our input when the game is paused.

Godot RPG

Finally, we’ll need to change our Player’s process mode to “Always”. This means our player will be able to add input to the game even when the game is paused.

Godot RPG

A problem will arise if we now play our game because our player will be able to walk away from the NPC when the dialog runs, so to fix this, we need to disable our player’s movement that is executed in their physics_process() function. To do this, we can simply call it at the end of our open() function and set its processing to false! Then in our close function, we just need to set it back to true because if the dialog popup is hidden, we want our player to move again. We’ll also show/hide our cursor.

    ### DialogPopup.gd

    extends CanvasLayer

    # Node refs
    @onready var animation_player = $"../../AnimationPlayer"
    @onready var player = $"../.."

    #gets the values of our npc from our NPC scene and sets it in the label values
    var npc_name : set = npc_name_set
    var message: set = message_set
    var response: set = response_set

    #reference to NPC
    var npc

    # ---------------------------- Text values ---------------------------
    #sets the npc name with the value received from NPC
    func npc_name_set(new_value):
        npc_name = new_value
        $Dialog/NPC.text = new_value

    #sets the message with the value received from NPC
    func message_set(new_value):
        message = new_value
        $Dialog/Message.text = new_value

    #sets the response with the value received from NPC
    func response_set(new_value):
        response = new_value
        $Dialog/Response.text = new_value

    # ------------------- Processing ---------------------------------
    #no input on hidden
    func _ready():
        set_process_input(false)

    #opens the dialog
    func open():
        get_tree().paused = true
        self.visible = true
        animation_player.play("typewriter")
        player.set_physics_process(false)
        Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    #closes the dialog  
    func close():
        get_tree().paused = false
        self.visible = false
        player.set_physics_process(true)
        Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)

    #input after animation plays
    func _on_animation_player_animation_finished(anim_name):
        set_process_input(true)

    # ------------------- Dialog -------------------------------------
    func _input(event):
        if event is InputEventKey:
            if event.is_pressed():
                if event.keycode == KEY_A:  
                    npc.dialog("A")
                elif event.keycode == KEY_B:
                    npc.dialog("B")
Enter fullscreen mode Exit fullscreen mode

If you run your game, and you run over to your NPC and press “TAB”, your NPC dialog tree should run, and you should be able to accept the quest. If you then run over the quest item and return to your NPC, your NPC will notice that you’ve gotten your quest item and completed the quest. Congratulations, you now have an NPC with a simple quest!

Godot RPG

Godot RPG

Unfortunately, our quest and dialog are directly tied to our NPC — and in a real game, you would have the NPC, Quest, and Dialog system in their scripts. I’m going to make a separate tutorial series on this, but for now, I just wanted to show you the basics of dialog trees and states.

If you want multiple NPCs, you’ll have to duplicate the NPC scene and script, as well as the Quest Item scene and script — and then just update the values to have unique dialogs and references to your second NPC. I’ll include an example of another NPC in the code reference above.

Godot RPG

Godot RPG

And there you have it! Now your game has an NPC with a Quest. The rest of the features that we’ll add from here on will be quick to implement. Most of the hard work is done, so now we just need to add a scene-transition ability to transport our player between worlds, and we’ll also give our player the ability to sleep to restore their health and stamina values in the next part! Remember to save your project, and I’ll see you in the next part.

The final source code for this part should look like this.

Buy Me a Coffee at ko-fi.com

FULL TUTORIAL

Godot RPG

The tutorial series has 23 chapters. I’ll be releasing all of the chapters in sectional daily parts over the next couple of weeks.

If you like this series or want to skip the wait and access the offline, full version of the tutorial series, you can support me by buying the offline booklet for just $4 on Ko-fi!😊

You can find the updated list of the tutorial links for all 23 parts in this series here.

Top comments (2)

Collapse
 
mxglt profile image
Maxime Guilbert

Really cool ! :) Good job

Collapse
 
facepalm profile image
Chris

Love it! Looking forward to more complex quest or inventory tutorial.