AI 대화형 NPC와 가상 생태계 기획: 독립적으로 살아 움직이는 게임 세계 설계법

AI 대화형 NPC가 단순한 대사 생성기를 넘어 경제·관계·자원 순환에 참여하도록 설계하는 방법을 다룬다. 시뮬레이션 계층, 안전한 대화 연결, 밸런싱 지표와 구현 절차를 정리한다.

✓ 복사 완료!
챕터NPC 대화 & 생성형 퀘스트
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
AI 대화형 NPC와 가상 생태계 기획: 독립적으로 살아 움직이는 게임 세계 설계법

핵심 요약

이 설계안에서는 규칙 기반 시뮬레이션이 세계 상태를 소유한다. NPC 대화는 계산된 관계, 재고, 소문, 일정 데이터를 표현하며 실제 행동 변경은 검증한 Intent 명령으로만 반영한다. 대화 품질은 별도로 평가하고, 시뮬레이션 재현에는 초기 상태·입력·난수·틱 순서를 기록한다. 이 구조만으로 자연스러움이나 결정성이 보장되지는 않는다.

AI 대화형 NPC가 가상 생태계에 연결되면 무엇이 달라지는가?

이 글에서는 퀘스트 플래그와 대사 테이블을 사용하는 기준안에 시간 경과와 자원 변화에 따른 행동을 추가하는 설계안을 비교한다. 아래 표는 이 두 설계안의 예시이며 기존 NPC 구현 전체의 분류가 아니다. 대장장이의 철광석 부족을 무기 재고 감소, 경비대의 조달 비용, 주민의 소문으로 연결하는 규칙을 제안할 수 있다.

AI 대화형 NPC는 이 상태를 자연어로 설명하는 인터페이스가 될 수 있다. 중요한 점은 NPC가 그럴듯한 이유를 즉석에서 지어내는 것이 아니라 시스템이 기록한 원인과 결과를 대화에 사용해야 한다는 것이다.

구분대사 중심 NPC생태계 연동 AI NPC
세계 상태퀘스트 플래그 중심자원, 가격, 관계, 일정, 사건 로그
대화 역할정해진 정보 전달상태 해석, 단서 제공, 감정 표현
행동 결정스크립트 분기유틸리티 점수와 제약 조건
검증 방식대사 QA시뮬레이션 재현, 지표 추적, 대화 안전성 검사
주요 위험반복 대사환각, 상태 불일치, 경제 폭주

독립적인 가상 생태계는 왜 LLM만으로 설계하면 안 되는가?

이 설계안에서는 가격·재고의 기준 값을 규칙 시스템에 보관한다. 생성 출력이 세계 상태와 일치하는지 검증해야 하므로 모델이 작성한 문장을 그대로 기준 데이터로 사용하지 않는다. 예를 들어 OpenAI Structured Outputs 문서는 스키마 준수와 별개로 내용 오류가 발생할 수 있음을 설명한다. 이는 모든 LLM 설정의 오류율이 같다는 뜻은 아니다.

따라서 역할을 분리한다.

  • 월드 시뮬레이터: 자원 생산, 소비, 이동, 사망, 가격, 사건을 계산한다.
  • NPC 의사결정기: 현재 상태에서 가능한 행동을 점수화해 선택한다.
  • 대화 모델: 허용된 사실과 NPC 성격을 바탕으로 문장을 생성한다.
  • 검증 계층: 대화가 변경하려는 행동을 규칙과 비용으로 검증한다.
flowchart LR
  W[월드 상태
재고·가격·관계·시간] --> S[시뮬레이터]
  S --> D[NPC 의사결정기]
  D --> E[행동 이벤트]
  E --> W
  W --> C[대화 컨텍스트 추출]
  C --> L[LLM 대화 생성]
  L --> I[Intent 제안]
  I --> V[규칙·권한·비용 검증]
  V -->|승인| E
  V -->|거절| L

이 구조에서는 검증을 통과한 명령만 실제 재고를 변경한다. 사실과 명령을 제한해도 모델이 “검이 남아 있다”는 잘못된 문장을 만들 수 있으므로, 거래에 필요한 수량·가격은 기준 데이터에서 직접 표시하고 생성 문장과 대조한다.

가상 생태계의 최소 모델: 무엇부터 시뮬레이션할까?

처음부터 모든 주민의 욕구와 생산 사슬을 세밀하게 만들 필요는 없다. 플레이어 경험에 직접 드러나는 세 계층부터 시작하는 편이 좋다.

1. 자원과 생산 사슬 정의

각 자원에는 생산자, 소비자, 저장 한도, 대체재를 둔다. 예를 들어 철광석 → 철괴 → 무기라는 사슬을 만들고 광부·제련소·대장장이를 각각 연결한다. 식량처럼 생존에 영향을 주는 필수재는 대체 경로를 두어 단일 실패 지점이 되지 않게 한다.

가격은 기준 가격과 수급 압력을 결합한 단순 모델로 시작할 수 있다.

Pt=P0×clamp⁡(1+αDt−Stmax⁡(St,1),0.5,2.0)P_t = P_0 \times \operatorname{clamp}\left(1 + \alpha \frac{D_t-S_t}{\max(S_t, 1)}, 0.5, 2.0\right)
  • PtP_t: 현재 가격
  • P0P_0: 기준 가격
  • DtD_t: 해당 틱의 수요
  • StS_t: 판매 가능한 공급량
  • α\alpha: 가격 민감도. 0.2~0.4는 초기 실험값이며 게임별 적정값은 아니다.

P0>0P_0>0, Dt,St≥0D_t,S_t\geq0, α≥0\alpha\geq0이고 값이 유한하다고 가정한다. 수요와 공급은 같은 틱의 자원 개수로 맞추며 분모의 1은 최소 1개를 뜻한다. 이 식의 clamp는 가격을 기준가의 0.5~2배로 제한할 뿐 품절을 해결하지 않는다. 상한이 없는 모든 경제가 폭주하는 것도 아니다.

2. NPC의 욕구와 행동을 분리

이 예제에서는 배고픔, 불안, 재산 부족, 외로움을 각각 0~100의 결핍 값으로 관리한다. 값이 낮을수록 충족된 상태이며, 행동 효용은 결핍 감소량과 비용을 조합해 정의한다.

U(a)=∑i=1nwi⋅ΔNi(a)−C(a)−R(a)U(a) = \sum_{i=1}^{n} w_i \cdot \Delta N_i(a) - C(a) - R(a)
  • U(a)U(a): 행동 aa의 효용
  • ΔNi(a)\Delta N_i(a): 행동 전 결핍 값에서 행동 후 결핍 값을 뺀 값. 감소하면 양수다.
  • wiw_i: NPC 성격 또는 직업에 따른 가중치
  • C(a)C(a): 시간, 돈, 체력 비용을 가중치로 같은 점수 단위에 환산한 값
  • R(a)R(a): 같은 점수 단위로 환산한 위험 비용

이 예제에서 야간 운송의 R(a)를 높이면 다른 항이 같을 때 그 행동의 효용이 낮아진다. 최종 선택은 다른 행동과의 비교 결과에 따라 달라진다. 실제로 운송을 취소한 사건이 기록됐을 때 대화에 그 이유를 전달한다.

3. 플레이어가 관찰할 수 있는 결과를 설계

이 글에서는 가격 변동, 상점 재고, NPC의 부재, 소문, 요청 퀘스트처럼 플레이어가 관찰할 결과를 먼저 정한다. 직접 보이지 않는 값도 후속 사건에 영향을 줄 수 있으므로 관찰 경로와 계산 비용을 함께 기록한다.

내부 상태플레이어 노출설계 목적
곡물 재고 감소빵 가격 상승, 제빵사의 불만 대사공급 부족 인지
호위대 부족운송 지연, 상인 퀘스트위험과 보상의 선택
관계 악화거래 거절, 소문 변화사회적 행동의 결과
광산 생산량 증가무기 재고 증가, 장비 가격 하락플레이어 개입의 보상

AI 대화형 NPC를 생태계 데이터에 안전하게 연결하는 방법

Step 1. 대화에 전달할 사실을 구조화한다

프롬프트에 전체 월드 상태를 넣지 말고 대화에 필요한 사실만 NPCContext로 추출한다. 값에는 출처와 유효 기간을 포함하면 디버깅이 쉬워진다.

interface NPCContext {
  npcId: string;
  role: "blacksmith" | "merchant" | "guard";
  mood: "calm" | "worried" | "angry";
  knownFacts: Array<{
    id: string;
    text: string;
    sourceEventId: string;
    expiresAtTick: number;
  }>;
  inventory: Array<{ itemId: string; quantity: number; price: number }>;
  availableIntents: Array<"offer_trade" | "share_rumor" | "request_help">;
}

대화 모델에는 knownFacts 밖의 사실을 단정하지 말고 모르는 내용은 모른다고 답하도록 지시한다. 단, 프롬프트 지시만으로는 충분하지 않다. 핵심 거래와 퀘스트 발생은 다음 단계의 검증을 거쳐야 한다.

Step 2. 대화의 행동 제안을 Intent로 분리한다

LLM의 출력은 일반 문장과 구조화된 의도 제안으로 나눈다. 예를 들어 대장장이가 플레이어에게 철괴 5개를 요청할 수는 있지만 대화 텍스트가 곧바로 퀘스트를 생성해서는 안 된다.

{
  "reply": "철괴가 모자라 오늘은 검을 만들기 어렵습니다. 가져다주신다면 제값을 치르겠습니다.",
  "intent": {
    "type": "request_help",
    "npcId": "blacksmith_01",
    "itemId": "iron_ingot",
    "quantity": 5,
    "rewardGold": 180
  }
}

Step 3. 서버 또는 권위 시뮬레이터에서 검증한다

Intent는 NPC의 소지금, 실제 필요 수량, 퀘스트 중복 여부, 플레이어 진행 조건을 확인한 뒤 승인한다. 아래 코드는 형식 파싱 후 호출하는 검증 함수 조각이다. HelpRequest, WorldState와 조회·진행 조건 함수는 프로젝트에서 정의한다. 요청의 npcId는 서버가 대화 세션의 실제 NPC와 일치하는지 먼저 확인하며 playerId는 인증된 세션에서 가져온다.

function validateHelpRequest(intent: HelpRequest, world: WorldState, playerId: string) {
  if (!intent || typeof intent.npcId !== "string" || !intent.npcId.trim()
      || typeof intent.itemId !== "string" || !intent.itemId.trim()
      || !Number.isSafeInteger(intent.quantity) || intent.quantity <= 0
      || !Number.isSafeInteger(intent.rewardGold) || intent.rewardGold < 0)
    return { ok: false, reason: "invalid_input" };
  if (!Object.prototype.hasOwnProperty.call(world.npcs, intent.npcId))
    return { ok: false, reason: "unknown_npc" };
  const npc = world.npcs[intent.npcId];
  if (!isRequestableItem(world, npc, intent.itemId))
    return { ok: false, reason: "invalid_item" };
  if (!canPlayerAcceptRequest(world, playerId, npc.id, intent.itemId))
    return { ok: false, reason: "progress_blocked" };
  const shortage = getShortage(world, npc, intent.itemId);

  if (!Number.isSafeInteger(shortage) || shortage < 0
      || !Number.isSafeInteger(npc.gold) || npc.gold < 0)
    return { ok: false, reason: "invalid_state" };
  if (shortage <= 0) return { ok: false, reason: "no_shortage" };
  if (intent.quantity > shortage) return { ok: false, reason: "exceeds_shortage" };
  if (intent.rewardGold > npc.gold) return { ok: false, reason: "insufficient_funds" };
  if (hasActiveRequest(world, npc.id, intent.itemId)) return { ok: false, reason: "duplicate" };

  return { ok: true };
}

검증 이후 요청 생성과 보상 예약은 같은 트랜잭션 또는 직렬화된 시뮬레이션 틱에서 다시 확인해 처리한다. 검증만 통과하고 나중에 생성하면 그 사이 재고·소지금·중복 상태가 바뀔 수 있다. 실패 사유에 대한 응답도 실제 사유 코드와 대조하며 생성된 설명이 항상 정확하다고 가정하지 않는다.

생태계 밸런싱은 어떤 지표로 검증할까?

플레이어 개입이 없는 조건도 포함해 장시간 시뮬레이션한다. 게임 내 30일, 서로 다른 시드 100회는 초기 실행 계획의 예시이며 안정성을 보장하는 최소 횟수가 아니다. 평균, 극단값, 분위수와 원본 사건을 함께 검토하고 실행 범위를 늘렸을 때 결과가 유지되는지 확인한다. 아래 경보 값도 실험용이다.

지표확인할 문제권장 경보 조건 예시
필수재 품절 시간 비율생활 경제 마비24시간 중 20% 초과
기준 가격 대비 현재 가격의 비율상한에 반복 도달하거나 가격 변화가 없는지 확인예시 가격식의 상한인 2배에 반복 도달
NPC 동일 행동의 연속 틱 수경직된 일상동일 행동이 10틱 이상 연속
퀘스트 발생량콘텐츠 과밀 또는 공백활성 퀘스트가 목표 범위 이탈
관계망 단절 수사회 시스템 붕괴핵심 직업군 연결이 0개

이 모델에서는 화폐·자원의 생성량과 소비량을 함께 기록한다. 세금·수리비·운송비, 제작·수출·부패 등은 소비처 후보이며 모든 게임의 필수 항목은 아니다. 무제한 복제 경로가 설계 목적과 맞는지, 공급 증가에 따른 품절·가격·퀘스트 변화가 목표 범위에 드는지 테스트한다.

시간 단위와 성능 예산은 어떻게 정할까?

이 설계안에서는 월드 시뮬레이션을 고정 틱으로 처리하고 대화 생성은 상호작용이나 중요한 사건에 연결한다. 호출 횟수, 입력·출력 크기, 실제 응답 시간과 비용을 측정해 예산을 정한다. 아래 갱신 간격은 게임 내 시간 기준의 초기 실험값이다.

  • 월드 경제·생산: 게임 내 10~60분마다 1틱
  • NPC 일정·목표 재평가: 1~5분마다 또는 목표 실패 시
  • 플레이어 근처 NPC: 상세 시뮬레이션
  • 원거리 NPC: 집계 시뮬레이션 또는 이벤트 기반 갱신
  • LLM 대화: 상호작용 시 호출하고 결과를 세션 동안 캐시

원거리 NPC를 마을 A에서 마을 B로 이동 중 같은 추상 상태로 처리하고 플레이어가 관찰 가능한 거리로 들어왔을 때 구체화하는 후보안을 시험한다. 이 글에서는 이를 거리·중요도에 따른 시뮬레이션 수준 조절로 부른다. 상세 모델과 집계 모델 전환 시 재고·시간·관계 상태가 보존되는지도 확인한다.

자주 묻는 질문 (FAQ)

LLM이 없으면 가상 생태계를 만들 수 없는가?

가능하다. 경제, 일정, 관계, 퀘스트 발생을 규칙 기반 시뮬레이션으로 구현하는 설계안을 만들 수 있다. LLM을 기존 상태 설명과 자유 입력 해석에 사용하는 후보안도 시험하되 자연스러움, 오류율과 비용은 규칙 기반 대화와 비교해 평가한다.

AI NPC가 거짓 정보를 말하는 문제는 어떻게 줄이는가?

대화 컨텍스트를 이벤트 로그 기반 사실로 제한하고 상태를 바꾸는 요청은 구조화된 Intent와 검증 함수로 처리한다. 상태 변경 명령 검증과 생성 문장 검사는 별도로 수행한다. 명령이 거절돼도 문장에 잘못된 정보가 남을 수 있다.

모든 NPC를 실제로 시뮬레이션해야 하는가?

아니다. 플레이어 주변과 핵심 생산자만 상세 시뮬레이션하고 나머지는 직업군·지역 단위의 집계 모델로 처리한다. 플레이어가 체감할 결과를 유지하는 것이 NPC 수 자체보다 중요하다.

마무리

좋은 AI 대화형 NPC는 말이 많은 NPC가 아니라 세계가 변한 이유를 설명하고 플레이어의 개입에 일관되게 반응하는 NPC다. 먼저 작은 생산 사슬과 제한된 수의 핵심 NPC로 시뮬레이션을 검증한 뒤 대화를 연결하자. 세계 상태의 권한을 규칙 시스템에 남겨 두면 대화의 자유도와 게임 디자인의 통제력을 함께 유지할 수 있다.