Carrot
본문 바로가기
Unity/OpenAI

[OpenAI API] AI와 대화 시스템 만들기

by 독기품은토끼 2026. 2. 12.

 

오늘은 NPC와 직접 상호작용 하는 대화 시스템을 구현하려고 한다.

 

1. Json 데이터 파싱

이전 포스팅에서는 match를 이용해서 원하는 데이터를 갖고 오는 작업을 진행했다.

그런데 match로 갖고오게되면 생기는 문제점은

json 내부 데이터 구조가 살짝이라도 바뀌면 원하는 데이터를 갖고 오지 못한다!

 

그래서 이번에는 파싱 작업을 통해서 데이터를 가져오려고 한다

 

[파싱의 장점]

  • 타입 기반 접근
    문자열 키가 아닌 클래스 구조를 통해 접근하므로 코드 안정성과 명확성이 높다.
  • 가독성과 유지보수성 향상
    계층 구조에 맞는 클래스 설계를 통해 IDE 자동완성과 명확한 필드 접근이 가능하다.
  • Unity JsonUtility와의 호환성
    [System.Serializable]이 적용된 클래스에 JSON 데이터를 자동 매핑할 수 있다.
  • 복잡한 JSON 구조 관리에 유리
    중첩된 데이터 구조를 계층적으로 표현할 수 있어 특정 필드 수정이나 확장이 용이하다.

 

찾아보니 실무에서는 Serializable 클래스를 정의하기보단 Newtonsoft.json 라이브러리를 활용해서 파싱한다고 한다.

이 부분은 다음 실습에서 활용해 보기로 하자!

 

[System.Serializable]
public class OpenAIResponse
{
    public Choice[] choices;
}

[System.Serializable]
public class Choice
{
    public Message message;
}

[System.Serializable]
public class Message
{
    public string content;
}

 

우선 JSON 응답 데이터를 구조적으로 매핑하기 위해 [System.Serializable]이 적용된 클래스를 정의하였다.

 

// 에러 핸들링
if (request.result == UnityWebRequest.Result.ConnectionError || request.result ==
    UnityWebRequest.Result.ProtocolError)
    Debug.Log("Error : " + request.error);
else
{
    // 응답 처리
    string responseText = request.downloadHandler.text;
    Debug.Log("Response : " + responseText);

    // 응답 데이터에서 assistant 메세지 추출
    var responseData = JsonUtility.FromJson<OpenAIResponse>(responseText);
    if (responseData.choices != null && responseData.choices.Length > 0)
    {
        string assistantMessage = responseData.choices[0].message.content;
        Debug.Log("Assistant : " + assistantMessage);
    }
    else
        Debug.LogWarning("No valid response from the assistant");
}

 

 

이제 JSON 구조에서 message의 content 부분만 가져오는 작업을 해주었다.

 

var responseData = JsonUtility.FromJson<OpenAIResponse>(responseText);

 

Unity에서는 JSON 데이터를 다루기 위해 JsonUtility 라이브러리를 사용하는데

여기서 FromJson<T>() 라는 메서드를 활용하여 JSON 형식의 문자열을 입력받아 이를 특정한 C# 클래스 타입으로 변환한다.

 

 

ㅎㅎ 예제로 토큰 사용하기 아까우니까 책에 있는 응답 내용을 가져와봤다.

 

지금 Open AI가 보내준 응답 구조를 살펴보면

choices 아래 message가 있고 message 안에 content가 있는 걸 확인할 수 있다.

이걸 Serializable로 구조화해서 데이터를 갖고오게하는 셈인 것!!

 

2. UI에 연결해서 채팅해보기

이제 UI 환경에서 사용자가 직접 텍스트를 입력하면 응답하는 구조로 바꿔보겠다.

 

우선 UI 로직과 API 통신 로직을 분리하여 구현해 주겠다!

using UnityEngine;
using UnityEngine.UI;

public class UIManager : MonoBehaviour
{
    public static UIManager Instance;
    public InputField inputField;

    public Text resultText;

    private void Awake()
    {
        if (Instance == null)
            Instance = this;
        else
            Destroy(gameObject);
    }

    private void Start()
    {
        inputField.onEndEdit.AddListener(OnInputFieldEndEdit);
    }

    private void OnInputFieldEndEdit(string inputText)
    {
        if (!string.IsNullOrEmpty(inputText))
        {
            StartCoroutine(OpenAIManager.Instance.SendOpenAIRequest("Answer any question.", inputText, resultText));
            inputField.text = "";
        }
        else
        {
            Debug.LogWarning("Input field is empty or null");
        }
    }
}

 

UIManager는 사용자 입력과 UI 출력을 담당하는 클래스이다.


InputField의 onEndEdit 이벤트를 통해 입력 완료 시점을 감지하고
입력 값이 유효할 경우 OpenAIManager의 SendOpenAIRequest() 코루틴을 호출하여 API 요청을 수행한다.


OpenAIManager는 응답을 처리한 뒤 결과 문자열을 resultText에 전달하여 화면에 출력한다.

 

 

대충 예제랑 비슷하게 UI 배치해 주고 스크립트도 연결해 주었다.

 

3. NPC에 애니메이션 넣어주기

using System.Collections;
using UnityEngine;

public class NPCManager : MonoBehaviour
{
    public static NPCManager Instance;
    public Animator anim;

    public GameObject balloon; //말풍선 프리팹

    void Start()
    {
        // 입력 발생 시 실행되는 이벤트
        UIManager.Instance.inputField.onValueChanged.AddListener(OnInputFieldChanged);

        // 입력 완료 시 실행되는 이벤트
        UIManager.Instance.inputField.onSubmit.AddListener(OnInputFieldSubmit);
    }

    private void OnInputFieldChanged(string inputText)
    {
        // 애니메이션 트리거
        anim.SetTrigger("listen");
        Debug.Log("OnInputFieldEdited");
    }

    private void OnInputFieldSubmit(string inputText)
    {
        balloon.SetActive(true);
    }

    public IEnumerator TalkThenIdle()
    {
        balloon.SetActive(false);
        anim.SetTrigger("talk");
        yield return new WaitForSeconds(5f);
        anim.SetTrigger("idle");
    }
}

 

예제가 생각보다 탄탄허네ㅎㅋㅋ

대화에 생기를 불어넣어 줄 애니메이션까지 적용 완료!

 

 

그럴싸한 NPC 시스템 구현 완료 ㅎㅅㅎ

 

Response : {
  "id": "chatcmpl-D8R75Fe8bayrOdWV4qe4kdLq6YQiH",
  "object": "chat.completion",
  "created": 1770903219,
  "model": "gpt-4o-2024-08-06",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "오늘 날씨가 어떤지는 알 수 없지만, 어떤 날씨에도 어울리는 명언 하나 추천해드릴게요:\n\n\"비가 내릴 때에는 비를 맞아야 한다. 구름 뒤에는 언제나 태양이 숨어 있다.\" - 칼릴 지브란\n\n이 명언은 힘든 시기가 지나면 밝은 날이 올 것이라는 희망을 주는 메시지를 담고 있습니다. 날씨의 양면성을 떠올리며 오늘도 긍정적인 하루 보내세요!",
        "refusal": null,
        "annotations": []
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 27,
    "completion_tokens": 111,
    "total_tokens": 138,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "audio_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0,
      "audio_tokens": 0,
      "accepted_prediction_tokens": 0,
      "rejected_prediction_tokens": 0
    }
  },
  "service_tier": "default",
  "system_fingerprint": "fp_009ac31db9"
}

 

실제 응답 내용은 이렇게 왔다!

UI가 좀 구더기라 아쉽긴 한데 당장은 API 사용법 익히는 거니까 우선은 그냥 넘어가야징..

 

암튼 오늘 학습 끝!