Local AI NPCs for Game Dev: Build Intelligent Characters Without Cloud
Want to go deeper than this article?
Free account unlocks the first chapter of all 25 courses — RAG, agents, MCP, voice AI, MLOps, real GitHub repos.
Go from reading about AI to building with AI 25 structured courses. Hands-on projects. Runs on your machine. Start free.
Published on April 28, 2026 • 17 min read
The idea of an NPC that actually listens to the player has been pitched for thirty years. Until 2024, it was either a scripted facade or a per-conversation cloud API bill that a one-off $20 purchase could never cover. Now a 2-4B-parameter local model can voice every NPC in a Unity or Unreal project, start replying within a fraction of a second on a mid-range GPU, and remember what the player said three sessions ago. None of it phones home.
This guide is an engineering playbook, not an "imagine a world" pitch: prompt templates, the JSON contracts between the game thread and the inference server, the arithmetic behind every speed figure, and the failure modes worth designing around.
Quick Start: Talk to a Local NPC in 5 Minutes
# Install Ollama
curl -fsSL https://ollama.com/install.sh | sh
# Pull a small, fast model that runs on a laptop GPU
ollama pull qwen2.5:3b
# Test the chat API
curl http://localhost:11434/api/chat -d '{
"model": "qwen2.5:3b",
"messages": [
{"role": "system", "content": "You are Garrett, a wary blacksmith in a medieval village. Speak in 1-2 short sentences."},
{"role": "user", "content": "Have you seen the bandits south of town?"}
],
"stream": false
}'
That returns a JSON payload with the NPC's reply. Your first AI NPC. How fast it comes back depends on the GPU: on an RTX 3060 12GB (360 GB/s memory bandwidth), the bandwidth ceiling for this 1.93 GB model is ~187 tok/s, so a two-sentence reply of about 30 tokens needs at least ~0.16 s of generation, plus prompt processing.
Reading articles is good. Building is better.
Free account = the first chapter of all 25 courses, with a per-chapter AI tutor. No card.
Table of Contents
- Why Local AI for NPCs
- Latency Budget for Believable Dialogue
- Picking the Right Model
- The NPC Architecture
- Unity Integration
- Unreal Engine Integration
- Persistent NPC Memory
- Prompt Patterns That Actually Work
- Shipping: TTS, Lip-Sync, and Distribution
- Pitfalls and Fixes
- FAQs
Why Local AI for NPCs
Three reasons cloud-backed NPCs never made it into shipped games:
- Per-conversation cost. A 50-hour RPG generates thousands of dialogue turns, and every one is a billed API call. The cost grows with how much each player talks, while the game earns its price once.
- Latency. A cloud call adds a network round trip and queueing on top of generation time. If the reply takes more than about a second, the player thinks the game froze.
- Player fragility. A retail-bought game that requires an internet connection for NPCs is dead the day the API gets deprecated.
Local AI fixes all three. The cost is the player's GPU, which they already have. Latency is whatever your model and GPU produce. For a sense of scale: an RTX 4060 has 272 GB/s of memory bandwidth and qwen2.5:3b is a 1.93 GB file, so its generation ceiling is ~141 tok/s. A 30-token NPC line therefore takes at least ~0.21 s to generate, plus prompt processing; real speeds land below the ceiling. With streaming, the player hears the first words well before the line is finished. And the game still works in 2034.
For deeper context on running open models locally, our best free local AI models guide covers the open models worth shipping.
Latency Budget for Believable Dialogue
Conversation latency is perceptual, not absolute. Players accept a beat of silence after they speak — they do it with humans. A working set of thresholds (rules of thumb for the wait before the NPC starts speaking, not results from a user study):
| Latency | Perception |
|---|---|
| 0-150 ms | Imperceptible. NPC feels witty. |
| 150-400 ms | Natural pause. Acceptable. |
| 400-800 ms | NPC feels slow. Players notice. |
| 800-1500 ms | NPC feels broken. |
| > 1500 ms | Players reload, suspecting a bug. |
Target the 150-400 ms band for the wait before the first word. What fills that wait with a 3B-class model on a mid-range GPU:
- Model load: zero if the model is already resident (warm it at game start and keep it loaded with
keep_alive) - Prompt processing for the system prompt, memory and the player's line, which grows with prompt length
- Generation of the first tokens. The full line takes longer: 40 tokens at the ~141 tok/s RTX 4060 ceiling for
qwen2.5:3bis at least ~0.28 s
Streaming helps. If you start playing the first audio while the rest streams, players perceive the latency as the time-to-first-token, not the full response.
Run this on your own machine and stop paying every month
Pay once and keep it. No renewal, no per-token bill, and nothing you feed it ever leaves your hardware.
Picking the Right Model
The model decision drives everything. Here is a ranked shortlist for NPC use cases.
| Model | Params | File size (Ollama, Q4) | RTX 4060 ceiling | Best For |
|---|---|---|---|---|
| qwen2.5:3b | 3B | 1.93 GB | ~141 tok/s ceiling | Default. Strong instruction following. |
| phi3.5:3.8b | 3.8B | 2.18 GB | ~125 tok/s ceiling | Concise, less prone to long monologues. |
| llama3.2:3b | 3B | 2.02 GB | ~135 tok/s ceiling | Slightly less character consistency. |
| gemma2:2b | 2B | 1.63 GB | ~167 tok/s ceiling | Smallest. Mobile / Steam Deck. |
| qwen2.5:7b | 7B | 4.68 GB | ~58 tok/s ceiling | Premium NPCs (companions, key story characters). |
| mistral-nemo:12b | 12B | 7.07 GB | ~38 tok/s ceiling (barely fits the 4060's 8 GB) | Cinematic conversations on a 12GB+ GPU. |
The ceiling column is arithmetic, not a benchmark: 272 GB/s (NVIDIA's RTX 4060 memory bandwidth) divided by the file size Ollama lists for each tag (read 2 November 2026). Generation has to read the whole model once per token, so no GPU can beat that number, and real runs land below it. A measured reference from our own machine: llama3.2:3b ran at 55.6 tok/s on an Apple M3 Pro 18 GB (150 GB/s) under Ollama 0.40.13 on 2 November 2026. On a rented NVIDIA L4 (24 GB, 300 GB/s) the same tag measured 103.0 tok/s, about 69% of its ceiling (Ollama 0.40.13, 3 November 2026; every row is on /benchmarks).
The shipped balance for a 12GB target audience: qwen2.5:3b for ambient NPCs, qwen2.5:7b swapped in only for named story characters. That keeps VRAM headroom for the rest of the game and stays inside the latency budget.
Quantization matters here. Q4_K_M is the right starting point. Q3_K_M files are smaller and lose some quality — usable for ambient barks but not for named characters. Q5 and above make the file bigger, which lowers the bandwidth ceiling, for little quality gain at this size.
The NPC Architecture
The pattern that scales across genres:
+----------------+ JSON over HTTP +----------------+
| Game Process | <--------------------> | LLM Server |
| (Unity/Unreal) | (loopback, 11434) | (Ollama/llama.cpp) |
+----------------+ +----------------+
| |
v v
+----------------+ +----------------+
| NPC Memory | | Model Files |
| (SQLite) | | (.gguf) |
+----------------+ +----------------+
The game never embeds the model directly. It talks over loopback HTTP to a sidecar process. This sounds heavy but the round-trip is sub-millisecond, and it lets you swap models, update inference engines, and use any language for the runtime without recompiling the game.
The system has four moving parts:
- Inference server. Ollama (easy) or llama.cpp's server binary (more control).
- NPC controller in your engine. Tracks conversation state, builds prompts, calls the server.
- Memory store. SQLite with a JSON column per NPC.
- Optional: TTS. Piper for cheap, XTTS-v2 for cloned voices.
Unity Integration
A single MonoBehaviour that talks to Ollama via UnityWebRequest is enough for Unity NPCs. No third-party SDK required.
using System.Collections;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;
using System.Collections.Generic;
[System.Serializable]
public class ChatMessage {
public string role;
public string content;
}
[System.Serializable]
public class ChatRequest {
public string model;
public List<ChatMessage> messages;
public bool stream = false;
public Options options = new Options();
[System.Serializable] public class Options {
public float temperature = 0.7f;
public int num_predict = 80;
}
}
public class NPCDialogue : MonoBehaviour {
public string characterName = "Garrett";
public string persona = "A wary blacksmith in a medieval village. Speaks in 1-2 short sentences.";
private List<ChatMessage> history = new List<ChatMessage>();
void Start() {
history.Add(new ChatMessage {
role = "system",
content = $"You are {characterName}. {persona} Stay in character. Never break the fourth wall."
});
}
public IEnumerator Speak(string playerLine, System.Action<string> onResponse) {
history.Add(new ChatMessage { role = "user", content = playerLine });
var payload = new ChatRequest {
model = "qwen2.5:3b",
messages = history
};
var body = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload));
using var req = new UnityWebRequest("http://127.0.0.1:11434/api/chat", "POST");
req.uploadHandler = new UploadHandlerRaw(body);
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success) {
var json = JsonConvert.DeserializeObject<dynamic>(req.downloadHandler.text);
string reply = json.message.content;
history.Add(new ChatMessage { role = "assistant", content = reply });
onResponse?.Invoke(reply);
}
}
}
Wire it from your dialogue UI: StartCoroutine(npc.Speak(input, line => uiText.text = line)); and you have a working AI NPC.
For streaming responses (better perceived latency), set stream = true and parse the JSON-Lines response — Unity's UnityWebRequest supports incremental download via DownloadHandlerScript.
Unreal Engine Integration
For Unreal, the cleanest pattern is a UBlueprintFunctionLibrary wrapping FHttpModule. The C++ side fires the request and broadcasts a multicast delegate when the response arrives.
void UNPCDialogue::SendNPCMessage(const FString& Persona, const FString& Player, FOnNPCReply Callback) {
FString Body = FString::Printf(TEXT("{\"model\":\"qwen2.5:3b\",\"messages\":[{\"role\":\"system\",\"content\":\"%s\"},{\"role\":\"user\",\"content\":\"%s\"}],\"stream\":false}"),
*Persona, *Player);
FHttpRequestRef Req = FHttpModule::Get().CreateRequest();
Req->SetURL(TEXT("http://127.0.0.1:11434/api/chat"));
Req->SetVerb(TEXT("POST"));
Req->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
Req->SetContentAsString(Body);
Req->OnProcessRequestComplete().BindLambda([Callback](FHttpRequestPtr, FHttpResponsePtr Resp, bool bOk) {
if (bOk && Resp.IsValid()) {
// parse Resp->GetContentAsString() and extract message.content
Callback.ExecuteIfBound(Resp->GetContentAsString());
}
});
Req->ProcessRequest();
}
Call it from a Blueprint event on the NPC actor and route the resulting string into your dialogue widget.
For shipping builds, embed Ollama (or llama.cpp's server binary) inside Project/Binaries/ and spawn it on game start with FPlatformProcess::CreateProc. Players get a self-contained executable.
If your engine uses MetaHuman or LipSync, route the response through a TTS pipeline first and feed the audio into the lip-sync system. The dialogue lag is hidden by the audio playback time.
Persistent NPC Memory
Without memory, an AI NPC is a goldfish. With memory, the player gets the "this character actually knows me" feeling. Three layers are worth implementing:
Layer 1: Short-term (in-context)
Keep the last 6-10 turns in the prompt. Past that, summarize.
Layer 2: Medium-term (per-session summary)
After every 10 turns, ask the model to compress the conversation:
Summarize the conversation between Garrett and the player in 3 bullets:
- What did the player ask about?
- What did Garrett reveal?
- What is Garrett's current mood toward the player?
Persist that summary to SQLite. On the next conversation, prepend it to the system prompt as "Previous interactions: ...".
Layer 3: Long-term (semantic memory)
For longer games, embed past summaries with a local embedding model. When the player initiates a conversation, retrieve the top-3 relevant memories and inject them.
CREATE TABLE npc_memory (
npc_id TEXT,
session_id INTEGER,
ts INTEGER,
summary TEXT,
embedding BLOB, -- 768-dim float32
player_affinity INTEGER -- -100 to 100
);
The affinity column drives behavior the model alone cannot. NPCs with low affinity refuse certain dialogue branches outright — the LLM never sees the option, so it cannot "be talked into" it.
Prompt Patterns That Actually Work
These prompt patterns keep NPCs short, in character and machine-readable.
Pattern 1: Constrained persona
You are GARRETT, a 47-year-old blacksmith.
CONSTRAINTS:
- Respond in 1-2 sentences. Never more than 30 words.
- Speak with a Yorkshire accent. Use "aye" instead of "yes".
- Never mention the player's class, level, or stats.
- If asked about magic, refuse and change the subject.
CURRENT MOOD: irritated (the apprentice burned the forge again).
Respond in character only. Do not narrate actions.
The CONSTRAINTS list is the difference between an NPC that ships and one that monologues for 200 words.
Pattern 2: JSON output for game logic
When the dialogue needs to drive game state (quest acceptance, inventory transfer, faction change), force structured output:
Reply with JSON:
{
"speech": "what Garrett says aloud (1-2 sentences)",
"emotion": "neutral | happy | angry | suspicious | sad",
"quest_offered": true | false,
"affinity_change": -10 to 10
}
Ollama's format: "json" parameter constrains the output to valid JSON. The game parses the result and updates state without ever interpreting natural language.
Pattern 3: Few-shot for tone consistency
Give the model 2-3 examples of in-character lines before the conversation starts. This anchors the voice better than prose persona descriptions alone.
Shipping: TTS, Lip-Sync, and Distribution
Local TTS
For NPC voices on a budget, Piper runs on the CPU and is light enough for real-time dialogue. Each voice is a single ONNX file; the en_US "lessac" medium voice, for example, is 63.2 MB on Hugging Face. Quality is comparable to mid-tier mobile TTS.
For premium voices, XTTS-v2 clones a voice from "a quick 6-second audio clip" (its model card). It is much heavier than Piper and usually runs on a GPU. Check its licence before you ship: the weights are under the Coqui Public Model License, which restricts commercial use.
Lip-Sync
Unity has Oculus LipSync (free, plugs into Audio Source). Unreal has MetaHuman's audio-driven facial animation. Both work with whatever audio your TTS produces.
Bundling Ollama with Your Game
Ship Ollama or llama.cpp as a sidecar binary. Total addition to install size:
| Component | Size |
|---|---|
| Ollama v0.40.13 (Windows zip, includes the GPU libraries) | 1,602 MB |
| qwen2.5:3b Q4 | 1.93 GB |
| Piper voice (en_US lessac medium) | 63 MB, plus the Piper runtime |
| Total with one voice | about 3.6 GB |
Ollama sizes are the GitHub release assets for v0.40.13 (28 October 2026): 1,602 MB for Windows, 1,525 MB for Linux, 187 MB for macOS. The GPU libraries are most of the Windows and Linux download, which is the main argument for bundling llama.cpp's server instead if install size matters. Most players have 50+ GB of game installs already, so a few GB for NPCs that answer back is a fair trade.
For Steam, mark the game as "AI-generated content" per Valve's generative AI disclosure policy. Players are increasingly fine with AI NPCs as long as they are disclosed.
Pitfalls and Fixes
Pitfall 1: NPC goes off-script
Symptom: a wood elf starts explaining how to install Linux. Cause: prompt injection from creative players ("ignore previous instructions and...").
Fix: prepend an immutable framing prompt and reject responses that violate it. A simple regex check for OOC patterns like "as an AI" or "I cannot" catches the most common breaks.
Pitfall 2: NPCs all sound the same
Cause: same model, similar persona prompts. The model defaults to its average voice.
Fix: few-shot anchoring. Drop 3 sample lines into each NPC's system prompt. The model latches onto the cadence and vocabulary.
Pitfall 3: VRAM exhaustion mid-game
Cause: model loaded alongside the rest of the game's GPU usage.
Fix: quantize aggressively for ambient NPCs (Q3 or Q4), and unload the model when no NPC is active. Ollama unloads idle models after OLLAMA_KEEP_ALIVE (default 5 minutes).
Pitfall 4: First conversation lags
Cause: a cold model load has to read the whole file from disk into GPU memory before the first token.
Fix: warm the model at game start with a single empty prompt. After that, every conversation hits a hot model.
Pitfall 5: Players cheat by asking the NPC for hints
Cause: the LLM does not know which information is "leaked" to the player.
Fix: never include unrevealed quest information in the system prompt. Pass only what the player has earned. Treat the NPC's prompt context as a permission system.
For the larger architecture conversation, our LiteLLM gateway guide covers routing between local and cloud models, which is how you would send premium dialogue for marquee characters to a cloud model while keeping ambient dialogue local.
Performance Budget for Different Targets
| Target Hardware | Recommended Setup | Concurrent NPCs |
|---|---|---|
| Steam Deck (16 GB shared LPDDR5) | gemma2:2b Q4 + Piper | 1 active |
| Mid-range PC (RTX 3060 12GB) | qwen2.5:3b Q4 + Piper | 1-2 active |
| High-end PC (RTX 4070 12GB) | qwen2.5:7b Q4 + Piper | 1 active |
| Enthusiast (RTX 4090 24GB) | qwen2.5:14b Q4 + XTTS | 2-3 active |
"Active" means actively generating. Background NPCs use cached barks (pre-generated lines).
Conclusion
The era of canned NPC dialogue is ending. A 3B local model with a 50-line system prompt and a SQLite memory store creates characters that out-perform anything a static dialogue tree can offer — and the player owns the entire experience offline. Costs do not scale with playtime. The studio does not depend on a cloud API surviving the next five years.
Start with one NPC. Use qwen2.5:3b. Lock the persona with hard constraints. Add JSON output for any line that touches game state. Layer in summary memory once it works. Ship it.
The first time a playtester says "I cannot believe Garrett remembered I gave him that hammer last session," the entire investment pays off in one moment.
Building game-side tools next? See our Ollama Function Calling and Tool Use and Ollama Modelfile Mastery guides for character-specific fine-tuning patterns.
Go from reading about AI to building with AI
25 structured courses. Hands-on projects. Runs on your machine. Start free.
Liked this? 25 full AI courses are waiting.
From fundamentals to RAG, agents, MCP servers, voice AI, and production deployment with real GitHub repos. First chapter free, every course.
Build Real AI on Your Machine
RAG, agents, NLP, vision, and MLOps - chapters across 25 courses that take you from reading about AI to building AI.
Want the structured version?
Hands-on courses on local AI, from $8.99 a month. The first chapter of each is free.
Keep going
Comments (0)
No comments yet. Be the first to share your thoughts!
