1. 파일다루기
2. 네트워크 프로그래밍
3. 데이터베이스 연동
3.1 DBMS의 이해와 MYSQL 설치하기
데이터베이스(Database)는 데이터를 체계적으로 저장, 관리, 그리고 검색할 수 있도록 구조화한 데이터의 집합이다. 이는 데이터의 중복을 최소화하고 효율적인 액세스를 보장하며, 여러 사용자와 애플리케이션이 데이터를 공유할 수 있도록 설계된다. 데이터베이스는 일반적으로 데이터의 저장, 수정, 삭제, 검색을 처리하는 데이터베이스 관리 시스템(DBMS)에 의해 관리되며, 관계형 데이터베이스, NoSQL 데이터베이스 등 다양한 유형으로 분류될 수 있다. 이를 통해 데이터 무결성과 보안이 유지되며, 대량의 데이터를 효율적으로 처리하는 데 필수적인 역할을 한다. 데이터베이스의 특징은 다음과 같다.
- 데이터의 통합성: 데이터는 중복을 최소화하고 일관성을 유지하도록 중앙 집중적으로 관리된다.
- 데이터의 공유성: 여러 사용자나 애플리케이션이 동일한 데이터를 동시에 접근하고 사용할 수 있다.
- 데이터 독립성: 데이터와 이를 사용하는 응용 프로그램이 분리되어 데이터 구조 변경이 프로그램에 영향을 주지 않는다.
- 데이터 무결성: 데이터의 정확성과 일관성을 유지하기 위해 제약 조건을 설정하고 관리한다.
- 데이터 보안성: 사용자 권한을 설정하여 데이터를 보호하며, 인증 및 접근 제어를 통해 무단 사용을 방지한다.
- 효율적인 데이터 처리: 데이터 검색, 삽입, 삭제, 수정 등의 작업이 효율적으로 수행되도록 최적화된다.
- 백업과 복구: 시스템 오류나 장애 발생 시 데이터를 보호하고 복구할 수 있는 기능을 제공한다.
- 다양한 데이터 처리: 관계형, 비관계형 데이터 등 여러 유형의 데이터를 처리할 수 있는 유연성을 갖춘다.
1. 구조화 데이터 전송 (Structured Data Transmission)

유니티에서 도출한 데이터를 기록하는 방법에는 여러 가지가 있다. 첫 번째로, StreamWriter를 사용하여 로컬 시스템에 데이터를 저장하는 방법이 있다. 이를 통해 텍스트 파일로 데이터를 기록하거나 로그를 남길 수 있다. 두 번째로, HTTP 요청을 통해 서버와 통신하여 데이터를 전송하고, 서버 측에서 이를 처리하여 데이터베이스나 파일 시스템에 저장하는 방식이다. 마지막으로, 클라우드 저장소를 이용해 데이터를 기록하는 방법도 있다. 클라우드 서비스를 활용하면, 인터넷을 통해 언제 어디서나 데이터에 접근할 수 있으며, AWS, Google Cloud, Azure와 같은 서비스를 통해 데이터를 안정적으로 저장하고 관리할 수 있다. 이 외에도 각 저장 방식은 데이터의 크기, 보안 요구 사항, 접근성 등에 따라 선택할 수 있다.
1. C# StreamWriter 클래스를 통한 로컬 경로에 저장
1. 파일 입출력
C#에서 파일 입출력을 위해 StreamReader와 StreamWriter 클래스를 사용할 수 있다. StreamReader는 텍스트 파일을 읽기 위한 버퍼링된 문자 스트림을 제공하며, ReadLine() 메서드를 통해 파일의 각 줄을 읽거나 ReadToEnd() 메서드로 전체 텍스트를 한 번에 읽을 수 있다. 반면, StreamWriter는 텍스트 파일에 데이터를 쓰기 위한 스트림을 제공하며, Write()와 WriteLine() 메서드를 통해 파일에 텍스트를 쓸 수 있다. 이 두 클래스는 System.IO 네임스페이스에 포함되어 있으므로 파일 입출력을 사용하기 위해서는 코드 상단에 using System.IO; 구문을 추가해야 한다.
using System.IO;
1. 파일 읽기
파일을 읽기 위해서 StreamReader를 사용한다. 파일 경로를 통해 새로운 StreamReader 개체를 생성하자.
void ReadFile(string path)
{
var sr = new StreamReader(path);
}
StreamReader의 생성자에 전달되는 path는 파일 경로와 파일명(파일 이름 + 확장자)을 포함하는 전체 경로여야 한다. 예를 들어, "C:\\example\\myfile.txt"와 같이 지정할 수 있다. 단, 파일이 현재 실행 중인 프로그램(*.exe)과 같은 디렉터리에 존재한다면, 파일명만을 지정하여 "myfile.txt"로 사용할 수도 있다. 파일이 존재하지 않거나 다른 프로세스에서 사용 중인 경우에는 IOException 예외가 발생하므로, 파일 접근 전 예외 처리가 필요하다.
StreamReader로 파일을 읽는 방법은 크게 세 가지가 있다. 첫 번째는 ReadLine() 메서드를 사용해 파일의 각 줄을 한 줄씩 읽어오는 방식이다. 두 번째는 ReadToEnd() 메서드를 사용해 파일의 전체 내용을 한 번에 읽어오는 방법이다. 세 번째는 Read() 메서드를 통해 파일을 문자 하나씩 읽어오는 방법이다. 각 방식은 읽어야 할 데이터의 양이나 형태에 따라 선택적으로 사용할 수 있다.
(1) 한 문자씩 읽기 - Read()
void ReadFile(string path)
{
var sr = new StreamReader(path);
richTextBox1.AppendText(((char)sr.Read()).ToString());
}
StreamReader의 Read() 메서드는 파일 스트림의 현재 위치(Position)에서 문자 하나를 읽고, Position을 한 칸 전진시킨다. 이 Position은 Stream.Position 속성으로, 스트림 내 현재 문자 위치를 의미하며 StreamReader.BaseStream.Position을 통해 이 값을 직접 확인할 수 있다. Read() 메서드는 읽은 문자의 아스키 값을 int로 반환하므로 일반적으로 char로 형 변환하여 실제 문자를 출력할 수 있다. 파일의 끝에 도달하여 더 이상 읽을 문자가 없을 경우에는 -1을 반환하여 스트림의 끝을 알린다.
이 방법으로 파일의 끝까지 읽기 위해서는 아래처럼 작성하면 된다.
void ReadFile(string path)
{
var sr = new StreamReader(path);
int c;
while(true)
{
c = sr.Read();
if(c == -1) break;
richTextBox1.AppendText(((char)c).ToString());
}
sr.Close();
}
void ReadFile(string path)
{
var sr = new StreamReader(path);
int c;
while((c = sr.Read()) != -1)
{
richTextBox1.AppendText(((char)c).ToString());
}
sr.Close();
}
(2) 한 줄씩 읽기 - ReadLine()
void ReadFile(string path)
{
var sr = new StreamReader(path);
richTextBox1.AppendText(sr.ReadLine());
}
StreamReader의 ReadLine() 메서드는 파일 스트림의 현재 위치(Position)부터 개행 문자(줄 바꿈)를 만나기 전까지 한 줄의 문자열을 읽는다. 읽은 문자열 뒤에 개행 문자는 포함되지 않으며 Position은 다음 줄의 시작 위치로 이동한다. ReadLine()의 반환 값은 읽은 문자열이며, 파일 끝에 도달해 더 이상 읽을 줄이 없을 경우에는 null을 반환하여 스트림의 끝임을 알린다.
마찬가지로 아래와 같이 파일을 끝까지 읽을 수 있다.
void ReadFile(string path)
{
var sr = new StreamReader(path);
string line;
while((line = sr.ReadLine()) != null)
{
richTextBox1.AppendText(line + Environment.NewLine);
}
sr.Close();
}
StreamReader의 Read() 메서드는 파일에서 한 문자씩 읽기 때문에, 개행 문자(줄 바꿈)도 포함하여 읽는다. 반면, ReadLine() 메서드는 개행 문자를 포함하지 않고, 개행 전까지의 문자열만을 반환한다. 따라서 파일의 내용을 원본 그대로 복사하려면, ReadLine()으로 읽은 후 개행을 유지하려면 각 줄 끝에 개행 문자를 직접 추가해야 한다. 일반적으로 Environment.NewLine을 사용하여 시스템에 맞는 개행 문자를 추가할 수 있다.
(3) 전체를 한 번에 읽기 - ReadToEnd()
void ReadFile(string path)
{
var sr = new StreamReader(path);
richTextBox1.AppendText(sr.ReadToEnd());
}
treamReader의 ReadToEnd() 메서드는 스트림의 현재 위치(Position)부터 파일 끝까지의 모든 내용을 한 번에 읽어온다. 이 때문에 파일 전체를 읽어 출력하거나 간단히 처리할 경우 ReadToEnd()를 사용하여 빠르게 작업할 수 있다. 파일을 모두 사용한 후에는 반드시 Close() 메서드를 호출해 파일 스트림을 닫아야 하며, using 블록을 사용하면 자동으로 닫히므로 권장된다.
void ReadFile(string path)
{
var sr = new StreamReader(path);
// ... 파일 사용 ...
sr.Close();
}
void ReadFile(string path)
{
using (var sr = new StreamReader(path))
{
// ... 파일 사용 ...
}
}
2. 파일 쓰기
파일에 텍스트를 쓰기 위해서는 StreamWriter 클래스를 사용할 수 있다. 먼저 파일 경로를 지정하여 StreamWriter 개체를 생성하는데 이때 지정된 경로에 파일이 존재하지 않으면 새 파일을 생성하고, 존재하면 기본적으로 덮어쓰기를 한다. StreamWriter 개체를 통해 Write()와 WriteLine() 메서드를 사용하여 파일에 텍스트를 작성할 수 있다.
void WriteFile(string path, string text)
{
var sw = new StreamWriter(path);
}
StreamWriter의 생성자에 지정하는 path는 StreamReader와 동일하게 파일 경로와 파일명(파일 이름 + 확장자)을 포함한 전체 경로를 사용한다. 지정된 경로에 파일이 존재하지 않는다면 해당 경로에 새로운 파일이 생성된다. StreamWriter를 통해 파일에 데이터를 쓰는 방법은 Write()와 WriteLine() 두 가지가 있다. Write()는 텍스트를 개행 없이 파일에 작성하고, WriteLine()은 텍스트 뒤에 자동으로 개행 문자를 추가하여 줄 단위로 작성한다. 필요에 따라 두 메서드를 적절히 조합해 텍스트를 파일에 기록할 수 있다.
(1) 일반적인 쓰기 - Write()
void WriteFile(string path, string text)
{
var sw = new StreamWriter(path);
sw.Write(text);
}
StreamWriter.Write() 메서드는 지정된 값을 스트림에 쓰는 메서드로, 다양한 오버로드된 형태를 제공한다. 가장 흔히 사용되는 메서드는 Write(string value)로, 지정된 문자열을 그대로 파일에 기록한다. Write(int value), Write(double value)와 같은 텍스트가 아닌 값을 매개변수로 갖는 메서드는 내부적으로 ToString() 메서드를 호출하여 값을 문자열로 변환한 후 스트림에 기록한다. 예를 들어, sw.Write(123)을 호출하면 정수 123의 문자열 표현인 "123"이 파일에 쓰인다. (물론, "123"에서 큰따옴표는 포함되지 않는다.
(2) 줄 단위로 쓰기 - WriteLine()
void WriteFile(string path, string text)
{
var sw = new StreamWriter(path);
sw.WriteLine(text);
}
StreamWriter.WriteLine() 메서드는 StreamWriter.Write()와 사용법이 거의 동일하지만, 주요 차이점은 문자열 뒤에 자동으로 개행 문자를 추가한다는 점이다. 즉, WriteLine()은 기본적으로 '개행문자'를 하나의 입력값으로 가지므로, 매개변수가 없는 형태로 호출하면 줄바꿈만 수행된다. 이 때문에 WriteLine()은 총 18개의 오버로드된 메서드를 제공한다. 예를 들어, WriteLine()을 사용하면 텍스트를 파일에 기록한 후 자동으로 줄 바꿈이 이루어지고, 매개변수가 없는 WriteLine() 호출은 단순히 줄 바꿈만 한다.
void WriteFile(string path, string text)
{
var sw = new StreamWriter(path);
sw.WriteLine(text);
sw.WriteLine(); // 줄바꿈
sw.WriteLine(15); // 15
}
StreamWriter를 사용한 후에는 반드시 Close() 메서드를 호출하여 스트림을 닫아줘야 한다. 파일 작업이 완료된 후 스트림을 닫지 않으면 파일이 제대로 저장되지 않거나 시스템 리소스가 낭비될 수 있다. using 구문을 사용하면 스트림이 자동으로 닫히지만, 만약 using을 사용하지 않는 경우에는 명시적으로 Close()를 호출하여 파일 스트림을 닫아주는 것이 좋다. 이를 통해 파일에 대한 변경 사항이 정상적으로 반영되고, 리소스가 해제된다.
private List<float> gazedCorrectly = new List<float>(); // 제대로 응시한 값
private List<float> gazedIncorrectly = new List<float>(); // 제대로 응시하지 않은 값
// 기록할 파일 경로
private string filePath;
void Start()
{
// 파일 경로 설정
filePath = Application.dataPath + "/GazeData.csv";
// 기존 파일이 있으면 덮어쓰지 않도록 체크 (파일이 없다면 새로 생성)
if (File.Exists(filePath))
{
File.Delete(filePath);
}
// 첫 번째 줄에 헤더 작성
WriteHeaderToCSV();
// 예시 데이터 추가 (시뮬레이션)
gazedCorrectly.Add(0.5f);
gazedCorrectly.Add(1.2f);
gazedIncorrectly.Add(0.2f);
gazedIncorrectly.Add(0.7f);
// 데이터를 시계열 형태로 파일에 기록
WriteDataToCSV();
}
void WriteHeaderToCSV()
{
// CSV 파일의 첫 번째 줄에 헤더 추가
using (StreamWriter writer = new StreamWriter(filePath, true))
{
writer.WriteLine("제대로 응시한 값,제대로 응시하지 않은 값");
}
}
void WriteDataToCSV()
{
// 시계열 형태로 데이터를 파일에 기록
using (StreamWriter writer = new StreamWriter(filePath, true))
{
int maxLength = Mathf.Max(gazedCorrectly.Count, gazedIncorrectly.Count);
for (int i = 0; i < maxLength; i++)
{
// 각 배열에서 해당하는 값이 없으면 0으로 대체
string correctlyGazed = (i < gazedCorrectly.Count) ? gazedCorrectly[i].ToString() : "0";
string incorrectlyGazed = (i < gazedIncorrectly.Count) ? gazedIncorrectly[i].ToString() : "0";
// 시계열 형태로 기록
writer.WriteLine(correctlyGazed + "," + incorrectlyGazed);
}
}
}
3. 파일에 이어서 쓰기
StreamWriter는 기본적으로 Append 옵션을 제공하므로, 이미 존재하는 파일에 내용을 이어서 쓸 수 있다. StreamWriter를 생성할 때 두 번째 매개변수로 true를 전달하면, 기존 파일의 끝에 새로운 데이터를 추가하는 방식으로 동작한다. 이 방식은 StreamReader로 파일을 먼저 읽고 내용을 보관한 후, 스트림을 닫고 StreamWriter로 파일을 열어 내용을 덧붙이는 복잡한 과정을 거칠 필요 없이, 간단히 파일에 내용을 이어쓸 수 있게 해준다. 또한, Append 옵션을 사용하면 예외 처리를 간단하게 할 수 있으며, 파일을 덧붙이는 과정이 안전하게 이루어진다.
void WriteFile(string path, string text)
{
var sw = new StreamWriter(path, true);
sw.Write(text);
}
StreamWriter의 오버로드된 생성자 중에 StreamWriter(string path, bool append)가 있다. 여기서 append 옵션을 위 코드와 같이 true로 하면 파일을 덮어쓰지 않고 내용을 파일의 뒤에 추가하게 된다. 이후에는 StreamWriter의 사용법을 그대로 사용한다.
2. HTTP 네트워크 통신을 통한 서버에 저장

HTTP 서버에 원시 데이터 업로드(PUT) - Unity 매뉴얼
일부 최신 웹 애플리케이션은 HTTP PUT 동사를 통해 파일을 업로드하는 것을 선호합니다. 이 시나리오의 경우 Unity는 UnityWebRequest.PUT 함수를 제공합니다.
docs.unity3d.com
유니티에서 HTTP 네트워크 통신을 통해 서버에 파일을 저장하려면, UnityWebRequest를 사용하여 HTTP POST 요청을 보내고, 서버 측에서는 이를 처리할 수 있는 API 엔드포인트를 구현해야 한다. 서버는 PHP, Python, Node.js와 같은 언어로 파일을 받는 API를 만들어, 클라이언트로부터 전송된 파일을 받아 지정된 위치에 저장하거나 필요한 처리를 수행한 후 응답을 반환한다. 이렇게 HTTP 요청을 통해 파일을 서버에 업로드하면, 로컬 저장소 대신 원격 서버에 데이터를 안전하게 저장할 수 있다.
1. www 클래스
WWW 클래스는 유니티에서 웹 서버와의 통신을 간편하게 처리할 수 있도록 도와주는 클래스로, 주로 웹 페이지 데이터를 다운로드하거나 이미지 파일을 불러올 때 사용된다. 그러나 현재 유니티는 WWW 클래스 사용을 권장하지 않으며, 대신 UnityWebRequest 클래스를 사용하도록 권장하고 있다. UnityWebRequest는 더 유연하고 기능이 풍부하며, 다양한 프로토콜을 지원하는 장점이 있다.
2. UnityWebRequest 클래스
UnityWebRequest 클래스는 유니티에서 HTTP 통신을 수행할 수 있게 해주는 클래스이며, GET, POST와 같은 다양한 HTTP 요청 방식을 지원한다. 이를 통해 파일 다운로드, 업로드 등 다양한 작업을 처리할 수 있다. 유니티에서는 이전에 WWW 클래스를 사용했으나, 현재는 UnityWebRequest를 사용하는 것이 권장된다. UnityWebRequest는 더욱 유연하고 효율적인 방식으로 네트워크 통신을 처리할 수 있기 때문이다.
2.1 GET 방식
GET 방식은 주로 서버에서 정보를 조회할 때 사용되며, 필요한 매개변수를 URL에 포함시켜 서버로 전송한다. 이 방식은 전송할 수 있는 데이터 크기가 제한적이고, 보안상 민감한 데이터를 전송하기에는 적합하지 않을 수 있다. UnityWebRequest를 사용하면 GET 요청을 보내고, 요청이 성공하면 서버로부터 응답된 데이터를 로그로 출력할 수 있다.
using UnityEngine.Networking;
IEnumerator GetRequest(string uri)
{
using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
{
// 요청 보내기
yield return webRequest.SendWebRequest();
if (webRequest.isNetworkError || webRequest.isHttpError)
{
Debug.LogError(webRequest.error);
}
else
{
Debug.Log(webRequest.downloadHandler.text);
}
}
}
2.2 POST 방식
POST 방식은 서버의 데이터 생성이나 업데이트를 위해 사용되며, 데이터를 요청 본문(body)에 포함해 전송하므로 URL에 노출되지 않는다. 이 방식은 더 많은 데이터를 안전하게 전송할 수 있어 파일 업로드 등에 유용하다. UnityWebRequest를 사용하여 POST 요청을 보내는 코드에서는 postData에 서버로 전송할 데이터가 포함된다.
using UnityEngine.Networking;
using System.Collections;
IEnumerator PostRequest(string uri, string postData)
{
using (UnityWebRequest webRequest = UnityWebRequest.Post(uri, postData))
{
// 요청 보내기
yield return webRequest.SendWebRequest();
if (webRequest.isNetworkError || webRequest.isHttpError)
{
Debug.LogError(webRequest.error);
}
else
{
Debug.Log("Form upload complete!");
}
}
}
3. UploadHandler
UploadHandler는 UnityWebRequest 시스템에서 클라이언트의 데이터를 서버로 전송하는 데 사용되며, 주로 POST 요청에서 데이터를 "업로드"할 때 활용된다. 예를 들어, 사용자가 입력한 데이터를 서버로 전송할 때 UploadHandler를 구성하여 사용할 수 있다. 유니티에서는 UploadHandler의 다양한 유형을 지원하며, UploadHandlerRaw는 JSON이나 텍스트 데이터를 전송할 때, UploadHandlerFile은 파일을 전송할 때, UploadHandlerFormData는 폼 데이터와 파일을 함께 전송할 때 유용하게 사용된다.
3.1 UploadHandlerRaw
UploadHandlerRaw는 서버에 바이트 배열 데이터를 원시 형태로 업로드할 때 사용되는 유니티의 기능으로, 주로 JSON 형식의 데이터나 일반 텍스트 데이터를 서버로 전송하는 데 유용하다. 예를 들어, 클라이언트에서 사용자 입력을 JSON 형식으로 만들어 서버에 전송해야 하는 상황을 생각해볼 수 있다. 이때 UploadHandlerRaw를 사용하여 JSON 데이터를 바이트 배열로 변환한 뒤 서버로 보낼 수 있으며, contentType을 "application/json"으로 설정해 서버가 이 데이터가 JSON 형식임을 인식하게 할 수 있다. 이를 통해 서버와의 데이터 통신을 간편하게 수행할 수 있다.
using UnityEngine;
using UnityEngine.Networking;
public class UploadRawDataExample : MonoBehaviour
{
void Start()
{
StartCoroutine(Upload());
}
IEnumerator Upload()
{
byte[] myData = System.Text.Encoding.UTF8.GetBytes("이것은 테스트 데이터입니다.");
UnityWebRequest www = new UnityWebRequest("http://www.your-server.com/upload", "POST");
UploadHandlerRaw uhr = new UploadHandlerRaw(myData);
uhr.contentType = "application/json";
www.uploadHandler = uhr;
www.downloadHandler = new DownloadHandlerBuffer();
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
Debug.Log("Upload complete!");
}
}
}
3.2 UploadHandlerFile
UploadHandlerFile은 클라이언트가 서버로 파일을 직접 업로드할 때 유용하게 사용되는 기능이다. 예를 들어, 사용자가 앱 내에서 사진 파일을 서버에 업로드하려는 경우, UploadHandlerFile을 사용해 로컬 파일 경로를 지정하면 해당 파일 데이터를 서버로 전송할 수 있다. 사용자는 파일을 별도로 메모리에 불러오지 않아도 되므로 메모리 사용을 줄이고 효율적으로 파일을 업로드할 수 있다. 서버 측에서 이 파일을 특정한 폴더나 데이터베이스에 저장하고, 이후 처리나 분석에 활용하는 방식으로 응용할 수 있다.
using UnityEngine;
using UnityEngine.Networking;
public class UploadFileExample : MonoBehaviour
{
void Start()
{
StartCoroutine(Upload());
}
IEnumerator Upload()
{
string filePath = "path/to/your/file.txt";
UnityWebRequest www = UnityWebRequest.Put("http://www.your-server.com/upload", filePath);
www.uploadHandler = new UploadHandlerFile(filePath);
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
Debug.Log("File upload complete!");
}
}
}
3.3 UploadHandlerFormData
UploadHandlerFormData는 폼 데이터와 파일을 함께 서버로 업로드할 때 유용하며, 특히 멀티파트 폼 데이터 형식을 통해 데이터를 전송해야 할 경우 사용된다. 예를 들어, 사용자가 앱에서 프로필 정보를 입력하고 사진 파일을 업로드하려는 상황을 생각해볼 수 있다. 이때 UploadHandlerFormData를 사용하면, 이름, 이메일 등 텍스트 데이터와 함께 프로필 사진 파일을 하나의 요청으로 서버에 전송할 수 있다. 서버는 이를 멀티파트 형식으로 받아 텍스트 정보와 파일을 각각 처리할 수 있어, 사용자 정보를 포함한 파일 업로드 작업을 간편하게 처리할 수 있다.
using UnityEngine;
using UnityEngine.Networking;
using System.Collections.Generic;
public class UploadFormDataExample : MonoBehaviour
{
void Start()
{
StartCoroutine(Upload());
}
IEnumerator Upload()
{
List<IMultipartFormSection> formData = new List<IMultipartFormSection>();
formData.Add(new MultipartFormDataSection("field1=foo&field2=bar"));
formData.Add(new MultipartFormFileSection("myFile", "이것은 파일 데이터입니다.", "text/plain"));
UnityWebRequest www = UnityWebRequest.Post("http://www.your-server.com/upload", formData);
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
Debug.Log("Form upload complete!");
}
}
}
4. DownloadHandler
DownloadHandler는 서버로부터 데이터를 받아 클라이언트로 "다운로드"하는 역할을 합니다. 서버의 응답 데이터를 처리하고, 필요한 형태로 변환하여 애플리케이션에 제공합니다. 또한 유니티에서는 특정 유형의 데이터 처리에 최적화되어 있는 다운로드 핸들러를 제공하고 있습니다. 여기에는 DownloadHandlerBuffer, DownloadHandlerFile, DownloadHandlerAssetBundle 가 있습니다. DownloadHandlerBuffer는 텍스트나 바이너리 데이터 처리에, DownloadHandlerFile은 파일 다운로드에, 그리고 DownloadHandlerAssetBundle은 애셋 번들 다운로드와 관리에 적합합니다.
4.1 DownloadHandlerBuffer
DownloadHandlerBuffer는 서버의 응답을 메모리 버퍼에 저장합니다. 이는 주로 텍스트나 바이너리 데이터를 처리할 때 사용됩니다. 저장된 데이터는 문자열이나 바이트 배열로 추출할 수 있습니다.
using UnityEngine;
using UnityEngine.Networking;
public class DownloadBufferExample : MonoBehaviour
{
void Start()
{
StartCoroutine(Download());
}
IEnumerator Download()
{
UnityWebRequest www = UnityWebRequest.Get("http://www.your-server.com/data");
www.downloadHandler = new DownloadHandlerBuffer();
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
// 텍스트 데이터로 추출
string textData = www.downloadHandler.text;
Debug.Log(textData);
// 또는 바이트 배열로 추출
byte[] binaryData = www.downloadHandler.data;
}
}
}
4.2 DownloadHandlerFile
DownloadHandlerFile은 서버의 응답을 직접 파일로 저장합니다. 이는 대용량 데이터나 파일을 다운로드할 때 유용합니다. 다운로드 과정에서 메모리 사용량을 최소화할 수 있습니다.
using UnityEngine;
using UnityEngine.Networking;
public class DownloadFileExample : MonoBehaviour
{
void Start()
{
StartCoroutine(Download());
}
IEnumerator Download()
{
string filePath = "path/to/save/the/file.txt";
UnityWebRequest www = new UnityWebRequest("http://www.your-server.com/file", UnityWebRequest.kHttpVerbGET);
www.downloadHandler = new DownloadHandlerFile(filePath);
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
Debug.Log("File download complete!");
}
}
}
4.3 DownloadHandlerAssetBundle
DownloadHandlerAssetBundle은 애셋 번들을 다운로드하고, 애셋 번들로부터 애셋을 로드할 때 사용됩니다. 애셋 번들은 여러 애셋을 묶어 놓은 컨테이너로, 게임 리소스를 효율적으로 관리할 수 있게 해줍니다.
using UnityEngine;
using UnityEngine.Networking;
public class DownloadAssetBundleExample : MonoBehaviour
{
void Start()
{
StartCoroutine(Download());
}
IEnumerator Download()
{
string bundleUrl = "http://www.your-server.com/assetbundle";
UnityWebRequest www = UnityWebRequestAssetBundle.GetAssetBundle(bundleUrl);
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.Success)
{
Debug.Log(www.error);
}
else
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(www);
// 애셋 번들에서 애셋 로드하기
// 예: bundle.LoadAsset<GameObject>("MyAsset");
}
}
}
5. UnityWebRequest의 작업 흐름
클라이언트 설정(Create/Configure) : UnityWebRequest 인스턴스를 생성하고, GET이나 POST 등의 HTTP 메서드를 설정합니다. 필요한 경우 요청 헤더를 추가하고, UploadHandler를 통해 전송할 데이터를 구성합니다.
전송(Send/Abort) : 구성된 요청을 서버로 전송합니다. 전송 과정은 비동기적으로 진행되므로, 게임의 메인 루프에 영향을 주지 않습니다. 전송 중에 요청을 중단할 수도 있습니다.
처리(Receive/Buffer/Process) : 서버로부터의 응답을 받습니다. DownloadHandler를 사용하여 서버의 응답을 적절한 형태로 처리합니다.
완료 : 모든 통신 과정이 완료되면, UnityWebRequest 객체는 사용한 자원을 해제하고, 결과를 반환합니다
3. 데이터베이스 통합 (Database Integration)
4. 프로세스 간 통신 (Inter-process Communication)

루프백 통신은 동일한 기기 내에서 데이터를 주고받는 네트워크 연결 방법이다. 일반적으로 IP 주소 127.0.0.1을 사용하며, 이를 통해 컴퓨터가 외부 네트워크나 인터넷을 거치지 않고 자체적으로 데이터를 송수신할 수 있다. 루프백은 네트워크 개발과 테스트에 유용한데, 서버와 클라이언트 역할을 모두 하나의 기기에서 수행할 수 있어 외부 환경과의 연결 없이 로컬 네트워크 통신을 모사할 수 있기 때문이다. 이 방식을 활용하면 파이썬에서 보낸 데이터를 유니티에서 쉽게 수신할 수 있으며, 외부 네트워크의 영향을 받지 않으므로 지연 시간이 짧고 안정적이다.


이 구조에서 Unity는 서버로서 Python의 연결 요청을 기다리며, Python은 클라이언트 역할로 Unity에 연결을 설정한다. 루프백 통신을 통해 연결이 확립되면 Python은 좌표 데이터 또는 바이트 형태의 데이터를 Unity로 보낼 수 있다. Unity는 Python으로부터 수신된 데이터를 즉시 처리하며, 이를 기반으로 씬에 있는 큐브의 위치를 업데이트한다. 이와 같은 구조는 Unity와 Python 간의 실시간 데이터 전송을 가능하게 하여, Python에서 계산된 좌표 값을 실시간으로 Unity 씬의 오브젝트 위치에 반영하는 등 다양한 상호작용을 구현할 수 있도록 한다.
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
using System.Threading;
public class MyListener : MonoBehaviour
{
Thread thread;
public int connectionPort = 25001;
TcpListener server;
TcpClient client;
bool running;
private void Start()
{
// Receive on a separate thread so Unity doesn't freeze waiting for data
ThreadStart ts = new ThreadStart(GetData);
thread = new Thread(ts);
thread.Start();
}
void GetData()
{
// Create the server
server = new TcpListener(IPAddress.Any, connectionPort);
server.Start();
// Create a Clinent to get the data stream
client = server.AcceptTcpClient();
// Start listening
running = true;
while(running)
{
Connection();
}
server.Stop();
Debug.Log("서버 스탑");
}
// Position is the data being received in this example
Vector3 position = Vector3.zero;
void Connection()
{
// Read data from the network stream
NetworkStream nwStream = client.GetStream();
byte[] buffer = new byte[client.ReceiveBufferSize];
int bytesRead = nwStream.Read(buffer, 0, client.ReceiveBufferSize);
// Decode the bytes int a string
string dataReceived = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// Make sure we're not getting an empty string
// dataReceived.Trim();
if (dataReceived != null && dataReceived != "")
{
// Convert the received string of data to the format we are using
position = ParseData(dataReceived);
nwStream.Write(buffer, 0, bytesRead);
}
}
public static Vector3 ParseData(string dataString)
{
// Remove the parenthes
if (dataString.StartsWith("(") && dataString.EndsWith(")"))
{
dataString = dataString.Substring(1, dataString.Length - 2);
Debug.Log(dataString);
}
// Split the elements into an array
string[] stringArray = dataString.Split(',');
// Store as a Vector3
Vector3 result = new Vector3(
float.Parse(stringArray[0]),
float.Parse(stringArray[1]),
float.Parse(stringArray[2]));
return result;
}
private void Update()
{
// Set this Object's position in the scene according to the position received
transform.position = position;
}
}
Thread thread;
public int connectionPort = 25001;
TcpListener server;
TcpClient client;
bool running;
Thread는 메인 쓰레드와 별도로 서버 전용 쓰레드를 생성하여 네트워크 처리를 담당하는 역할을 한다. 이를 통해 메인 쓰레드가 Unity의 다른 작업을 처리하는 동안에도 네트워크 연결을 지속적으로 대기할 수 있으며, Unity가 실행되지 않더라도 데이터를 수신할 수 있는 독립적인 서버 환경을 제공한다. connectionPort는 클라이언트가 서버에 접속하기 위해 사용하는 포트 번호를 지정하여, 동일한 포트 번호를 통해 Python 클라이언트가 서버에 연결할 수 있게 한다. TcpListener는 네트워크에서 클라이언트 연결 요청을 감지하고 수신하는 역할을 수행하며, TcpClient는 클라이언트와 서버 간에 데이터를 전송하고 수신하는 스트림을 생성하여 연결을 관리한다.
서버의 상태를 나타내는 running은 서버가 열려 있는지 여부를 지속적으로 확인해, 예를 들어 서버를 시작하거나 중지할 때 이를 통해 상태를 모니터링할 수 있다. 예를 들어, Unity에서 running이 true로 설정되면 TcpListener는 연결을 대기하고, Python 클라이언트가 connectionPort를 통해 연결 요청을 보낼 때 서버 쓰레드에서 이 연결을 수신하고 TcpClient로 데이터를 주고받아 큐브 위치를 업데이트하는 방식이다.
private void Start()
{
// Receive on a separate thread so Unity doesn't freeze waiting for data
ThreadStart ts = new ThreadStart(GetData);
thread = new Thread(ts);
thread.Start();
}
메서드를 별도의 스레드에서 실행하면 메인 프로그램의 흐름과 독립적으로 해당 메서드가 실행되기 때문에, CPU의 멀티스레딩 기능을 활용하여 동시 작업 처리가 가능해진다. 이를 통해 특정 작업(예: 네트워크 연결 대기, 데이터 수신 등)이 메인 스레드를 차지하지 않고도 병렬로 처리되므로, 메인 스레드는 Unity의 렌더링이나 사용자 입력 처리 같은 다른 작업을 지속적으로 수행할 수 있다. 예를 들어 Unity에서 네트워크 서버 메서드를 별도 스레드에서 실행하면 클라이언트의 연결 대기나 데이터 수신 과정이 메인 스레드를 방해하지 않으므로, 게임 씬이 끊김 없이 매끄럽게 작동하며 동시에 데이터 통신을 유지할 수 있다.
void GetData()
{
// Create the server
// 클라이언트 연결.
server = new TcpListener(IPAddress.Any, connectionPort);
server.Start();
// Create a Clinent to get the data stream
client = server.AcceptTcpClient();
// Start listening
running = true;
while(running)
{
Connection();
}
server.Stop();
Debug.Log("서버 스탑");
}
GetData 메서드는 서버를 생성하고 클라이언트로부터 데이터를 수신하는 기능을 담당한다. 이 메서드는 특정 포트를 동기화하고 IPAddress.Any를 파라미터로 전달하여 TcpListener 인스턴스를 생성함으로써 서버를 설정한다. IPAddress.Any는 현재 기기의 모든 네트워크 인터페이스(IP 주소)를 통해 들어오는 연결을 수신할 수 있음을 의미한다. 즉, 특정 IP에 제한되지 않고, 해당 컴퓨터에 연결된 모든 네트워크 경로에서 접근할 수 있도록 하여, 로컬 네트워크나 인터넷을 통해 자유롭게 클라이언트가 연결될 수 있다.
네트워크 인터페이스는 장치와 네트워크 간의 연결을 담당하는 역할을 한다. 이더넷 인터페이스는 유선 연결 방식으로, 이더넷 케이블을 통해 장치를 LAN 또는 WAN에 연결하며, 주로 컴퓨터와 서버에서 사용된다. Wi-Fi 인터페이스는 무선 통신을 지원하여 장치가 네트워크에 무선으로 연결되도록 한다. 루프백 인터페이스는 IP 주소 127.0.0.1을 사용하는 가상 인터페이스로, 장치가 자체적으로 통신하도록 해 주며, 테스트 및 진단에 자주 활용된다. VPN 인터페이스는 장치가 VPN을 통해 암호화된 트래픽을 전송할 때 사용하는 가상 인터페이스로, 장치와 VPN 서버 간의 안전한 통신을 위한 터널을 형성한다.
AcceptTcpClient()는 클라이언트 연결 요청이 수신되면 TcpClient 객체를 반환하여 연결을 성립한다. 이를 통해 실시간 데이터 통신을 지원하기 위해, 별도의 Connection() 메서드를 생성하여 지속적으로 클라이언트와 통신할 수 있도록 한다.
Vector3 position = Vector3.zero;
void Connection()
{
// Read data from the network stream
NetworkStream nwStream = client.GetStream();
byte[] buffer = new byte[client.ReceiveBufferSize];
int bytesRead = nwStream.Read(buffer, 0, client.ReceiveBufferSize);
// Decode the bytes int a string
string dataReceived = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// Make sure we're not getting an empty string
// dataReceived.Trim();
if (dataReceived != null && dataReceived != "")
{
// Convert the received string of data to the format we are using
position = ParseData(dataReceived);
nwStream.Write(buffer, 0, bytesRead);
}
}
Connection() 메서드는 클라이언트와의 네트워크 연결을 유지하며, 클라이언트로부터 지속적으로 데이터를 수신하기 위해 while문 내에서 반복 호출된다. 여기서 NetworkStream은 클라이언트로부터 전송된 바이트 스트림을 읽고 쓸 수 있는 기능을 제공하며, 수신된 데이터는 buffer라는 바이트 배열에 임시 저장된다. buffer의 크기는 client.ReceiveBufferSize로 확인할 수 있으며, 수신된 바이트 데이터는 UTF-8 인코딩을 통해 문자열로 변환되어 dataReceived에 저장된다. 이 변환된 문자열에는 파이썬에서 입력된 좌표값이 포함될 것이다.
스트림은 파일, 네트워크 연결, 사용자 입력 등에서 데이터를 읽거나 쓰는 순차적 데이터 흐름을 다루는 개념으로, 프로그래밍에서 데이터를 일정한 흐름으로 처리하기 위해 도입된 것이다. 예를 들어 음악을 재생할 때 음악 파일을 한 번에 전부 로드하는 대신 스트림으로 필요한 부분을 순서대로 읽어와 실시간 재생이 가능해진다. 이렇게 스트림은 데이터가 마치 물처럼 일정한 방향으로 흐르는 것을 상상하며, 데이터를 개별적으로 다루지 않고 끊임없는 흐름으로 처리할 수 있게 하여 파일 입출력, 네트워크 통신 등에서 일관성 있는 데이터 관리 방법을 제공하는 것이다.
public static Vector3 ParseData(string dataString)
{
// Remove the parenthes
if (dataString.StartsWith("(") && dataString.EndsWith(")"))
{
dataString = dataString.Substring(1, dataString.Length - 2);
Debug.Log(dataString);
}
// Split the elements into an array
string[] stringArray = dataString.Split(',');
// Store as a Vector3
Vector3 result = new Vector3(
float.Parse(stringArray[0]),
float.Parse(stringArray[1]),
float.Parse(stringArray[2]));
return result;
}
private void Update()
{
// Set this Object's position in the scene according to the position received
transform.position = position;
}
이 메서드는 좌표 값을 파싱하는 기능을 수행한다. 문자열을 파라미터로 받아, 불필요한 괄호나 공백 등을 제거한 후, 실시간으로 Vector3 값을 업데이트해준다. 이를 통해 클라이언트에서 받은 좌표 데이터를 적절하게 처리하여, 해당 좌표를 3D 공간에서 위치값으로 변환하여 업데이트하는 작업을 실시간으로 수행한다.
여기까지 유니티 전체 코드다. 다음은 파이썬으로 넘어간다.
아래는 1초마다 3차원상의 z값을 1씩 증가시켜서 유니티로 전송시키는 파이썬 코드다.
import socket
import time
host = "127.0.0.1"
port = 25001
x = 0
y = 0
z = 0
def join_string(x, y, z):
return f"{x},{y},{z}"
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
while True:
try:
z += 1
data = join_string(x, y, z)
sock.sendall(data.encode("utf-8"))
response = sock.recv(1024).decode("utf-8")
print(response)
except ConnectionResetError:
print("The server disconnected.")
# Add any necessary handling for the server disconnection here
except Exception as e:
print("An error occurred:", e)
time.sleep(1)
except ConnectionRefusedError:
print("Connection refused. Make sure the server is running.")
except Exception as e:
print("An error occurred:", e)
finally:
sock.close()
먼저, socket 라이브러리를 pip를 통해 설치하고 불러온다. 루프백 인터페이스 방식으로 통신을 처리하기 위해 host를 127.0.0.1로 설정하고, 서버에 접속하기 위해 포트 번호는 유니티와 동일한 25001로 설정한다. join_string 메서드는 3차원 좌표값을 int 타입으로 받아서 string 타입으로 반환하는 역할을 한다. sock는 AF_INET 주소 계열을 지정하여 스트림 지향 통신을 위한 TCP 소켓을 생성하며, 서버에 연결하기 위해 host와 port 번호를 파라미터로 전달하고 sock.connect()를 사용하여 서버에 연결한다. while문 내부에서는 data를 스트링 타입으로 반환한 후, 인코딩 과정을 거쳐 sock.sendall에 전달하여 데이터를 전송하며, sock.recv를 통해 서버로부터 받은 응답을 확인할 수 있다.

유니티와 파이썬을 순차적으로 실행시키면 큐브가 서서히 전진하는것을 확인할 수 있다.
5. 멀티미디어 통신 (Inter-process Communication)

Unity와 멀티미디어 간의 통신은 외부 소스(예: 카메라 스트림, 네트워크 스트림, 오디오/비디오 파일)에서 데이터를 가져와 이를 Unity 엔진 내부에서 처리하고 렌더링하는 과정을 말한다. 이 통신은 FFmpeg와 같은 멀티미디어 처리 도구를 활용해 이루어진다. FFmpeg는 다양한 형식의 멀티미디어 데이터를 읽고 Unity에서 사용할 수 있는 포맷(RGBA 비디오 데이터 등)으로 변환하여 제공하며, Unity는 이를 텍스처, 오디오 클립 등의 객체로 매핑해 씬에 렌더링하거나 상호작용 요소로 활용한다. 이를 통해 Unity는 실시간 스트리밍 데이터(RTMP, HLS 등)나 외부 멀티미디어 콘텐츠와의 원활한 통합을 지원하며, 특히 가상현실(VR), 게임, 인터랙티브 시뮬레이션과 같은 실시간성이 중요한 응용 분야에서 강력한 가능성을 제공한다.
FFmpeg는 다양한 멀티미디어 파일과 스트림을 처리할 수 있는 강력한 오픈소스 소프트웨어이다. 비디오, 오디오, 그리고 스트림 데이터를 변환하거나 처리할 때 사용되며, 이 코드에서는 FFmpeg를 통해 RTMP 스트림 데이터를 Unity로 가져오도록 한다. 간단히 말해, FFmpeg는 실시간으로 비디오 데이터를 받아 Unity에서 사용할 수 있는 형식으로 변환하는 작업을 한다.
'Development > 3D Engine Programming' 카테고리의 다른 글
Unity Programming [5] : 유니티 소프트웨어 설계 (Attribute, Gizmos) (1) | 2024.12.27 |
---|---|
Unity Programming [4] : AI 통합 게임 엔진 (AI-Integrated Game Engine) (3) | 2024.11.19 |
Unity Programming [3] : 시스템 로직 (Unity System Logics) (2) | 2024.11.15 |
Unity Programming [1]: 병행성 추상화 프레임워크 (Concurrency Abstraction Framework) (0) | 2024.10.28 |
Unity Programming [0] : C# 프로그래밍 (1) | 2024.10.11 |