이 문서는 Django 백엔드에서 비디오 데이터를 제공하고 Unity 클라이언트에서 스크롤 가능한 비디오 리스트를 구현하는 전체 과정을 설명합니다. Unity의 Scroll View, 프리팹, WebRequest를 활용하여 동적으로 비디오 카드를 생성하고 표시합니다.
1. Django 백엔드 API 준비
Unity가 요청할 비디오 데이터를 제공하는 Django API가 필요합니다. 예시 엔드포인트는 /user/get_user_video_statistics/이며, JSON 형식으로 응답해야 합니다. 응답 구조는 아래 데이터 모델과 일치해야 합니다.
2. Unity 데이터 모델 정의
JSON 응답을 역직렬화하기 위한 C# 클래스를 정의합니다. VideoListResponse, UserVideoData, VideoItem 및 하위 모델을 포함합니다.
using System;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class Episode
{
public int title;
public string url;
public string price;
}
[Serializable]
public class Comment
{
public string commentContent;
public string creationTime;
}
[Serializable]
public class Danmu
{
public string text;
public string color;
public string time;
}
[Serializable]
public class VideoItem
{
public int video_id;
public string avatar;
public string cover;
public string nickName;
public string title;
public string description;
public List<Comment> commentContent;
public List<Danmu> danmuList;
public List<Episode> episodes;
public string account;
public int user_id;
public string nickname;
public float peoplePlayCount;
public int followers_count;
public int total_videos;
public string total_play_count;
public int total_comments_count;
public string date;
public string total_thumbs_up;
public string total_dislikes;
public string total_mantou;
public string total_collections;
public int total_forwards;
}
[Serializable]
public class UserVideoData
{
public string account;
public int user_id;
public string avatar;
public string nickname;
public int followers_count;
public int total_videos;
public float total_play_count;
public int total_comments_count;
public string date;
public float total_thumbs_up;
public float total_dislikes;
public float total_mantou;
public float total_collections;
public int total_forwards;
public List<VideoItem> videos;
}
[Serializable]
public class VideoListResponse
{
public List<UserVideoData> data;
}
3. Scroll View UI 구성
Unity 에디터에서 Scroll View를 생성하고 Content 영역에 자동 레이아웃을 설정합니다.
- Hierarchy > UI > Scroll View를 선택합니다.
- Scroll View 크기를 조정하고(예: 800x600), Content 하위의 기본 Text 요소는 삭제합니다.
- Content 오브젝트에 Vertical Layout Group 컴포넌트를 추가합니다.
- Child Force Expand > Width를 체크하여 카드 너비를 Content에 맞춥니다.
- Child Force Expand > Height는 체크 해제하여 각 카드의 고유 높이를 유지합니다.
- Spacing을 10으로 설정하여 카드 간격을 줍니다.
- Padding을 (20, 20, 20, 20)으로 설정합니다.
- Content에 Content Size Fitter 컴포넌트를 추가합니다.
- Horizontal Fit을 Unconstrained로 설정합니다.
- Vertical Fit을 Preferred Size로 설정하여 Content 높이가 카드에 따라 자동 조절됩니다.
4. 비디오 카드 프리팹 생성
VideoCard 프리팹을 생성합니다.
- 빈 GameObject를 만들고 이름을
VideoCard로 지정합니다. - Image 컴포넌트를 추가하여 배경색(예: 밝은 회색)을 설정합니다. 크기는 700x200으로 설정합니다.
- Button 컴포넌트를 추가하여 클릭 이벤트를 처리합니다.
- 자식 UI 요소 추가:
Cover(Image): 비디오 썸네일 표시 (크기 200x180).Title(TextMeshPro - Text): 비디오 제목 (크기 450x40).Description(TextMeshPro - Text): 비디오 설명 (크기 450x40).Author(TextMeshPro - Text): 작성자 이름 (크기 200x20).ViewCount(TextMeshPro - Text): 조회수 (크기 100x20, 우측 정렬).
- 완성된
VideoCard를Assets/Resources폴더에 프리팹으로 저장합니다.
5. 메인 관리 스크립트 작성
아래 스크립트는 API 호출, 데이터 파싱, 카드 생성을 담당합니다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using TMPro;
public class VideoFeedManager : MonoBehaviour
{
[Header("UI References")]
public GameObject cardPrefab;
public Transform contentRoot;
[Header("Network Configuration")]
public string apiEndpoint = "http://192.168.0.103:8000/user/get_user_video_statistics/";
public string mediaBaseUrl = "http://192.168.0.103:8000/";
void Start()
{
ConfigureSecurityProtocols();
StartCoroutine(FetchVideoList());
}
private void ConfigureSecurityProtocols()
{
System.Net.ServicePointManager.SecurityProtocol =
System.Net.SecurityProtocolType.Tls12 |
System.Net.SecurityProtocolType.Tls11 |
System.Net.SecurityProtocolType.Tls;
System.Net.ServicePointManager.ServerCertificateValidationCallback =
(sender, certificate, chain, sslPolicyErrors) => true;
}
IEnumerator FetchVideoList()
{
using (UnityWebRequest request = UnityWebRequest.Get(apiEndpoint))
{
request.timeout = 10;
request.SetRequestHeader("Accept", "application/json");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string rawJson = request.downloadHandler.text;
ProcessJsonResponse(rawJson);
}
else
{
Debug.LogError($"API request failed: {request.error}");
}
}
}
private void ProcessJsonResponse(string json)
{
VideoListResponse parsedResponse = null;
if (json.StartsWith("["))
{
string wrappedJson = "{\"data\":" + json + "}";
parsedResponse = JsonUtility.FromJson<VideoListResponse>(wrappedJson);
}
else
{
parsedResponse = JsonUtility.FromJson<VideoListResponse>(json);
}
if (parsedResponse?.data != null)
{
ClearExistingCards();
PopulateVideoCards(parsedResponse.data);
}
else
{
Debug.LogError("Failed to parse JSON response or data is null.");
}
}
private void ClearExistingCards()
{
foreach (Transform child in contentRoot)
{
Destroy(child.gameObject);
}
}
private void PopulateVideoCards(List<UserVideoData> userDataList)
{
foreach (UserVideoData userData in userDataList)
{
if (userData.videos == null) continue;
foreach (VideoItem video in userData.videos)
{
CreateVideoCardInstance(video);
}
}
}
private void CreateVideoCardInstance(VideoItem videoData)
{
if (cardPrefab == null || contentRoot == null)
{
Debug.LogError("Card prefab or content root not assigned.");
return;
}
GameObject newCard = Instantiate(cardPrefab, contentRoot);
newCard.name = $"VideoCard_{videoData.video_id}";
ApplyTextToChild(newCard.transform, "Title", videoData.title);
ApplyTextToChild(newCard.transform, "Description", videoData.description);
ApplyTextToChild(newCard.transform, "Author", videoData.nickName);
ApplyTextToChild(newCard.transform, "ViewCount", $"Plays: {videoData.total_play_count}");
ApplyTextToChild(newCard.transform, "Date", FormatDateString(videoData.date));
LoadImageForChild(newCard.transform, "Cover", videoData.cover);
LoadImageForChild(newCard.transform, "Avatar", videoData.avatar);
Button cardButton = newCard.GetComponent<Button>();
if (cardButton != null)
{
cardButton.onClick.AddListener(() => OnCardClicked(videoData));
}
}
private void ApplyTextToChild(Transform parent, string childName, string textContent)
{
Transform child = parent.Find(childName);
if (child == null) return;
Text legacyText = child.GetComponent<Text>();
if (legacyText != null)
{
legacyText.text = textContent;
return;
}
TextMeshProUGUI tmpText = child.GetComponent<TextMeshProUGUI>();
if (tmpText != null)
{
tmpText.text = textContent;
}
}
private void LoadImageForChild(Transform parent, string childName, string relativeUrl)
{
Transform child = parent.Find(childName);
if (child == null) return;
Image targetImage = child.GetComponent<Image>();
if (targetImage == null || string.IsNullOrEmpty(relativeUrl)) return;
string fullUrl = relativeUrl.StartsWith("http") ? relativeUrl : mediaBaseUrl + relativeUrl;
StartCoroutine(DownloadAndSetTexture(fullUrl, targetImage, childName));
}
IEnumerator DownloadAndSetTexture(string url, Image targetImage, string imageLabel)
{
using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
Texture2D texture = DownloadHandlerTexture.GetContent(request);
if (texture != null)
{
targetImage.sprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
Vector2.one * 0.5f
);
}
}
else
{
Debug.LogWarning($"Failed to load image {imageLabel}: {request.error}");
}
}
}
private void OnCardClicked(VideoItem clickedVideo)
{
Debug.Log($"Card clicked: {clickedVideo.title} (ID: {clickedVideo.video_id})");
if (clickedVideo.episodes != null && clickedVideo.episodes.Count > 0)
{
string episodeUrl = clickedVideo.episodes[0].url;
if (!episodeUrl.StartsWith("http"))
{
episodeUrl = mediaBaseUrl + episodeUrl;
}
Debug.Log($"First episode URL: {episodeUrl}");
// Add video playback logic here
}
}
private string FormatDateString(string rawDate)
{
if (string.IsNullOrEmpty(rawDate)) return "Unknown Date";
if (DateTime.TryParse(rawDate, out DateTime parsedDate))
{
return parsedDate.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
}
if (DateTime.TryParseExact(rawDate, "yyyy-MM-ddTHH:mm:ss.fffZ",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out DateTime isoDate))
{
return isoDate.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
}
return rawDate;
}
}
6. TextMeshPro 한글 폰트 설정
TextMeshPro가 한글을 올바르게 표시하려면 한글을 지원하는 폰트 에셋이 필요합니다.
- 한글 폰트 파일 준비: 시스템 폰트(
malgun.ttf등)를 프로젝트에 임포트합니다. - 폰트 에셋 생성: 임포트한 폰트 파일을 우클릭 > Create > TextMeshPro > Font Asset을 선택합니다.
- Font Asset Creator에서 Character Set을 Chinese Simplified로 선택하고 Generate Font Atlas를 클릭합니다.
- 적용: 생성된 폰트 에셋을 각 TextMeshPro 컴포넌트의 Font Asset 필드에 할당하거나, TextMeshPro Settings (Window > TextMeshPro > Settings)에서 기본 폰트로 설정합니다.
7. HTTP 프로토콜 허용 설정
Unity에서 HTTP URL에 접근하려면 플레이어 설정을 변경해야 합니다.
- Edit > Project Settings > Player로 이동합니다.
- Other Settings 섹션에서 Configuration > Allow downloads over HTTP를 체크합니다.
8. 최종 확인
Django 서버가 실행 중인 상태에서 Unity 씬을 실행합니다. Scroll View에 동적으로 비디오 카드가 생성되고, 각 카드에 썸네일, 제목, 설명 등이 표시됩니다. TextMeshPro가 한글을 올바르게 렌더링하는지 확인합니다.