DEV Community

Ekin Odabas
Ekin Odabas

Posted on • Originally published at ekin.page

AI-Powered Dynamic Conversations With NPCs in Godot

This is an example of how you can add dynamic AI-generated responses to your Godot game based on arbitrary player input so that your NPCs can respond in character when talking to them.

For this, you will need an LLM server to consume from inside the game. Here, we will use a local LLM server with Ollama running next to the game on the same machine. We will then take the player's text input, hit the LLM with a special prompt, display the result on the screen, and keep track of the conversation to provide sufficient context with each request.

So these are what we'll need:

  • Godot 4.x (I used 4.5.0)
  • Ollama (any model)

Setting Up Your Local LLM with Ollama

Let's grab Ollama from its official site at:
https://ollama.com/download

In my case, I'm using the Linux instructions, which is to run the below command:

curl -fsSL https://ollama.com/install.sh | sh
Enter fullscreen mode Exit fullscreen mode

Confirm your installation has succeeded by running this in your terminal:

ollama
Enter fullscreen mode Exit fullscreen mode

We need to install a model to run. There's a list of available models at:
https://github.com/ollama/ollama?tab=readme-ov-file#model-library

Some of these models are fairly large, others not as much. The quality of the responses will mostly depend on the size of the model. For this example, I chose one of the smallest ones, gemma3:1b, but feel free to pick whichever you prefer. Larger models will, of course, run slower and use more resources.

This command will download and install a model (replace gemma3:1b with your chosen model):

ollama run gemma3:1b
Enter fullscreen mode Exit fullscreen mode

When the process finishes, you will be able to talk to the model in the command line. Hit CTRL + D to exit this mode. Instead run the below command to start the local Ollama server:

ollama serve
Enter fullscreen mode Exit fullscreen mode

Now we can make API calls to localhost:11434. To generate a response, you can hit the /api/generate endpoint with a POST request that has a body as below:

{
    "model": "gemma3:1b",
    "prompt": prompt,
    "stream": false
}
Enter fullscreen mode Exit fullscreen mode

Using Ollama Responses From a Godot Project

Let's build a small example in Godot to use our LLM for generating dynamic responses for our NPCs.

Create a new Node2D node called Npc and an HTTPRequest node as it's child. Check the Access as Unique Name on your Npc node as well.

Create a new script and name it npc.gd.

Add below variables:

@export var age = 67
@export var gender = "male"
@export var profession = "farmer"
@export var name_ = "Henry"
Enter fullscreen mode Exit fullscreen mode

Of course, normally we'll set these values somewhere, like in the inspector.

Also add a variable to hold our conversation context:

var context = ""
Enter fullscreen mode Exit fullscreen mode

Let's now create a function to make the request to the local Ollama server:

func respondTo(dialogText):
    context += "Player said: " + dialogText + "\n"

    var prompt = """
        You are an npc in a medieval fantasy world named {name}, age {age}, 
        gender {gender}, profession {profession}. The player who is a 
        seasoned mercenary is saying to you: {dialogText}. Respond in 
        character, do not output anything other than your lines of dialog, 
        do not output action lines or descriptions, and prefer responses 
        around one or two paragraphs. The context of the conversation is 
        as follows: {context}
    """.format({
        "name": name_,
        "age": age,
        "gender": gender,
        "profession": profession,
        "dialogText": dialogText,
        "context": context
    })

    var body = {
        "model": "gemma3:1b",
        "prompt": prompt,
        "stream": false
    }

    var json_body = JSON.stringify(body)
    $HTTPRequest.request(
        "http://localhost:11434/api/generate",
        ["Content-Type: application/json"],
        HTTPClient.METHOD_POST,
        json_body
    )
Enter fullscreen mode Exit fullscreen mode

Note the prompt we give the LLM to get the right kind of response. The more detailed your prompt is, the better quality your answers will likely be. And the dialog context allows the LLM to keep track of what has been said previously in the same conversation.

The above function makes the request but does not handle the response that gets returned. Let's add a handler for that too:

func _on_request_completed(result, response_code, headers, body):
    var response = JSON.parse_string(body.get_string_from_utf8()).response
    %TextDisplay.text = response
    context += "You said: " + response + "\n"
Enter fullscreen mode Exit fullscreen mode

And register it on ready as below:

func _ready():
    $HTTPRequest.request_completed.connect(_on_request_completed)
Enter fullscreen mode Exit fullscreen mode

As you can see, we're adding the LLM response to the context as well.

We also need to create a Control node, and inside it a RichTextLabel node called TextDisplay, and check Access as Unique Name to be able to display the output on the screen. Adjust size and position as you prefer.

Now we can grab the NPC node anywhere in the code, call its respondTo method with any string, and get an in-character response from the LLM.

Taking Text Input from the Player

We now need a way to allow the user type a message for our NPC to respond to. Let's start by creating a LineEdit node inside our Control node. Adjust size, place it where you want on the screen. Then attach a new script to LineEdit called input.gd and add this:

func _process(delta):
    if (Input.is_action_just_pressed("ui_submit")):
        %Npc.respondTo(text)
        text = ""
Enter fullscreen mode Exit fullscreen mode

Next, we need to define ui_submit in Project -> Project Settings -> Input Map. Map it to the physical Enter key.

Node Hierarchy

Here's what the final node hierarchy will look like:

Node2D
├── Npc
│   ├── HTTPRequest
├── Control
│   ├── LineEdit
│   └── TextDisplay
Enter fullscreen mode Exit fullscreen mode

Try It Out

You can now run your scene, type in anything into your input box, hit Enter, and see your NPC respond to it. You can keep your dialogue going for as long as you want (within your context window limit). The conversation should keep some level of consistency thanks to the context passed, although it's not going to be 100% consistent.

Example Dialogue

Here's what my output looked like for my setup:

Player: Hey introduce yourself to me.

NPC: Henry. Henry Blackwood. Been tending this land for nigh on sixty years now, you see. My hands are weathered, but my heart… well, my heart still beats with the rhythm of the seasons. And you, sir? A mercenary, are you? A sharp eye, I’ve noticed. I’ve seen more men come and go than stones on a hillside. Don’t expect a lengthy welcome, though. Just… a quiet life, really.

Player: Yes, I am a mercenary. How did you know?

NPC: That’s a rather… bold claim, young one. Let’s just say my eyes have observed a certain restlessness in your stride. You carry yourself with a practiced alertness, a readiness that speaks of journeys undertaken. A man like you, seeking purpose beyond the harvest, wouldn’t be here to waste time with idle pleasantries.

Player: It is true, my purpose is beyond the harvest. I have a shield that needs a repair. Is there a blacksmith in this village?

NPC: Aye, there’s a smith in the village, just past the well. Though he’s a stubborn sort, takes his time. A simple iron plate, a good man, though he charges a decent price. Would you like me to bring you some ale while you wait?

Player: You have my gratitude.

NPC: Thank you, good sir. It is… a simple request, really. A warm ale would be welcome, though. It’s been a long day tending these fields, and a man needs a moment’s rest.

As you can see, some fine tuning of the prompt is necessary, but it's a good start. Bear in mind that better models will perform much better than gemma3:1b.

Flesh It Out

You can go in many interesting directions from here. You can:

  • Add guardrails so that the player is less likely to break the AI, which can teach you a bit of AI security
  • Add features to allow AI responses impact the game state, such as grant you an item, heal your character, or give you a quest
  • Have the LLM decide what each NPC should be doing at any given moment based on variables like time of day
  • Prompt the model to act as a sort of DM in a DnD campaign instead of an individual NPC

And so on... Possibilities are basically endless, have fun exploring them!

Top comments (0)