유니티와 파이썬 연동 TCP 소켓 활용 심화편

현대 게임 개발에서 유니티와 파이썬의 연동은 단순한 편의를 넘어선 필수적인 기술로 자리 잡고 있습니다. 유니티는 게임의 시각적, 물리적 구현을 담당하는 ‘몸’이라면, 파이썬은 복잡한 데이터 분석, 머신러닝, 인공지능 로직을 처리하는 ‘두뇌’ 역할을 수행합니다. 이 두 시스템을 연결하는 통신망으로, 가장 신뢰성 높은 TCP 소켓 통신을 활용하는 방법을 심층적으로 다룹니다. 이 글은 단순히 데이터를 주고받는 것을 넘어, 실용적인 데이터 직렬화(Serialization), 멀티스레딩, 그리고 오류 처리까지 포함된 완성도 높은 연동 가이드입니다.


1. 유니티 클라이언트 스크립트 (PythonClient.cs) – 심화

아래 코드는 유니티에서 작동하며, JSON 데이터를 파이썬으로 보내고, 응답을 수신하는 모든 과정을 책임집니다. Unity의 메인 스레드 멈춤 현상을 방지하기 위해 네트워크 통신을 전담하는 백그라운드 스레드를 사용합니다.

C#

using System;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
using System.Threading;
using System.IO;

[Serializable]
public class VectorData // JSON 직렬화를 위한 데이터 구조체
{
    public float x, y, z;
    public string message;
}

public class PythonClient : MonoBehaviour
{
    [Header("서버 연결 설정")]
    public string serverIp = "127.0.0.1";
    public int port = 8080;

    private TcpClient client;
    private Thread receiveThread;
    
    private NetworkStream stream;
    private StreamWriter writer;
    private StreamReader reader;

    private bool isConnected = false;

    // 데이터 수신 시 호출될 이벤트
    public event Action<string> OnDataReceived;

    void Start()
    {
        // 애플리케이션 시작 시 자동으로 서버 연결 시도
        ConnectToServer();
    }

    public void ConnectToServer()
    {
        try
        {
            client = new TcpClient(serverIp, port);
            stream = client.GetStream();
            writer = new StreamWriter(stream, Encoding.UTF8);
            reader = new StreamReader(stream, Encoding.UTF8);

            isConnected = true;
            Debug.Log($"<color=lime>서버에 연결되었습니다. IP:{serverIp} Port:{port}</color>");

            // 데이터 수신을 위한 백그라운드 스레드 시작
            receiveThread = new Thread(new ThreadStart(ReceiveData));
            receiveThread.IsBackground = true;
            receiveThread.Start();
        }
        catch (Exception e)
        {
            Debug.LogError($"<color=red>연결 실패:</color> {e.Message}");
            isConnected = false;
        }
    }

    // 파이썬 서버로 JSON 데이터를 전송하는 함수
    public void SendData(Vector3 position)
    {
        if (isConnected && writer != null)
        {
            try
            {
                // Vector3 데이터를 JSON으로 직렬화
                VectorData dataToSend = new VectorData
                {
                    x = position.x,
                    y = position.y,
                    z = position.z,
                    message = "Unity의 위치 데이터입니다."
                };
                string jsonData = JsonUtility.ToJson(dataToSend);

                writer.WriteLine(jsonData);
                writer.Flush();
                Debug.Log($"<color=cyan>데이터 전송:</color> {jsonData}");
            }
            catch (Exception e)
            {LogError($"데이터 전송 오류: {e.Message}");
            }
        }
    }

    // 백그라운드 스레드에서 서버로부터 데이터를 수신
    private void ReceiveData()
    {
        while (isConnected)
        {
            try
            {
                // 서버에서 보낸 한 줄의 문자열을 읽음
                string receivedData = reader.ReadLine();
                if (receivedData != null)
                {
                    // 받은 데이터를 메인 스레드에 전달 (유니티는 메인 스레드에서만 API 호출 가능)
                    if (OnDataReceived != null)
                    {
                        // 받은 데이터를 이벤트로 전달
                        OnDataReceived.Invoke(receivedData);
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogError($"<color=red>데이터 수신 오류:</color> {e.Message}");
                Disconnect();
                break;
            }
        }
    }

    // 어플리케이션 종료 시 연결 해제 및 스레드 정리
    void OnApplicationQuit()
    {
        Disconnect();
    }

    // 연결을 안전하게 해제하는 함수
    private void Disconnect()
    {
        if (isConnected)
        {
            isConnected = false;
            if (receiveThread != null && receiveThread.IsAlive)
            {
                receiveThread.Abort(); // 스레드 강제 종료
            }
            if (client != null)
            {
                client.Close();
            }
            Debug.Log("<color=yellow>연결이 해제되었습니다.</color>");
        }
    }
}

2. 파이썬 서버 스크립트 (server.py) – 심화

이 파이썬 스크립트는 여러 클라이언트의 동시 접속을 처리할 수 있는 멀티스레드 서버로, JSON 데이터를 분석하여 간단한 계산을 수행한 후 결과를 다시 JSON 형식으로 반환합니다.

Python

import socket
import json
import threading

HOST = '127.0.0.1'
PORT = 8080

# 클라이언트 요청을 처리하는 스레드 함수
def handle_client(conn, addr):
    print(f"\n--- 클라이언트 연결: {addr} ---")
    try:
        while True:
            data = conn.recv(1024)
            if not data:
                break
            
            decoded_data = data.decode('utf-8').strip()
            print(f"[{addr}] 수신: {decoded_data}")

            try:
                # 수신한 JSON 데이터를 파이썬 딕셔너리로 변환
                json_data = json.loads(decoded_data)
                
                # 간단한 연산 수행
                x, y, z = json_data.get("x", 0), json_data.get("y", 0), json_data.get("z", 0)
                result_value = x + y + z
                
                # 결과를 새로운 JSON 형식으로 재구성
                response_json = {
                    "status": "success",
                    "result": result_value,
                    "message": "계산이 완료되었습니다."
                }
                response = json.dumps(response_json) + "\n"
                
            except json.JSONDecodeError:
                response = f"{decoded_data}를 받았습니다. JSON 형식이 아닙니다.\n"

            conn.sendall(response.encode('utf-8'))

    except Exception as e:
        print(f"[{addr}] 오류 발생: {e}")
    finally:
        print(f"--- 클라이언트 연결 종료: {addr} ---")
        conn.close()

def start_server():
    print("파이썬 서버를 시작합니다...")
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
        server_socket.bind((HOST, PORT))
        server_socket.listen()
        print(f"[{HOST}:{PORT}]에서 연결을 기다리는 중...")
        
        while True:
            # 클라이언트 연결 요청을 수락
            conn, addr = server_socket.accept()
            # 클라이언트 처리를 위한 새로운 스레드 생성
            client_thread = threading.Thread(target=handle_client, args=(conn, addr))
            client_thread.daemon = True # 메인 스레드 종료 시 함께 종료되도록 설정
            client_thread.start()

if __name__ == "__main__":
    start_server()

3. 심화 원리 및 실행 가이드

  1. 멀티스레딩의 중요성: 유니티에서 TcpClientGetStream()은 데이터가 올 때까지 무한정 기다리는 블로킹(blocking) 함수입니다. 이 함수를 메인 스레드에서 직접 호출하면 게임이 멈추게 됩니다. 따라서, Thread를 사용해 백그라운드에서 데이터를 수신하는 것이 필수적입니다. 파이썬 서버 역시 threading을 사용하여 여러 클라이언트의 요청을 동시에 처리할 수 있는 구조를 만듭니다.
  2. JSON 직렬화/역직렬화: 게임 오브젝트의 위치(Vector3)와 같은 복잡한 데이터를 네트워크를 통해 전송하려면 이를 문자열로 변환하는 과정이 필요합니다. C#에서는 [Serializable] 속성과 **JsonUtility.ToJson()**을, 파이썬에서는 **json.loads()**와 **json.dumps()**를 사용해 데이터를 주고받습니다. JSON은 사람도 읽기 쉬우면서 모든 언어에서 지원하는 표준 형식이라서 두 플랫폼 간의 데이터 교환에 최적입니다.
  3. 이벤트 기반 데이터 처리: PythonClient 스크립트의 OnDataReceived 이벤트는 백그라운드 스레드에서 수신한 데이터를 유니티의 메인 스레드로 안전하게 전달하는 역할을 합니다. 이렇게 하면 네트워크 통신 로직과 게임 로직을 분리하여 더욱 견고한 코드를 만들 수 있습니다.

이 코드를 실행하려면 먼저 파이썬 서버를 터미널에서 실행하고, 유니티 에디터의 Play 버튼을 누르면 됩니다. 이 강력한 기초를 바탕으로 게임 내 캐릭터의 위치를 파이썬으로 보내거나, 파이썬의 AI 모델이 계산한 결과를 다시 받아 게임 캐릭터의 행동을 제어하는 등 다양한 시나리오를 구현해 보세요.

댓글 남기기