현대 게임 개발에서 유니티와 파이썬의 연동은 단순한 편의를 넘어선 필수적인 기술로 자리 잡고 있습니다. 유니티는 게임의 시각적, 물리적 구현을 담당하는 ‘몸’이라면, 파이썬은 복잡한 데이터 분석, 머신러닝, 인공지능 로직을 처리하는 ‘두뇌’ 역할을 수행합니다. 이 두 시스템을 연결하는 통신망으로, 가장 신뢰성 높은 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. 심화 원리 및 실행 가이드
- 멀티스레딩의 중요성: 유니티에서
TcpClient
의GetStream()
은 데이터가 올 때까지 무한정 기다리는 블로킹(blocking) 함수입니다. 이 함수를 메인 스레드에서 직접 호출하면 게임이 멈추게 됩니다. 따라서,Thread
를 사용해 백그라운드에서 데이터를 수신하는 것이 필수적입니다. 파이썬 서버 역시threading
을 사용하여 여러 클라이언트의 요청을 동시에 처리할 수 있는 구조를 만듭니다. - JSON 직렬화/역직렬화: 게임 오브젝트의 위치(Vector3)와 같은 복잡한 데이터를 네트워크를 통해 전송하려면 이를 문자열로 변환하는 과정이 필요합니다. C#에서는
[Serializable]
속성과 **JsonUtility.ToJson()
**을, 파이썬에서는 **json.loads()
**와 **json.dumps()
**를 사용해 데이터를 주고받습니다. JSON은 사람도 읽기 쉬우면서 모든 언어에서 지원하는 표준 형식이라서 두 플랫폼 간의 데이터 교환에 최적입니다. - 이벤트 기반 데이터 처리:
PythonClient
스크립트의OnDataReceived
이벤트는 백그라운드 스레드에서 수신한 데이터를 유니티의 메인 스레드로 안전하게 전달하는 역할을 합니다. 이렇게 하면 네트워크 통신 로직과 게임 로직을 분리하여 더욱 견고한 코드를 만들 수 있습니다.
이 코드를 실행하려면 먼저 파이썬 서버를 터미널에서 실행하고, 유니티 에디터의 Play 버튼을 누르면 됩니다. 이 강력한 기초를 바탕으로 게임 내 캐릭터의 위치를 파이썬으로 보내거나, 파이썬의 AI 모델이 계산한 결과를 다시 받아 게임 캐릭터의 행동을 제어하는 등 다양한 시나리오를 구현해 보세요.