How to Build a Godot Dialogue System for NPCs

A Godot dialogue system needs to do more than display an NPC’s words: it also has to handle player input and decide what happens when the conversation ends. Zenva’s experience teaching coding and game development across more than 400 courses provides the foundation for this walkthrough, where you’ll connect those pieces into a reusable NPC conversation system.

You’ll create a dialogue resource, build a panel with a character portrait and typewriter text, lock player controls during conversations, and let a wizard give the player a sword. This tutorial assumes you know Godot basics and are working with the course’s RPG project, including its player, inventory, weapon, and interaction systems.

Download Project Files

The project files and full project are available through the course included in the Intermediate Godot Mini-Degree. Follow that guided path if you’d like the complete project, or keep reading for the dialogue-system walkthrough.

Make a Complete Card Battler in Godot e1788415191322 - How to Build a Godot Dialogue System for NPCs
FREE GODOT COURSE
LEARN GODOT, UNITY, UNREAL & MORE
ACCESS FOR FREE
AVAILABLE FOR A LIMITED TIME ONLY

Plan Your Godot Dialogue System

The player will approach an NPC, press the interaction key, and see a dialogue panel at the bottom of the screen. Pressing E advances through the conversation, and the NPC can optionally give the player an item at the end. Start by defining the data each conversation needs.

Planning the Dialogue Data

A conversation brings together the speaker’s name, their icon, their dialogue lines, and any item they hand over. Store these in a custom resource instead of hard-coding them into a script. Godot resources are saveable data assets, so you can create a separate conversation asset from the FileSystem dock for each NPC.

Creating the Dialogue Script

  1. Open the Scripts folder in the FileSystem dock.
  2. Right-click, choose New Folder…, and name it Dialogue.
  3. Right-click the new folder and choose New Script….
  4. Name the script dialogue.gd and click Create.

This script describes a data asset rather than a node, so it extends Resource. Giving it a class_name makes the custom type available in the editor. Replace the script’s content with:

class_name Dialogue
extends Resource

@export var npc_name : String
@export var npc_icon : Texture2D

@export var lines : Array[String]

@export var item_to_give : ItemData
@export var item_to_give_quantity : int = 1

Each exported variable becomes an Inspector field on the resource:

  • npc_name stores the speaker’s name.
  • npc_icon stores the Texture2D displayed beside the text.
  • lines contains one string for each line of dialogue.
  • item_to_give optionally references an ItemData reward.
  • item_to_give_quantity sets the reward quantity, with a default of 1.

For example, an NPC could ask the player to clear out monsters and provide a few arrows. The item and quantity fields keep that reward in the conversation data.

Creating a Dialogue Resource

Keep conversation assets separate from the scripts:

  1. Right-click the res:// root in the FileSystem dock and create a folder named NPC Dialogue.
  2. Right-click that folder and choose New Resource….
  3. Search for and select the custom Dialogue type.
  4. Save the resource as NPC_Welcome.tres.

Configuring the Dialogue

Select NPC_Welcome.tres to edit its fields in the Inspector. Find the wizard sprite in Sprites/Characters and drag it into Npc Icon, then set Npc Name to Wizard.

Set the Lines array size to 3 and enter these lines in order:

  • Element 0: “Please help! Our local dungeon has been overrun with monsters.”
  • Element 1: “We need your help defeating these monsters.”
  • Element 2: “Here, take this sword. I think it can help you out.”

Godot Inspector panel showing the Dialogue resource with Npc Name set to Wizard, an Npc Icon texture assigned, and the Lines array being filled with dialogue entries.

From the Items folder, drag Sword.tres into Item To Give. Leave Item To Give Quantity at 1.

Godot Inspector panel showing a fully configured Dialogue resource with Npc Name, Npc Icon, three Lines entries, Item to Give set to Sword.tres, and Item to Give Quantity set to 1.

You now have the wizard’s conversation in one resource. Duplicate and adjust resources to create more conversations without changing the dialogue code.

Build the NPC Dialogue UI

Next, create the panel that will display the NPC’s icon, name, and dialogue. The UI lives under the player’s HUD, a CanvasLayer whose children render directly to the screen.

Creating the Background Screen

In the Player scene, add a Panel as a child of HUD and rename it DialogueScreen. This will dim the game world while the conversation is open.

  1. Set Layout > Anchors Preset to Full Rect.
  2. Under Theme Overrides > Styles, create a New StyleBoxFlat in the Panel field.
  3. Set its Bg Color to black and lower the alpha (A) value to around 150, leaving the game visible behind the overlay.

Adding the Dialogue Panel

Add another Panel as a child of DialogueScreen and name it DialoguePanel. The outer screen will toggle the whole UI, while this inner panel holds the conversation.

  1. Set Layout > Layout Mode to Anchors and Anchors Preset to Center Bottom.
  2. Under Transform, set the size to roughly 600 by 200. Reapply Center Bottom if needed, then move the panel slightly up from the screen edge.
  3. Create a StyleBoxFlat under Theme Overrides > Styles and choose a background color.

Creating a Border Effect With a BG Child

Duplicate DialoguePanel, rename the copy BG, and move it inside DialoguePanel as a child.

  1. Set BG‘s Anchors Preset to Center.
  2. Set its size to around 590 by 190 so the parent remains visible as a thin outline.
  3. Create a new StyleBoxFlat for BG and give it a darker color.

Godot 2D viewport showing the DialoguePanel with a lighter tan outer border created by the parent panel and a darker brown inner BG child, producing an outlined frame effect.

Adding the NPC Icon

Add a TextureRect under DialoguePanel and rename it NPCIcon. Drag in the wizard sprite as a placeholder texture.

To keep the image from warping when resized, set Stretch Mode to Keep Aspect Centered. Position the icon on the left of the dialogue box, set Layout Mode to Anchors, and choose Left Wide for the preset. Adjust the margins until the icon fits.

Godot editor with the NPCIcon TextureRect selected inside DialoguePanel, showing a wizard placeholder sprite rendered at the correct aspect ratio and the Inspector Stretch Mode set to Keep Aspect Centered.

Adding the NPC Name Label

Add a Label under DialoguePanel and name it NPCName. Give it placeholder text such as NPC Name. Set its layout mode to Anchors and its preset to Top Wide, then adjust its position and size.

Create a New LabelSettings resource under Label Settings. Set the font size to around 28, choose a color such as warm yellow, and add a subtle shadow.

Adding the Dialogue Text Label

Duplicate NPCName, move the copy down inside the panel, and rename it Dialogue. Change the placeholder text to “This is where the NPC will talk”.

Set Autowrap Mode to Word so long lines wrap within the label. Create a separate LabelSettings resource, set the font size to around 20, reduce the line spacing, and add a small shadow.

The finished dialogue box in the Godot viewport, showing the wizard icon on the left, the yellow NPC Name label at the top, and word-wrapped dialogue text below it.

Setting Up the Dialogue Controller Node

With the UI arranged, add a generic Node as a child of Player and name it DialogueController. Create dialogue_controller.gd in Scripts/Dialogue and attach it to that node.

Declaring the Controller’s Variables

The controller needs the active resource, references to the UI and player, and two variables to track the text reveal and current line. Start the script with:

class_name DialogueController
extends Node

var current_dialogue : Dialogue

@onready var dialogue_screen : Panel = $"../HUD/DialogueScreen"
@onready var npc_name_text : Label = $"../HUD/DialogueScreen/DialoguePanel/NPCName"
@onready var npc_icon : TextureRect = $"../HUD/DialogueScreen/DialoguePanel/NPCIcon"
@onready var dialogue_text : Label = $"../HUD/DialogueScreen/DialoguePanel/Dialogue"

@onready var player : Player = $".."

var visible_chars : float
var current_line : int

visible_chars will increase over time to reveal the text character by character. current_line tracks the conversation’s position in the lines array.

Laying Out the Function Stubs

Add the functions you’ll fill in next. _ready handles startup, set_dialogue opens a conversation, _process animates the text, _set_line prepares a line, and close_screen closes the UI and restores control.

func _ready():
    pass

func set_dialogue(dialogue : Dialogue):
    pass

func _process(delta):
    pass

func _set_line(line : String):
    pass

func close_screen():
    pass

Add Typewriter Text, Input, and Item Rewards

You can now fill in the controller functions, starting with opening and closing the window before adding input and rewards.

Closing the Dialogue Window on Start

Call close_screen from _ready so the UI starts hidden. The helper hides the panel and clears the active dialogue. Leave the movement TODO in place until you connect player controls.

func _ready ():
	close_screen()

func close_screen ():
	dialogue_screen.visible = false
	current_dialogue = null
	# TODO: Enable player movement

Opening a New Dialogue

In set_dialogue, store the incoming resource, show the screen, and load the NPC’s name and icon. Set current_line to zero and pass the first line to _set_line.

func set_dialogue (dialogue : Dialogue):
	current_dialogue = dialogue

	dialogue_screen.visible = true
	npc_name_text.text = dialogue.npc_name
	npc_icon.texture = dialogue.npc_icon

	current_line = 0
	_set_line(dialogue.lines[0])

	# TODO: Disable player movement

Preparing the Line for the Typewriter Effect

A Label’s visible_characters property controls how many characters appear. A value of 0 hides them all; a value of 3 shows the first three. Reset the counters and assign the full text so it is ready to reveal:

func _set_line (line : String):
	visible_chars = 0
	dialogue_text.visible_characters = 0
	dialogue_text.text = line

Animating the Text in _process

Increase visible_chars by 30 * delta each frame for roughly 30 characters per second, independent of frame rate. Cast the float to an integer when assigning it to the label:

func _process (delta : float):
	visible_chars += 30 * delta
	dialogue_text.visible_characters = int(visible_chars)

Testing the Typewriter Effect

Temporarily comment out the close_screen() call in _ready so the window stays visible at startup. Run the game with placeholder text in the label to see the character-by-character reveal.

Handling Player Input to Advance the Conversation

Extend _process so input is handled only when a dialogue is active. When the player presses interact, close the window if the current line is the last one. Otherwise, advance the index and prepare the next line.

func _process (delta : float):
	visible_chars += 30 * delta
	dialogue_text.visible_characters = int(visible_chars)

	if not current_dialogue:
		return

	if Input.is_action_just_pressed("interact"):
		if len(current_dialogue.lines) == current_line + 1:
			# TODO: Give player item
			close_screen()
		else:
			current_line += 1
			_set_line(current_dialogue.lines[current_line])

Wiring Up a Test Dialogue

Add a temporary exported resource and call set_dialogue in _ready, keeping close_screen() commented out:

@export var test_dialogue : Dialogue

func _ready ():
	# close_screen()
	set_dialogue(test_dialogue)

Select DialogueController and drag the wizard’s welcome resource into Test Dialogue. Run the game: the first line appears with the typewriter effect, E advances the dialogue, and another press after the final line closes the window.

Giving the Player an Item at the End of the Dialogue

Replace the reward TODO in the input-handling branch with a check for item_to_give. When an item is assigned, loop through the configured quantity and add it to the player’s inventory before closing the window:

	if Input.is_action_just_pressed("interact"):
		if len(current_dialogue.lines) == current_line + 1:
			if current_dialogue.item_to_give:
				for i in range(current_dialogue.item_to_give_quantity):
					player.inventory.add_item(current_dialogue.item_to_give)

			close_screen()
		else:
			current_line += 1
			_set_line(current_dialogue.lines[current_line])

Complete the wizard’s conversation to receive a sword. To test the quantity, set it to 5 on the resource and complete the conversation again, then return the quantity to 1.

Godot game running with the wizard dialogue box visible over the overworld scene. The player inventory shows items, and the Inspector on the right displays the Dialogue resource with Sword.tres assigned as the item to give.

Lock Player Controls During Conversations

The dialogue now displays text and gives rewards. Next, stop the player from moving, attacking, or starting another interaction while a conversation is open.

Disabling Player Movement

Open character.gd and add a movement flag near the top. Keeping it on the base Character allows it to apply to other characters as well as the player.

var can_move : bool = true

In _physics_process, wrap the call to _move in a check:

func _physics_process(delta):
	if can_move:
		_move(delta)

Movement runs while can_move is true. Setting it to false stops that movement logic.

Disabling Weapon Usage

Add the equivalent flag to character_weapons.gd:

var can_use : bool = true

Then open player_weapons.gd, where weapon input is read. At the start of _process, return early when can_use is false, before the existing input and firing logic:

func _process (delta : float):
	if not can_use:
		return

	var mouse_pos : Vector2 = get_global_mouse_position()
	var mouse_dir : Vector2 = global_position.direction_to(mouse_pos)

	# ... rest of the function

A Central Toggle on the Player

In player.gd, add toggle_usability so one function can change both flags. Passing true enables movement and weapons; false disables them.

func toggle_usability (toggle : bool):
	can_move = toggle
	weapons.can_use = toggle

The same toggle also needs to disable interactions, preventing the player from repeatedly triggering the NPC while the dialogue is open.

Wiring In the Interaction Controller

Declare an InteractionController reference near the top of player.gd. Don’t use @onready here: toggle_usability may be called before the player’s _ready runs.

var interaction_controller : InteractionController

Extend the toggle to resolve that reference when needed, then call the existing interaction controller’s enable() or disable() method:

func toggle_usability (toggle : bool):
	can_move = toggle
	weapons.can_use = toggle

	if not interaction_controller:
		interaction_controller = $InteractionController

	if toggle:
		interaction_controller.enable()
	else:
		interaction_controller.disable()

Calling toggle_usability From the Dialogue Controller

Return to dialogue_controller.gd. At the end of set_dialogue, call player.toggle_usability(false) to disable movement, weapons, and new interactions:

func set_dialogue (dialogue : Dialogue):
	current_dialogue = dialogue

	dialogue_screen.visible = true
	npc_name_text.text = dialogue.npc_name
	npc_icon.texture = dialogue.npc_icon

	current_line = -1
	_set_line(dialogue.lines[0])

	player.toggle_usability(false)

In close_screen, restore those controls with player.toggle_usability(true):

func close_screen ():
	dialogue_screen.visible = false
	current_dialogue = null

	player.toggle_usability(true)

Testing the Locked Dialogue

Run the game and open the test conversation. Movement, weapon use, and new NPC interactions should be disabled until the window closes, while the dialogue’s input handling advances the conversation.

The running game viewport shows the player in a stone-walled dungeon room with the Wizard NPC dialogue box displayed at the bottom, demonstrating that the dialogue is now active.

Once you’ve checked the control lock, remove the temporary test-dialogue setup and return to closing the window at startup.

Challenge

Before continuing, try connecting the system to an NPC yourself:

  • Create an NPC scene in the overworld.
  • Add an Interactable node.
  • Attach a new script with the class name DialogueNPC.
  • Use the interaction to open a custom Dialogue resource.

The next section walks through that setup.

Connect the Dialogue to an NPC

With the resource, UI, and controller ready, you can connect them to a wizard in the overworld.

Setting Up the Dialogue NPC Scene

In the overworld scene, select Player and hide the dialogue window in the editor so it doesn’t cover the viewport. Then build the NPC:

  1. Create a Node2D, rename it DialogueNPC, and place it somewhere the player can reach.
  2. Add a Sprite2D child named Sprite and assign the wizard texture.
  3. Add an Area2D child named Interactable as the interaction zone.
  4. Under Interactable, add a CollisionShape2D with a CircleShape2D. Adjust its radius to the desired interaction range.
  5. Attach the project’s existing Interactable.gd script to Interactable and set its Prompt to Talk to Wizard.

Your scene branch should contain:

  • DialogueNPC (Node2D)
    • Sprite (Sprite2D)
    • Interactable (Area2D)
      • CollisionShape2D

Godot editor showing the new DialogueNPC node placed on the grass path next to a house, with the wizard sprite assigned in the overworld scene.
Godot Inspector showing the Interactable node with the Interactable.gd script attached and the Prompt property set to Talk to Wizard.

Creating the Dialogue NPC Script

Attach a new script to DialogueNPC and save it as dialogue_npc.gd in Scripts/Dialogue. Export a resource for the NPC’s conversation and reference its Interactable child:

class_name DialogueNPC
extends Node2D

@export var dialogue : Dialogue
@onready var interactable : Interactable = $Interactable

Connecting the Interact Signal

The Interactable node emits an Interact signal when the player is in range and presses the interaction key. The signal includes the interacting Player. Connect it in _ready and create a handler:

func _ready ():
	interactable.Interact.connect(_on_interact)

func _on_interact (player : Player):
	pass

Exposing the Dialogue Controller on the Player

In player.gd, add an @onready reference to the DialogueController child beside the other node references:

@onready var dialogue_controller : DialogueController = $DialogueController

This lets code with access to the player reach its dialogue controller.

Triggering the Dialogue on Interact

Back in dialogue_npc.gd, use the player passed by the signal to start the NPC’s assigned conversation:

func _on_interact (player : Player):
	player.dialogue_controller.set_dialogue(dialogue)

The complete NPC script is:

class_name DialogueNPC
extends Node2D

@export var dialogue : Dialogue
@onready var interactable : Interactable = $Interactable

func _ready ():
	interactable.Interact.connect(_on_interact)

func _on_interact (player : Player):
	player.dialogue_controller.set_dialogue(dialogue)

Assigning a Dialogue and Testing

Select the DialogueNPC instance and drag the wizard’s welcome resource into its exported Dialogue field.

Run the game and approach the wizard. The Talk to Wizard prompt appears within interaction range. Press E to start the conversation, then advance through the lines while movement and weapon use remain disabled.

Running game showing the wizard portrait and dialogue box at the bottom of the screen after interacting with the Dialogue NPC.

After you advance past the final line, the window closes, player control returns, and the configured sword reward is added to the inventory.

Running game after completing the wizard dialogue, showing a sword item added to the player's inventory hotbar.

Where to Take This Next

Each NPC can use a separate Dialogue resource, so you can add conversations without further dialogue-system code. Try NPCs that share lore, explain movement or combat, or hand out starter items like the wizard’s sword.

Recap and Next Steps

You now have an NPC conversation that connects reusable dialogue data to the player’s UI and controls. Along the way, you’ve covered how to:

  • Store names, portraits, dialogue lines, and item rewards in a custom resource.
  • Build a dialogue panel with wrapped text and a typewriter reveal.
  • Advance conversations through player input and deliver configured rewards.
  • Disable movement, weapons, and new interactions while dialogue is active.
  • Connect an NPC’s interaction signal to the dialogue controller.

Next, put those pieces to work with another conversation resource, whether your NPC explains the world or offers the player a starting item.

For a structured path through the full project and more Godot development, continue with the Intermediate Godot Mini-Degree and build on the systems you’ve practiced here.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image - How to Build a Godot Dialogue System for NPCs

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