ToolAIPilotTAP
Sub

Ad

how i created my first ai npc in unity 6 as a solo dev and what i wish someone had told me before i started
game-enginesGuideยท 7 min readยท 965

how i created my first ai npc in unity 6 as a solo dev and what i wish someone had told me before i started

I wanted NPCs in my RPG that players could actually have a conversation with instead of clicking through preset dialogue lines. I spent three weeks building it and broke it twice before it worked. This is exactly how I did it, what broke, what I learned, and the setup that finally gave me conversational NPCs running in a Unity 6 build for about six dollars a month in API costs.

๐Ÿ”ง Tools mentioned in this article
Unity

Unity

Game engine used for the entire project, Personal plan free under $100k revenue

unity.com

Visit
OpenAI API

OpenAI API

GPT-4o-mini used for NPC dialogue, approximately $6 per month at solo dev testing volume

platform.openai.com

Visit
Cursor

Cursor

AI code editor used to write all the NPC controller C# scripts, Pro plan $20 per month

cursor.sh

Visit
Inworld AI

Inworld AI

Dedicated NPC AI platform I tested first before building my own, free tier available

inworld.ai

Visit
PN

Priya Nair

June 24, 2026

#how i created ai npc unity 6 solo dev personal 2026#building ai npc unity 6 personal experience honest 2026#unity 6 ai npc dialogue solo developer personal guide 2026#my first ai npc unity 6 personal experience 2026#unity ai npc solo dev personal honest guide 2026

What I ended up with: four fully conversational NPCs in a Unity 6 RPG prototype. Each NPC has its own personality, remembers what the player said in the current session, and responds in character. API cost during six weeks of development and testing: $9.20 total using GPT-4o-mini. Monthly cost in a shipped game at 50 daily active players doing an average of 10 NPC conversations each: estimated $15 to $25 per month. For a solo dev shipping a premium indie game that is a manageable running cost.

Why I Did Not Just Use Inworld or Convai

I started with Inworld. It has the easiest setup experience of any NPC AI platform I tried and the character builder is genuinely well designed. I had my first NPC talking in about two hours. The problem came later. I wanted the NPC to know things about my game world that I had written. Specific lore, faction relationships, location names, the history of the town the player was in. Getting all of that into Inworld required working within their character knowledge system in a way that felt constrained. I also wanted the NPC to react differently based on the player's in-game reputation score, which is a variable that lives in my Unity game state. Connecting Inworld to my runtime game state was possible but added complexity I did not want to maintain. Building my own API bridge took one afternoon and gave me complete control over what context each NPC receives.

The Setup That Actually Works

csharp
// NPCDialogueController.cs
// Attach to any NPC GameObject in your scene
// Handles conversational AI dialogue using OpenAI API
// Works in Unity 6 LTS

using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

public class NPCDialogueController : MonoBehaviour
{
    [Header("NPC Identity")]
    [TextArea(3, 8)]
    public string npcPersona;
    // Example persona for a blacksmith NPC:
    // You are Aldric, a gruff blacksmith in the town of Ironfeld.
    // You have worked here for 30 years and distrust strangers.
    // You respect the Ironfeld Guard and distrust the Merchant Guild.
    // You speak in short sentences. You do not explain yourself.
    // Keep all responses under 60 words.

    [Header("Game Context")]
    public string npcName = "Aldric";
    public string currentLocation = "Ironfeld Forge";
    // Add any game state variables you want the NPC to know about
    // e.g. player reputation, current quest stage, time of day

    private string apiKey = "YOUR_OPENAI_KEY";
    // In production: load this from a StreamingAssets config file
    // never hardcode API keys in shipped builds

    private const string apiUrl = "https://api.openai.com/v1/chat/completions";
    private List<Dictionary<string, string>> conversationHistory
        = new List<Dictionary<string, string>>();

    private bool isWaitingForResponse = false;

    void Start()
    {
        // Prime the NPC with its persona and current game context
        string systemPrompt = BuildSystemPrompt();
        conversationHistory.Add(new Dictionary<string, string>
        {
            { "role", "system" },
            { "content", systemPrompt }
        });
    }

    private string BuildSystemPrompt()
    {
        // This is where you inject live game state into the NPC context
        StringBuilder sb = new StringBuilder();
        sb.AppendLine(npcPersona);
        sb.AppendLine($"Current location: {currentLocation}");
        // You can add anything here from your game state:
        // sb.AppendLine($"Player reputation with Ironfeld: {GameManager.Instance.Reputation}");
        // sb.AppendLine($"Current quest stage: {QuestManager.Instance.CurrentStage}");
        return sb.ToString();
    }

    public void SendPlayerMessage(string playerMessage,
        System.Action<string> onResponse, System.Action<string> onError)
    {
        if (isWaitingForResponse) return;
        StartCoroutine(GetResponse(playerMessage, onResponse, onError));
    }

    private IEnumerator GetResponse(string playerMessage,
        System.Action<string> onResponse, System.Action<string> onError)
    {
        isWaitingForResponse = true;

        conversationHistory.Add(new Dictionary<string, string>
        {
            { "role", "user" },
            { "content", playerMessage }
        });

        // Trim history to last 10 exchanges to control token cost
        // Always keep the system message at index 0
        if (conversationHistory.Count > 21)
        {
            conversationHistory.RemoveRange(1, 2);
        }

        string jsonBody = BuildRequestJson();
        byte[] bodyBytes = Encoding.UTF8.GetBytes(jsonBody);

        using UnityWebRequest request = new UnityWebRequest(apiUrl, "POST");
        request.uploadHandler = new UploadHandlerRaw(bodyBytes);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");
        request.SetRequestHeader("Authorization", "Bearer " + apiKey);

        yield return request.SendWebRequest();

        isWaitingForResponse = false;

        if (request.result == UnityWebRequest.Result.Success)
        {
            string npcReply = ParseResponse(request.downloadHandler.text);
            conversationHistory.Add(new Dictionary<string, string>
            {
                { "role", "assistant" },
                { "content", npcReply }
            });
            onResponse?.Invoke(npcReply);
        }
        else
        {
            Debug.LogError("NPC API error: " + request.error);
            onError?.Invoke("[" + npcName + " does not respond]");
        }
    }

    private string BuildRequestJson()
    {
        // Manual JSON build to avoid Newtonsoft dependency
        StringBuilder sb = new StringBuilder();
        sb.Append("{\"model\":\"gpt-4o-mini\",\"max_tokens\":120,\"messages\":[");

        for (int i = 0; i < conversationHistory.Count; i++)
        {
            var msg = conversationHistory[i];
            string content = msg["content"].Replace("\"", "\\\"");
            sb.Append($"{{\"role\":\"{msg["role"]}\",\"content\":\"{content}\"}}" );
            if (i < conversationHistory.Count - 1) sb.Append(",");
        }

        sb.Append("]}");
        return sb.ToString();
    }

    private string ParseResponse(string json)
    {
        // Basic extraction, works for standard OpenAI response format
        // Replace with Newtonsoft.Json in production for robustness
        int contentStart = json.IndexOf("\"content\":\"") + 11;
        int contentEnd = json.IndexOf("\"", contentStart);
        if (contentStart < 11 || contentEnd < 0)
            return "[No response]"; // Fallback
        return json.Substring(contentStart, contentEnd - contentStart)
            .Replace("\\n", "\n");
    }

    public void ResetConversation()
    {
        conversationHistory.Clear();
        Start(); // Re-add system prompt
    }
}

What Broke and Why

  • First break: API calls on the main thread. My first version called the API synchronously and Unity froze for one to three seconds every time the player sent a message. Fixed by switching everything to IEnumerator coroutines. This should be obvious to experienced Unity developers. It was not obvious to me until I experienced it.
  • Second break: The JSON parser failing on apostrophes. NPC responses that included words like do not or it is contained apostrophes that my basic string parser was treating as JSON string delimiters. The response came back corrupted and the NPC said nothing. Fixed by replacing apostrophes in the content string before parsing, then switching to a proper JSON library for production builds.
  • Third break that was not a code problem: I gave one NPC a persona that was too long. A 600 word backstory in the system prompt. The NPC responses became inconsistent because GPT-4o-mini was trying to honour too many constraints simultaneously. Trimmed all personas to under 100 words with the most important traits listed first. NPC consistency improved immediately.

The Persona Format That Produces Consistent NPCs

  • Name and role in one sentence: You are Mira, a herbalist in the village of Greenwatch who has lived here her whole life.
  • One defining personality trait: You are kind to strangers but suspicious of anyone who asks about the old mine.
  • Speaking style in one sentence: You speak warmly and use simple language. You occasionally reference plants or remedies.
  • What the NPC knows and does not know: You know local gossip and village history. You do not know anything about events in the capital.
  • One hard rule: Keep all responses under 60 words. Never break character.
  • That is the entire persona. 80 words total. Any longer and the model starts ignoring constraints. Any shorter and the NPC has no personality to lean on.

Mistakes That Cost Me the Most Time

  • Not budgeting for API latency in the UI: My first dialogue UI opened instantly and expected an instant response. The actual API response takes 600 to 1400 milliseconds. Added a typing indicator animation that plays while waiting. The latency feels natural when there is visible feedback. Without feedback it feels broken.
  • Testing only in the Unity Editor: Everything worked fine in the Editor because I could inspect errors in the console. When I tested in a build, the API key was exposed in the compiled binary. Moved to loading the key from a StreamingAssets config file for builds. Still not production secure but better than hardcoding.
  • Not adding a conversation reset button: After a long NPC conversation the history accumulated and token costs per request climbed. Added a button that resets the conversation when the player walks away from the NPC. Keeps costs predictable and stops NPCs from referencing conversations from an hour ago that the player has forgotten about.
  • Using GPT-4o for all NPCs when only one needed it: My blacksmith, innkeeper, and wandering merchant NPCs do not need GPT-4o quality. They need fast, cheap, in-character responses. GPT-4o-mini handles them perfectly at a fraction of the cost. I only use a higher model for story-critical NPCs where dialogue quality matters more.

Final Thoughts

Building my own NPC dialogue system instead of using a platform took one afternoon and gave me complete control over every part of the experience. The code above is genuinely what I use. The total investment was six weeks of iteration and about nine dollars in API costs during development. The result is NPCs that players in my playtesting sessions describe as feeling more alive than any scripted dialogue I have written. That felt worth documenting.

Ad

how i created my first ai npc in unity 6 as a solo dev and what i wish someone had told me before i started | ToolAIPilot