Godot에서 AI 기반 동적 NPC 대화

발행: (2025년 12월 25일 오전 04:41 GMT+9)
10 min read
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text of the post (the content you’d like translated). Could you please paste the article’s text here? I’ll keep the source line and all formatting exactly as you requested.

개요

이 예시는 플레이어가 입력한 임의의 텍스트에 따라 동적으로 AI‑생성 응답을 추가하여 NPC가 캐릭터에 맞게 대화할 수 있도록 하는 방법을 보여줍니다.

게임이 질의할 수 있는 LLM 서버가 필요합니다. 이 튜토리얼에서는 게임과 같은 머신에서 실행되는 로컬 Ollama 서버를 사용합니다. 워크플로는 다음과 같습니다:

  1. 플레이어의 텍스트 입력을 받습니다.
  2. 특수하게 구성한 프롬프트를 LLM에 보냅니다.
  3. LLM의 응답을 화면에 표시합니다.
  4. 대화를 추적하여 각 요청에 충분한 컨텍스트가 포함되도록 합니다.

필요 사항

  • Godot 4.x (예제는 4.5.0 사용)
  • Ollama (원하는 모델)

Ollama 로 로컬 LLM 설정하기

Ollama 설치

공식 사이트에서 Ollama를 다운로드하세요:

Linux에서는 다음 명령을 실행할 수 있습니다:

curl -fsSL https://ollama.com/install.sh | sh

설치를 확인하려면:

ollama

모델 설치

모델 라이브러리는 여기에서 확인하세요:

이번 예시에서는 작은 gemma3:1b 모델을 사용하지만, 원하는 모델을 자유롭게 선택하세요. 더 큰 모델은 속도와 리소스 비용이 증가하지만 품질이 향상됩니다.

ollama run gemma3:1b

명령이 완료되면 터미널에서 모델과 채팅할 수 있습니다. CTRL + D 를 눌러 종료하세요.

Ollama 서버 시작

ollama serve

서버가 이제 http://localhost:11434 에서 요청을 대기합니다.

API 요청 형식

응답을 생성하려면 /api/generate 엔드포인트에 JSON 본문을 POST합니다:

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

Source:

Ollama 응답을 Godot 프로젝트에서 사용하기

씬 설정

  1. Npc 라는 이름의 Node2D 노드를 생성합니다.
  2. 그 자식으로 HTTPRequest 노드를 추가합니다.
  3. Npc 노드에서 “Access as Unique Name” 을 활성화합니다.

새 스크립트 npc.gd 를 만들고 Npc 노드에 연결합니다.

내보낸 NPC 속성

@export var age: int = 67
@export var gender: String = "male"
@export var profession: String = "farmer"
@export var name_: String = "Henry"

이 값들은 인스펙터에서 편집할 수 있습니다.

대화 컨텍스트

var context: String = ""

Ollama에 질의하는 함수

func respondTo(dialogText: String) -> void:
    # 플레이어의 말을 컨텍스트에 추가
    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, a seasoned mercenary,
        says to you: {dialogText}. Respond in character, output only dialogue
        (no action lines or descriptions), and keep the reply to one or two paragraphs.
        Conversation context:
        {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
    )

응답 처리

func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
    var parsed := JSON.parse_string(body.get_string_from_utf8())
    if parsed.error != OK:
        push_error("Failed to parse Ollama response")
        return

    var response_text := parsed.result.response
    %TextDisplay.text = response_text
    context += "You said: " + response_text + "\n"

시그널 연결

func _ready() -> void:
    $HTTPRequest.request_completed.connect(_on_request_completed)

NPC 답변을 표시할 UI

  1. 씬에 Control 노드를 추가합니다.
  2. 그 안에 RichTextLabel 노드 TextDisplay 를 추가합니다.
  3. TextDisplay 에서 “Access as Unique Name” 을 활성화합니다.
  4. 라벨의 위치와 크기를 원하는 대로 조정합니다.

이제 게임 어디서든 Npc 노드에 접근하여 다음과 같이 호출할 수 있습니다:

$Npc.respondTo("Hello, traveler! What brings you to these woods?")

NPC는 LLM을 사용해 답변을 반환하고, 대화 내용은 context 에 저장되어 이후 프롬프트에서도 이전 대화를 기억하게 됩니다.

플레이어로부터 텍스트 입력 받기

이제 사용자가 NPC가 응답할 메시지를 입력할 수 있는 방법이 필요합니다.

  1. Control 노드 안에 LineEdit 노드를 생성합니다.
  2. 크기를 조정하고 화면에서 원하는 위치에 배치합니다.
  3. LineEdit에 input.gd라는 새 스크립트를 붙이고 다음을 추가합니다:
func _process(delta):
    if Input.is_action_just_pressed("ui_submit"):
        %Npc.respondTo(text)
        text = ""
  1. Project → Project Settings → Input Map에서 ui_submit을 정의하고 물리적 Enter 키에 매핑합니다.

Node Hierarchy

다음은 최종 노드 계층 구조가 어떻게 보이는지입니다:

Node2D
├── Npc
│   └── HTTPRequest
└── Control
    ├── LineEdit
    └── TextDisplay

시도해 보기

장면을 실행하고, 입력 상자에 아무 것이든 입력한 뒤 Enter 키를 눌러 NPC가 반응하는 것을 확인하세요.
대화는 원하는 만큼 계속할 수 있습니다(컨텍스트 창 제한 내에서).
전달된 컨텍스트 덕분에 대화가 어느 정도 일관성을 유지하지만, 100 % 완벽하지는 않을 것입니다.

Example Dialogue

Player: 안녕, 자기 소개 좀 해줘.

NPC: 헨리. 헨리 블랙우드. 이 땅을 거의 60년째 돌보고 있거든. 내 손은 거칠지만, 내 심장은… 음, 아직도 계절의 리듬에 맞춰 뛰고 있지. 그럼 당신은, 선생님? 용병이신가? 눈이 예리하군, 눈치챘어. 나는 이 언덕 위의 돌보다 더 많은 사람들을 보아왔지. 다만 긴 환영은 기대하지 마라. 그냥… 조용한 삶이란 말이야.

Player: 그래, 나는 용병이야. 어떻게 알았어?

NPC: 그건 꽤… 대담한 주장이군, 젊은이. 내 눈이 네 걸음걸이에서 약간의 안절부절함을 감지했지. 너는 연습된 경계심과 여행을 떠난 듯한 준비된 자세를 가지고 있군. 수확을 넘어선 목적을 찾는 사람이라면, 여기서 한가한 인사에 시간을 낭비하지 않을 거야.

Player: 맞아, 내 목적은 수확을 넘어서. 수리가 필요한 방패가 있어. 이 마을에 대장장이가 있나?

NPC: 그래, 마을에 대장장이가 있어, 우물 바로 지나쪽에 있지. 다만 고집이 센 편이라 시간이 걸려. 간단한 철판을 만들 수 있는 좋은 사람인데, 가격은 꽤 나가. 기다리는 동안 맥주를 가져다 줄까?

Player: 고맙다.

NPC: 고맙다, 좋은 선생님. 그건… 정말 간단한 요청이야. 따뜻한 맥주가 있으면 좋겠군. 이 땅을 돌보는 긴 하루였고, 사람은 잠시 쉬어야 하니까.

보시다시피, 프롬프트에 약간의 미세 조정이 필요하지만 좋은 시작이야. 더 나은 모델은 gemma3:1b보다 훨씬 좋은 성능을 보여줄 거야.

구체화하기

다음과 같은 흥미로운 방향으로 확장할 수 있습니다. 여러분은:

  • 플레이어가 AI를 손상시킬 가능성을 줄이기 위해 가드레일을 추가하세요 (AI 보안 입문에 좋습니다).
  • AI 응답이 게임 상태에 영향을 미치도록 하는 기능을 추가하세요 (아이템 부여, 캐릭터 치유, 퀘스트 제공 등).
  • LLM이 시간대와 같은 변수에 따라 각 NPC가 언제 어떤 행동을 해야 할지 결정하도록 하세요.
  • 모델에게 단일 NPC 대신 D&D 캠페인에서 일종의 DM 역할을 하도록 프롬프트하세요.

… 등등. 가능성은 사실상 무한합니다—즐겁게 탐험해 보세요!

Back to Blog

관련 글

더 보기 »

Kiro: 즉시 금융 및 비용 추적

![Kiro용 표지 이미지: Instant Finance & Expense Tracking](https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/h…)