OpenAI Function Calling 기반 인게임 치트 콘솔 명령어 자동 라우팅 시스템

Unity와 C#에서 OpenAI Function Calling으로 자연어 치트 요청을 구조화된 게임 명령어로 변환하고 화이트리스트·권한·인자 검증·메인 스레드 큐를 거쳐 안전하게 실행하는 콘솔 설계와 구현을 설명합니다.

✓ 복사 완료!
챕터일반 가이드
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
OpenAI Function Calling 기반 인게임 치트 콘솔 명령어 자동 라우팅 시스템

핵심 요약

OpenAI Function Calling을 사용하면 플레이어 또는 개발자의 자연어 요청을 give_gold, teleport_player, set_time_scale 같은 구조화된 게임 함수 호출로 변환할 수 있습니다.
단, 모델 응답을 곧바로 실행하지 말고 Unity 클라이언트의 화이트리스트, 개발 빌드 여부, 권한, 인자 범위 검증, 메인 스레드 실행 큐를 통과시켜야 합니다.

인게임 치트 콘솔에 Function Calling이 필요한 이유는?

전통적인 치트 콘솔은 고정된 문자열 문법을 사용합니다.

/give_gold 500
/teleport_player 120 0 48
/set_time_scale 0.25

이 방식은 빠르고 결정적이지만 명령어가 늘어나면 별칭, 오타, 타입 변환, 인자 검증 코드가 여러 곳으로 분산됩니다. 자연어 입력을 추가하려면 별도의 의도 분류기까지 필요합니다.

Function Calling은 모델이 사용할 수 있는 함수를 JSON Schema로 정의하고 모델이 선택한 함수 이름과 인자를 애플리케이션에 전달하는 방식입니다. OpenAI 공식 문서도 함수 호출을 모델과 애플리케이션 사이의 다단계 흐름으로 설명하며 애플리케이션이 호출을 실행한 뒤 결과를 다시 모델에 전달할 수 있도록 구성합니다. 자세한 동작은 OpenAI 공식 Function Calling 문서를 참고하면 됩니다.

예를 들어 다음 요청을 입력할 수 있습니다.

골드를 500만큼 지급하고 플레이어를 마을 입구로 이동해 줘.

모델은 다음처럼 구조화된 호출 후보를 반환할 수 있습니다.

{
  "name": "give_gold",
  "arguments": {
    "amount": 500
  }
}

모델은 명령어를 선택하고 인자를 제안하지만 실제 게임 상태 변경은 Unity 코드가 담당합니다.

전체 아키텍처는 어떻게 설계할까?

flowchart LR
    A[인게임 콘솔 입력] --> B[중계 서버와 Responses API]
    B --> C[Function Call 후보]
    C --> D[명령어 화이트리스트]
    D --> E[권한과 인자 검증]
    E --> F[Unity 메인 스레드 큐]
    F --> G[게임 시스템 실행]
    G --> H[콘솔 결과와 감사 로그]

각 계층은 다음 책임만 가져야 합니다.

계층책임
콘솔 UI입력, 출력, 취소, 실행 상태 표시
AI 어댑터OpenAI API 요청과 Function Call 추출
명령어 레지스트리함수 이름, 권한, 검증기, 실행 핸들러 관리
검증기타입, 범위, 대상, 실행 조건 확인
실행 큐Unity 메인 스레드에서 순차 실행
게임 시스템골드, 위치, 시간 배율 등 실제 상태 변경

OpenAI Function Calling 스키마는 어떻게 작성할까?

OpenAI API 키는 Unity 바이너리에 넣지 않고 중계 서버에서 관리하는 편이 안전합니다. 클라이언트에는 개발 세션 인증 토큰과 자연어 요청만 전달하고 서버가 OpenAI Responses API를 호출한 뒤 필요한 함수 호출 정보만 반환합니다.

Responses API에 전달할 함수 도구는 다음처럼 정의할 수 있습니다.

{
  "type": "function",
  "name": "give_gold",
  "description": "개발 빌드에서 플레이어에게 골드를 지급한다.",
  "parameters": {
    "type": "object",
    "properties": {
      "amount": {
        "type": "integer",
        "description": "지급할 골드 수량"
      }
    },
    "required": ["amount"],
    "additionalProperties": false
  },
  "strict": true
}

strict: true는 함수 인자가 정의된 스키마를 따르도록 제한하는 데 사용합니다. 그러나 정수라는 타입이 맞는지와 게임 규칙상 허용되는 값인지는 서로 다른 문제입니다. amount가 정수여도 1부터 100000 사이인지 현재 세션에서 지급 가능한지 다시 확인해야 합니다.

함수 설명에는 다음 정보를 명시하는 것이 좋습니다.

  • 수량의 단위와 최소·최대값
  • 월드 좌표인지 로컬 좌표인지
  • 대상 식별자의 형식
  • 전투 중 또는 일시정지 중 실행 가능 여부
  • 세이브 데이터에 미치는 영향
  • 반드시 사용자 확인이 필요한지 여부

Unity C# 명령어 레지스트리는 어떻게 만들까?

명령어를 하나의 거대한 switch 문에 넣기보다 이름, 권한, 검증 함수, 실행 함수를 하나의 정의로 묶으면 확장하기 쉽습니다.

using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;

public enum CheatPermission
{
    LocalDeveloper,
    QA,
    Designer
}

public sealed class CheatExecutionContext
{
    public bool IsDevelopmentBuild;
    public CheatPermission Permission;
    public string SessionId;
}

public sealed class CheatCommandDefinition
{
    public string Name { get; }
    public CheatPermission RequiredPermission { get; }
    public Func<JObject, string> ValidateArguments { get; }
    public Func<JObject, CheatExecutionContext, string> Execute { get; }

    public CheatCommandDefinition(
        string name,
        CheatPermission requiredPermission,
        Func<JObject, string> validateArguments,
        Func<JObject, CheatExecutionContext, string> execute)
    {
        Name = name;
        RequiredPermission = requiredPermission;
        ValidateArguments = validateArguments;
        Execute = execute;
    }
}

public sealed class CheatCommandRegistry
{
    private readonly Dictionary<string, CheatCommandDefinition> commands = new();

    public void Register(CheatCommandDefinition definition)
    {
        if (string.IsNullOrWhiteSpace(definition.Name))
            throw new ArgumentException("명령어 이름이 비어 있습니다.");

        if (!commands.TryAdd(definition.Name, definition))
            throw new InvalidOperationException($"중복 명령어: {definition.Name}");
    }

    public bool TryGet(string name, out CheatCommandDefinition definition)
        => commands.TryGetValue(name, out definition);
}

Unity의 기본 JsonUtility는 임의의 JSON 객체와 딕셔너리 처리에 제약이 있으므로 함수 인자를 다룰 때는 Newtonsoft.Json 같은 별도 JSON 라이브러리를 사용하는 편이 편리합니다.

Function Call 자동 라우팅은 어떻게 구현할까?

라우터는 다음 순서를 고정적으로 수행해야 합니다.

  1. 함수 이름이 등록된 명령어인지 확인합니다.
  2. 개발 빌드와 개발 세션인지 확인합니다.
  3. 사용자 권한이 충분한지 확인합니다.
  4. JSON 인자를 명령어별 규칙으로 검증합니다.
  5. 검증된 호출만 Unity 실행 큐에 넣습니다.
  6. 실제 게임 시스템의 결과를 콘솔과 로그에 표시합니다.
using Newtonsoft.Json.Linq;

public sealed class FunctionCallDto
{
    public string Name;
    public string ArgumentsJson;
}

public sealed class CheatRouter
{
    private readonly CheatCommandRegistry registry;
    private readonly CheatExecutionQueue executionQueue;

    public CheatRouter(
        CheatCommandRegistry registry,
        CheatExecutionQueue executionQueue)
    {
        this.registry = registry;
        this.executionQueue = executionQueue;
    }

    public string Route(FunctionCallDto call, CheatExecutionContext context)
    {
        if (call == null || string.IsNullOrWhiteSpace(call.Name))
            return "함수 호출이 비어 있습니다.";

        if (!context.IsDevelopmentBuild)
            return "개발 빌드에서만 치트 명령어를 실행할 수 있습니다.";

        if (!registry.TryGet(call.Name, out var definition))
            return $"허용되지 않은 명령어입니다: {call.Name}";

        if (context.Permission < definition.RequiredPermission)
            return "현재 권한으로 실행할 수 없는 명령어입니다.";

        try
        {
            if (string.IsNullOrWhiteSpace(call.ArgumentsJson))
                return "함수 인자 JSON이 비어 있습니다.";

            var arguments = JObject.Parse(call.ArgumentsJson);
            var validationError = definition.ValidateArguments(arguments);

            if (!string.IsNullOrEmpty(validationError))
                return validationError;

            executionQueue.Enqueue(() =>
            {
                var result = definition.Execute(arguments, context);
                UnityEngine.Debug.Log($"[Cheat] {call.Name}: {result}");
            });

            return $"실행 예약됨: {call.Name}";
        }
        catch (Newtonsoft.Json.JsonException)
        {
            return "함수 인자 JSON을 해석할 수 없습니다.";
        }
    }
}

라우터에는 모델의 설명이나 자연어 문자열을 넣지 않습니다. 라우터가 신뢰하는 값은 등록된 함수 이름과 검증된 JSON 인자뿐이어야 합니다.

Unity 메인 스레드 실행 큐는 왜 필요한가?

HTTP 응답이나 WebSocket 이벤트는 Unity 메인 스레드가 아닌 곳에서 도착할 수 있습니다. Transform, GameObject 등 UnityEngine API를 백그라운드 스레드에서 직접 호출하면 오류가 발생할 수 있으므로 실행 요청을 큐에 넣고 Update에서 처리합니다.

using System;
using System.Collections.Concurrent;
using UnityEngine;

public sealed class CheatExecutionQueue : MonoBehaviour
{
    private readonly ConcurrentQueue<Action> queue = new();

    public void Enqueue(Action action)
    {
        if (action != null)
            queue.Enqueue(action);
    }

    private void Update()
    {
        const int maxCommandsPerFrame = 8;
        var executed = 0;

        while (executed < maxCommandsPerFrame && queue.TryDequeue(out var action))
        {
            try
            {
                action.Invoke();
            }
            catch (Exception exception)
            {
                Debug.LogException(exception);
            }

            executed++;
        }
    }
}

maxCommandsPerFrame로 프레임당 실행량을 제한하면 여러 함수 호출이 동시에 들어왔을 때 게임플레이 프레임이 치트 처리에 잠식되는 문제를 줄일 수 있습니다.

여러 명령어를 순서대로 실행하려면 어떻게 할까?

다음과 같은 요청은 호출 순서가 중요합니다.

플레이어를 보스 방으로 이동시킨 다음 보스 체력을 1로 설정해.

teleport_player가 완료되기 전에 set_boss_hp를 실행하면 의도와 다른 상태가 될 수 있습니다. 따라서 모델이 반환한 여러 호출을 병렬 실행하지 말고 기본적으로 배열 순서를 유지하는 직렬 큐를 사용해야 합니다.

실행 유형예시처리 방식
순수 조회get_player_position병렬 처리 가능
단일 상태 변경give_gold순서대로 큐에 등록
순서 의존 변경이동 후 보스 체력 변경선행 성공 후 다음 실행
파괴적 변경세이브 삭제, 전체 적 제거별도 확인 필수

모델 설명에만 순서를 맡기지 말고 실행기에서 호출 배열, 선행 성공 여부, 중복 실행 여부를 확인해야 합니다.

Unity 인게임 치트 콘솔에서 자연어 요청과 구조화된 함수 호출 결과를 보여주는 UI 예시

모호하거나 위험한 요청은 어떻게 처리할까?

자연어 요청이 모호하면 임의의 값을 추측해 실행하지 않아야 합니다.

입력권장 동작
골드 500 지급give_gold(amount=500) 실행 예약
골드를 많이 줘수량 확인 질문
플레이어를 마을로 이동위치 식별자가 모호하면 질문
세이브를 삭제해확인 화면 후 실행
등록되지 않은 함수 호출즉시 거부

파괴적인 명령어에는 requiresConfirmation 같은 속성을 두고 UI에서 확인 단계를 강제할 수 있습니다. 모델이 성공했다고 말하기 전에 실제 핸들러 결과를 기다려야 하며 실패 메시지는 게임 시스템이 반환한 값을 기준으로 표시해야 합니다.

보안과 릴리스 안정성은 어떻게 확보할까?

인게임 치트 콘솔은 개발 도구이므로 릴리스 빌드에서 확실히 비활성화해야 합니다.

  • Debug.isDebugBuild 또는 프로젝트 전용 개발 빌드 플래그를 확인합니다.
  • 릴리스 빌드에서는 치트 명령어 레지스트리를 등록하지 않습니다.
  • 개발 세션 토큰과 사용자 권한을 서버에서도 검증합니다.
  • OpenAI API 키를 Unity 바이너리에 저장하지 않습니다.
  • 함수 이름과 인자 원문, 실행 결과를 감사 로그에 남깁니다.
  • 세이브 삭제나 아이템 대량 지급은 2단계 확인을 요구합니다.
  • 모델 응답을 C# 코드, 셸 명령어, SQL, 파일 경로로 실행하지 않습니다.

빌드 플래그만으로는 충분하지 않습니다. QA 공유 빌드가 외부에 노출될 수 있으므로 인증된 개발 세션과 권한 검사를 함께 적용해야 합니다.

성능을 최적화하려면 무엇을 줄여야 할까?

자연어 치트 콘솔은 일반적인 게임 루프가 아니므로 매 프레임 API를 호출하면 안 됩니다. 다음과 같은 이중 경로가 실용적입니다.

입력 유형처리 경로
명확한 슬래시 명령어로컬 파서 → 검증 → 실행 큐
자연어 요청중계 서버 → Function Calling → 검증 → 실행 큐
위험한 명령어Function Calling → 사용자 확인 → 검증 → 실행 큐

추가로 다음 최적화를 적용할 수 있습니다.

  • 입력 확정 시점에만 모델 API를 호출합니다.
  • 현재 콘솔 세션에 필요한 함수 목록만 전달합니다.
  • 긴 대화 기록 대신 최근 명령과 필요한 상태만 전달합니다.
  • 요청에 타임아웃과 취소 토큰을 둡니다.
  • 실행 큐의 프레임당 처리량을 제한합니다.
  • 함수가 많다면 현재 상황에서 허용할 도구 집합을 별도로 제한합니다.

모델 호출 비용과 네트워크 지연이 허용되지 않는 단순 명령은 로컬 파서로 먼저 처리하는 것이 좋습니다.

테스트에서는 무엇을 검증해야 할까?

모델의 함수 선택 품질과 게임 코드의 안전성을 분리해 테스트해야 합니다. 모델 응답은 변할 수 있지만 잘못된 호출이 실행되지 않는다는 규칙은 결정적으로 검증할 수 있어야 합니다.

단위 테스트

  • 등록되지 않은 함수 이름을 거부하는가?
  • 필수 인자가 없으면 거부하는가?
  • 정수 자리에 소수가 들어오면 거부하는가?
  • 최소·최대 범위를 벗어난 값이 실행되지 않는가?
  • 릴리스 빌드에서 모든 호출이 차단되는가?
  • 낮은 권한 사용자가 QA 전용 명령어를 실행할 수 없는가?

시나리오 테스트

입력: 골드 500 지급
기대: give_gold(amount=500) 실행 예약

입력: 골드를 많이 줘
기대: 수량 확인 질문

입력: 세이브를 삭제해
기대: 확인 단계 없이 실행하지 않음

중계 서버가 반환하는 name, arguments, 오류 상태, 여러 호출의 순서도 계약 테스트에 포함해야 합니다.

자주 묻는 질문 (FAQ)

Function Calling이 반환한 함수를 바로 실행해도 되나요?

안 됩니다. 함수 이름 화이트리스트, JSON Schema, 게임 규칙, 권한, 개발 빌드 여부를 모두 다시 검증한 뒤 실행해야 합니다.

OpenAI API 키를 Unity 클라이언트에 넣어도 되나요?

권장하지 않습니다. 클라이언트에 포함된 키는 추출될 수 있으므로 중계 서버에서 API를 호출하고 Unity에는 인증된 세션에 필요한 최소 응답만 전달하는 구조가 안전합니다.

모델 없이도 치트 콘솔을 만들 수 있나요?

가능합니다. 고정 문법과 로컬 파서만으로도 더 빠르고 결정적인 콘솔을 만들 수 있습니다. Function Calling은 자연어 입력과 명령어 탐색 편의성이 필요할 때 추가하는 것이 적절합니다.

정리

OpenAI Function Calling 기반 치트 콘솔의 핵심은 AI가 게임을 직접 조작하게 만드는 것이 아니라 자연어 요청을 제한된 함수 인터페이스로 변환하는 데 있습니다. Unity 클라이언트는 명령어 레지스트리와 메인 스레드 실행 큐를 중심으로 구성하고 화이트리스트·권한·인자 범위 검증을 항상 통과시켜야 합니다.

권장 실행 흐름은 다음과 같습니다.

  1. Unity 콘솔이 사용자 입력을 중계 서버로 보냅니다.
  2. OpenAI 모델이 허용된 함수 스키마 중 하나를 선택합니다.
  3. Unity 라우터가 함수 이름과 인자를 검증합니다.
  4. 검증된 명령만 메인 스레드 실행 큐에 등록합니다.
  5. 게임 시스템의 실제 실행 결과를 콘솔에 표시하고 로그로 남깁니다.

이 경계를 지키면 자연어 입력의 편의성과 기존 치트 콘솔의 예측 가능성을 함께 확보할 수 있습니다.