LYSC
Unity

LLM을 활용한 NPC 지능형 대화 시스템 구현: Unity와 OpenAI 연동

2026.05.09

단순한 선택지를 넘어 유저의 말에 실시간으로 반응하는 NPC 대화 시스템을 구축합니다. 프롬프트 엔지니어링과 API 최적화 전략을 심도 있게 다룹니다.

도입: 정적인 NPC의 시대는 끝났다

기존의 RPG나 어드벤처 게임에서 NPC와의 대화는 미리 정의된 스크립트와 선택지의 조합이었습니다. 이는 개발자가 의도한 서사를 전달하기에는 효과적이지만, 플레이어의 자유로운 몰입을 방해하는 요소가 되기도 합니다. 하지만 이제 거대 언어 모델(LLM)의 등장으로, 우리는 플레이어의 입력에 따라 실시간으로 반응하고, 게임 세계관 내의 지식을 바탕으로 자연스러운 대화를 나누는 지능형 NPC를 구현할 수 있게 되었습니다. 본 포스팅에서는 Unity 엔진과 OpenAI의 API를 연동하여 실제 작동 가능한 지능형 대화 시스템을 구축하는 과정을 상세히 살펴봅니다.

시스템 아키텍처 설계

LLM 기반 NPC 대화 시스템의 핵심은 '프롬프트(Prompt)'와 '상태 유지(State Management)'입니다. 단순한 API 호출을 넘어, NPC의 성격, 배경지식, 현재 상황을 AI에게 인지시켜야 합니다. 이를 위해 우리는 다음과 같은 구조를 설계합니다.

  • Context Manager: NPC의 페르소나와 세계관 정보를 관리합니다.
  • History Logger: 대화 내역을 저장하여 대화의 흐름을 유지합니다.
  • Unity Web Request Handler: OpenAI API와의 통신을 비동기로 처리합니다.
  • UI Controller: 유저의 입력을 받고 대화창에 텍스트를 출력합니다.

OpenAI API 연동을 위한 C# 스크립트

Unity에서 외부 API를 호출하기 위해 UnityWebRequest를 사용합니다. 보안을 위해 API 키는 환경 변수나 보안 저장소에 보관하는 것이 좋지만, 예제에서는 핵심 로직 전달을 위해 구조화된 클래스 형태로 설명합니다.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;

[Serializable]
public class OpenAIChatRequest {
    public string model = "gpt-4-turbo-preview";
    public List messages;
}

[Serializable]
public class Message {
    public string role;
    public string content;
}

public class LLMClient : MonoBehaviour {
    private const string API_URL = "https://api.openai.com/v1/chat/completions";
    [SerializeField] private string apiKey = "YOUR_API_KEY";

    public void SendChatMessage(string userMessage, string systemPrompt, Action onResponse) {
        StartCoroutine(PostChatRequest(userMessage, systemPrompt, onResponse));
    }

    private IEnumerator PostChatRequest(string userContent, string systemPrompt, Action callback) {
        var requestData = new OpenAIChatRequest {
            messages = new List {
                new Message { role = "system", content = systemPrompt },
                new Message { role = "user", content = userContent }
            }
        };

        string json = JsonConvert.SerializeObject(requestData);
        using (UnityWebRequest request = new UnityWebRequest(API_URL, "POST")) {
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader("Content-Type", "application/json");
            request.SetRequestHeader("Authorization", "Bearer " + apiKey);

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success) {
                var response = JsonConvert.DeserializeObject(request.downloadHandler.text);
                string content = response.choices[0].message.content;
                callback?.Invoke(content);
            } else {
                Debug.LogError("API Error: " + request.error);
            }
        }
    }
}

프롬프트 엔지니어링: NPC의 영혼을 불어넣다

단순히 "너는 상점 주인이야"라고 명령하는 것으로는 부족합니다. NPC가 게임의 설정을 어기지 않고 일관된 성격을 유지하도록 상세한 지시문을 작성해야 합니다. 예를 들어, '엘다린'이라는 노련한 대장장이 NPC를 위해 다음과 같은 시스템 프롬프트를 구성할 수 있습니다.

System Prompt: "너는 판타지 세계관의 숙련된 대장장이 '엘다린'이다. 말투는 투박하지만 정이 넘치며, 30년 동안 검을 제련해왔다. 너는 '마왕의 부활' 소문에 대해 걱정하고 있으며, 유저에게 강한 무기의 필요성을 역설한다. 게임 외적인 질문(예: 프로그래밍, 현실 세계 뉴스)에는 모른다고 답하고 대장간 이야기로 화제를 돌려라."

대화의 문맥 유지 (Conversation Memory)

단답형 대화가 아닌 연속적인 대화를 위해서는 이전 대화 내역을 리스트에 저장했다가 다음 요청 시 함께 보내야 합니다. 하지만 LLM에는 토큰 제한이 있으므로, 중요하지 않은 이전 대화는 요약하거나 삭제하는 'Sliding Window' 기법이나 'Summarization' 기법을 도입해야 합니다.

private List conversationHistory = new List();

public void Chat(string userText) {
    // 유저 메시지 추가
    conversationHistory.Add(new Message { role = "user", content = userText });
    
    // API 호출 (history 포함)
    // ... 요청 처리 로직 ...
    
    // 응답 메시지 추가
    conversationHistory.Add(new Message { role = "assistant", content = aiResponse });
    
    // 히스토리가 너무 길어지면 오래된 것부터 제거
    if (conversationHistory.Count > 10) {
        conversationHistory.RemoveRange(0, 2);
    }
}

비용 및 지연 시간(Latency) 최적화

실시간 게임에서 API 응답을 기다리는 1~3초의 시간은 매우 길게 느껴질 수 있습니다. 이를 해결하기 위해 다음과 같은 전략을 사용합니다.

  • Streaming API: 텍스트가 완성될 때까지 기다리지 않고, 생성되는 즉시 한 글자씩 출력하여 체감 속도를 높입니다.
  • Local Cache: 자주 묻는 질문이나 고정된 대화는 로컬에 캐싱하여 API 호출 횟수를 줄입니다.
  • Local LLM: 보안이나 비용이 중요하다면 Llama 3나 Mistral 같은 모델을 Unity 내부(Sentis 이용)에서 직접 구동하는 방안도 고려해 볼 수 있습니다.

결론: 새로운 내러티브의 가능성

LLM과 Unity의 결합은 단순한 기술적 실험을 넘어, 게임이 유저에게 줄 수 있는 경험의 깊이를 바꿉니다. 이제 개발자는 모든 대사를 직접 쓰지 않아도 되며, 유저는 자신만의 고유한 서사를 게임 속에서 만들어낼 수 있습니다. 물론 할루시네이션(가짜 정보 생성) 문제나 비용 문제 등 해결해야 할 과제가 많지만, 지능형 NPC는 향후 오픈월드 RPG의 필수 요소가 될 것임이 분명합니다. 여러분의 프로젝트에도 이 기술을 적용하여 살아 숨 쉬는 세계를 만들어 보시기 바랍니다.

작성자 프로필

LYSC Studio

1인 게임 개발과 웹 기술에 관심이 많은 개발자입니다. 경험을 통해 배운 것을 공유하고, 함께 성장하는 것을 즐깁니다.