비트 찍기 완료 및 클로드를 통한 api작업

This commit is contained in:
jongjae0305
2026-05-20 16:44:28 +09:00
commit 2cd1be88d4
1596 changed files with 444234 additions and 0 deletions
+740
View File
@@ -0,0 +1,740 @@
#if UNITY_EDITOR
using System.Collections.Generic;
using System.IO;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// Tools → VRBeatSaber → 전체 자동 설정
/// 씬 생성, 프리팹 생성, Build Settings 등록까지 한 번에 처리
/// </summary>
public static class VRBeatSaberSceneBuilder
{
private static GameObject s_cardPrefab;
[MenuItem("Tools/VRBeatSaber/전체 자동 설정 (한 번만 실행)")]
public static void SetupAll()
{
if (!EditorUtility.DisplayDialog(
"VRBeatSaber 자동 설정",
"Intro / SongSelect / SongCreator 씬과\nSongCard 프리팹, Build Settings를 자동으로 설정합니다.\n\n현재 씬이 저장되지 않으면 경고가 뜹니다.",
"실행", "취소")) return;
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return;
EnsureFolder("Assets/Scenes");
EnsureFolder("Assets/Prefab");
// 1. SongCard 프리팹
s_cardPrefab = CreateSongCardPrefab();
// 2. SongSelect 씬 생성
BuildSongSelectScene();
// 3. SongCreator 씬 생성
BuildSongCreatorScene();
// 4. Intro 씬에 UI 추가
ModifyIntroScene();
// 5. Game 씬 AudioClip 제거
FixGameScene();
// 6. Build Settings 등록
RegisterBuildSettings();
AssetDatabase.Refresh();
EditorUtility.DisplayDialog(
"완료",
"자동 설정 완료!\n\n" +
"[ 직접 해야 할 것 (2가지) ]\n\n" +
"1. SongCreator 씬 → [CreatorManager]\n" +
" → NasPublisher → Nas Password\n" +
" (DSM 비밀번호 입력)\n\n" +
"2. Quest에 MP3 파일 넣기\n" +
" adb push 파일.mp3\n" +
" /sdcard/Android/data/{패키지명}/files/input/",
"확인");
}
// ══════════════════════════════════════════════════════════
// 1. SongCard 프리팹
// ══════════════════════════════════════════════════════════
private static GameObject CreateSongCardPrefab()
{
var root = new GameObject("SongCard");
root.AddComponent<Image>().color = new Color(0.12f, 0.12f, 0.12f);
var btn = root.AddComponent<Button>();
var rt = root.GetComponent<RectTransform>();
rt.sizeDelta = new Vector2(430, 90);
// 다운로드 뱃지 (우측 녹색 원)
var badge = new GameObject("DownloadedBadge");
badge.transform.SetParent(root.transform, false);
badge.AddComponent<Image>().color = new Color(0.2f, 0.9f, 0.4f);
var bRt = badge.GetComponent<RectTransform>();
bRt.anchorMin = new Vector2(1, 0.5f);
bRt.anchorMax = new Vector2(1, 0.5f);
bRt.pivot = new Vector2(1, 0.5f);
bRt.anchoredPosition = new Vector2(-12, 0);
bRt.sizeDelta = new Vector2(20, 20);
// 제목
var titleTMP = MakeTMP("TitleText", root.transform, "곡 제목",
new Vector2(-10, 16), new Vector2(370, 30), 20);
// 아티스트
var artistTMP = MakeTMP("ArtistText", root.transform, "아티스트",
new Vector2(-30, -16), new Vector2(260, 24), 15);
artistTMP.color = new Color(0.7f, 0.7f, 0.7f);
// 길이
var durTMP = MakeTMP("DurationText", root.transform, "0:00",
new Vector2(-45, -16), new Vector2(120, 24), 15);
durTMP.color = new Color(0.6f, 0.6f, 0.6f);
durTMP.alignment = TextAlignmentOptions.MidlineRight;
// SongCard 컴포넌트 바인딩
var card = root.AddComponent<SongCard>();
var so = new SerializedObject(card);
Bind(so, "titleText", titleTMP);
Bind(so, "artistText", artistTMP);
Bind(so, "durationText", durTMP);
Bind(so, "downloadedBadge", badge);
Bind(so, "button", btn);
so.ApplyModifiedProperties();
var prefab = PrefabUtility.SaveAsPrefabAsset(root, "Assets/Prefab/SongCard.prefab");
Object.DestroyImmediate(root);
Debug.Log("[SceneBuilder] SongCard 프리팹 생성 완료");
return prefab;
}
// ══════════════════════════════════════════════════════════
// 2. SongSelect 씬
// ══════════════════════════════════════════════════════════
private static void BuildSongSelectScene()
{
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
MakeCamera(new Vector3(0, 1.6f, 0));
MakeEventSystem();
// SongLibrary는 DontDestroyOnLoad를 사용하므로 단독 오브젝트에 배치
// (GameManager와 같이 두면 DownloadManager까지 DontDestroyOnLoad로 이동해 버림)
var slGO = new GameObject("[SongLibrary]");
slGO.AddComponent<SongLibrary>();
// GameManager: 씬마다 새로 생성되는 매니저들
var gm = new GameObject("[GameManager]");
gm.AddComponent<CacheManager>();
var dlMgr = gm.AddComponent<DownloadManager>();
var canvas = MakeCanvas(new Vector2(1200, 700), new Vector3(0, 1.5f, 2f));
// 탭
var tabPanel = MakePanel("TabPanel", canvas.transform, new Vector2(0, 310), new Vector2(1180, 60));
var tabAllBtn = MakeButton("TabAllBtn", tabPanel.transform, "전체", new Vector2(-290, 0), new Vector2(200, 50));
var tabOwnedBtn = MakeButton("TabOwnedBtn", tabPanel.transform, "보유중", new Vector2(-80, 0), new Vector2(200, 50));
// 목록 (좌측)
var listPanel = MakePanel("ListPanel", canvas.transform, new Vector2(-355, -30), new Vector2(460, 600));
var (_, cardContainer) = MakeScrollRect("SongScroll", listPanel.transform, Vector2.zero, new Vector2(450, 590));
// 상세 패널 (우측)
var detailGO = MakePanel("DetailPanel", canvas.transform, new Vector2(250, -30), new Vector2(680, 600));
var detail = detailGO.AddComponent<SongDetailPanel>();
var titleTxt = MakeTMP("TitleText", detailGO.transform, "곡 제목", new Vector2(0, 250), new Vector2(640, 50), 28);
var artistTxt = MakeTMP("ArtistText", detailGO.transform, "아티스트", new Vector2(0, 200), new Vector2(640, 36), 20);
var infoTxt = MakeTMP("InfoText", detailGO.transform, "BPM | 길이", new Vector2(0, 160), new Vector2(640, 30), 18);
var diffPanel = MakePanel("DiffPanel", detailGO.transform, new Vector2(0, 90), new Vector2(640, 60));
var btnEasy = MakeButton("EasyBtn", diffPanel.transform, "Easy", new Vector2(-240, 0), new Vector2(140, 50));
var btnNorm = MakeButton("NormalBtn", diffPanel.transform, "Normal", new Vector2(-80, 0), new Vector2(140, 50));
var btnHard = MakeButton("HardBtn", diffPanel.transform, "Hard", new Vector2(80, 0), new Vector2(140, 50));
var btnExp = MakeButton("ExpertBtn", diffPanel.transform, "Expert", new Vector2(240, 0), new Vector2(140, 50));
var actPanel = MakePanel("ActionPanel", detailGO.transform, new Vector2(0, 15), new Vector2(640, 60));
var dlBtn = MakeButton("DownloadBtn", actPanel.transform, "▼ 다운로드", new Vector2(-165, 0), new Vector2(290, 50));
var delBtn = MakeButton("DeleteBtn", actPanel.transform, "삭제", new Vector2(165, 0), new Vector2(290, 50));
var progGroup = MakePanel("ProgressGroup", detailGO.transform, new Vector2(0, -50), new Vector2(640, 40));
var progSlider = MakeSlider("ProgressSlider", progGroup.transform, new Vector2(-70, 0), new Vector2(480, 28));
var progText = MakeTMP("ProgressText", progGroup.transform, "0%", new Vector2(275, 0), new Vector2(100, 30), 16);
var playBtn = MakeButton("PlayButton", detailGO.transform, "▶ 플레이", new Vector2(0, -140), new Vector2(300, 65));
var loadingOvr = MakePanel("LoadingOverlay", canvas.transform, Vector2.zero, new Vector2(1200, 700));
loadingOvr.GetComponent<Image>().color = new Color(0, 0, 0, 0.75f);
MakeTMP("LoadingText", loadingOvr.transform, "불러오는 중...", Vector2.zero, new Vector2(400, 60), 26);
var errorOvr = MakePanel("ErrorOverlay", canvas.transform, Vector2.zero, new Vector2(1200, 700));
errorOvr.GetComponent<Image>().color = new Color(0, 0, 0, 0.75f);
var errorTxt = MakeTMP("ErrorText", errorOvr.transform, "", Vector2.zero, new Vector2(700, 120), 22);
loadingOvr.SetActive(false);
errorOvr.SetActive(false);
detailGO.SetActive(false);
// SongDetailPanel 바인딩
var dso = new SerializedObject(detail);
Bind(dso, "titleText", titleTxt);
Bind(dso, "artistText", artistTxt);
Bind(dso, "infoText", infoTxt);
Bind(dso, "btnEasy", btnEasy.GetComponent<Button>());
Bind(dso, "btnNormal", btnNorm.GetComponent<Button>());
Bind(dso, "btnHard", btnHard.GetComponent<Button>());
Bind(dso, "btnExpert", btnExp.GetComponent<Button>());
Bind(dso, "downloadButton", dlBtn.GetComponent<Button>());
Bind(dso, "deleteButton", delBtn.GetComponent<Button>());
Bind(dso, "playButton", playBtn.GetComponent<Button>());
Bind(dso, "progressGroup", progGroup);
Bind(dso, "progressSlider", progSlider);
Bind(dso, "progressText", progText);
dso.ApplyModifiedProperties();
// SongSelectManager 바인딩
var mgr = canvas.AddComponent<SongSelectManager>();
var mso = new SerializedObject(mgr);
Bind(mso, "tabAllBtn", tabAllBtn.GetComponent<Button>());
Bind(mso, "tabOwnedBtn", tabOwnedBtn.GetComponent<Button>());
Bind(mso, "cardContainer", cardContainer);
Bind(mso, "songCardPrefab", s_cardPrefab);
Bind(mso, "detailPanel", detail);
Bind(mso, "downloadManager", dlMgr);
Bind(mso, "loadingOverlay", loadingOvr);
Bind(mso, "errorOverlay", errorOvr);
Bind(mso, "errorText", errorTxt);
mso.ApplyModifiedProperties();
EditorSceneManager.SaveScene(scene, "Assets/Scenes/SongSelect.unity");
Debug.Log("[SceneBuilder] SongSelect 씬 완료");
}
// ══════════════════════════════════════════════════════════
// 3. SongCreator 씬
// ══════════════════════════════════════════════════════════
private static void BuildSongCreatorScene()
{
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
MakeCamera(new Vector3(0, 1.6f, 0));
MakeEventSystem();
// SongLibrary 싱글턴 — SongCreator에서 직접 쓰진 않지만 DontDestroyOnLoad 체계 유지
new GameObject("[SongLibrary]").AddComponent<SongLibrary>();
var gm = new GameObject("[CreatorManager]");
var uploader = gm.AddComponent<BeatSageUploader>();
var publisher = gm.AddComponent<NasPublisher>();
var scMgr = gm.AddComponent<SongCreatorManager>();
var canvas = MakeCanvas(new Vector2(900, 760), new Vector3(0, 1.5f, 2f));
MakeTMP("TitleLabel", canvas.transform, "노래 만들기", new Vector2(0, 345), new Vector2(860, 55), 32);
var audioPanel = MakePanel("AudioPanel", canvas.transform, new Vector2(0, 250), new Vector2(860, 90));
var audioDd = MakeDropdown("AudioDropdown", audioPanel.transform, new Vector2(-120, 0), new Vector2(540, 60));
var refreshBtn = MakeButton("RefreshBtn", audioPanel.transform, "새로고침", new Vector2(255, 0), new Vector2(160, 58));
var pathHint = MakeTMP("PathHint", canvas.transform, "", new Vector2(0, 195), new Vector2(860, 28), 13);
var metaPanel = MakePanel("MetaPanel", canvas.transform, new Vector2(0, 105), new Vector2(860, 110));
var titleInput = MakeInputField("TitleInput", metaPanel.transform, "곡 제목", new Vector2(-280, 0), new Vector2(255, 55));
var artistInput = MakeInputField("ArtistInput", metaPanel.transform, "아티스트", new Vector2(0, 0), new Vector2(255, 55));
var bpmInput = MakeInputField("BpmInput", metaPanel.transform, "BPM", new Vector2(280, 0), new Vector2(255, 55));
var diffPanel = MakePanel("DiffPanel", canvas.transform, new Vector2(0, -15), new Vector2(860, 65));
var togEasy = MakeToggle("EasyToggle", diffPanel.transform, "Easy", new Vector2(-310, 0));
var togNormal = MakeToggle("NormalToggle", diffPanel.transform, "Normal", new Vector2(-155, 0));
var togHard = MakeToggle("HardToggle", diffPanel.transform, "Hard", new Vector2(0, 0));
var togExpert = MakeToggle("ExpertToggle", diffPanel.transform, "Expert", new Vector2(155, 0));
togHard.isOn = true;
togExpert.isOn = true;
var actPanel = MakePanel("ActionPanel", canvas.transform, new Vector2(0, -125), new Vector2(860, 80));
var genBtn = MakeButton("GenerateButton", actPanel.transform, "AI 생성 시작", new Vector2(-175, 0), new Vector2(440, 65));
var manualBtn = MakeButton("ManualButton", actPanel.transform, "직접 만들기 →", new Vector2(245, 0), new Vector2(185, 42));
var progGroup = MakePanel("ProgressGroup", canvas.transform, new Vector2(0, -250), new Vector2(860, 95));
var statusTxt = MakeTMP("StatusText", progGroup.transform, "", new Vector2(0, 28), new Vector2(840, 36), 18);
var progSlider = MakeSlider("ProgressSlider", progGroup.transform, new Vector2(0, -15), new Vector2(820, 28));
progGroup.SetActive(false);
var sso = new SerializedObject(scMgr);
Bind(sso, "audioDropdown", audioDd);
Bind(sso, "refreshBtn", refreshBtn.GetComponent<Button>());
Bind(sso, "inputPathHint", pathHint);
Bind(sso, "titleInput", titleInput);
Bind(sso, "artistInput", artistInput);
Bind(sso, "bpmInput", bpmInput);
Bind(sso, "toggleEasy", togEasy);
Bind(sso, "toggleNormal", togNormal);
Bind(sso, "toggleHard", togHard);
Bind(sso, "toggleExpert", togExpert);
Bind(sso, "generateButton", genBtn.GetComponent<Button>());
Bind(sso, "manualEditorButton", manualBtn.GetComponent<Button>());
Bind(sso, "progressGroup", progGroup);
Bind(sso, "statusText", statusTxt);
Bind(sso, "progressSlider", progSlider);
Bind(sso, "beatSageUploader", uploader);
Bind(sso, "nasPublisher", publisher);
sso.ApplyModifiedProperties();
EditorSceneManager.SaveScene(scene, "Assets/Scenes/SongCreator.unity");
Debug.Log("[SceneBuilder] SongCreator 씬 완료");
}
// ══════════════════════════════════════════════════════════
// 4. Intro 씬 수정
// ══════════════════════════════════════════════════════════
private static void ModifyIntroScene()
{
const string path = "Assets/Scenes/Intro.unity";
if (!File.Exists(path)) { Debug.LogWarning("[SceneBuilder] Intro.unity 없음 — 건너뜀"); return; }
var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
// 이미 IntroManager가 있으면 건너뜀
if (Object.FindObjectOfType<IntroManager>() != null)
{
Debug.Log("[SceneBuilder] Intro 씬 IntroManager 이미 존재 — 건너뜀");
return;
}
var canvas = MakeCanvas(new Vector2(600, 420), new Vector3(0, 1.5f, 2f));
MakeTMP("TitleText", canvas.transform, "VR BEAT SABER", new Vector2(0, 145), new Vector2(560, 70), 38);
var playBtn = MakeButton("PlayButton", canvas.transform, "게임하기", new Vector2(0, 30), new Vector2(420, 85));
var createBtn = MakeButton("CreateButton", canvas.transform, "노래만들기", new Vector2(0, -80), new Vector2(420, 85));
var introGO = new GameObject("[IntroManager]");
var mgr = introGO.AddComponent<IntroManager>();
var iso = new SerializedObject(mgr);
Bind(iso, "playButton", playBtn.GetComponent<Button>());
Bind(iso, "createButton", createBtn.GetComponent<Button>());
iso.ApplyModifiedProperties();
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] Intro 씬 수정 완료");
}
// ══════════════════════════════════════════════════════════
// 5. Game 씬 — AudioClip 참조 제거
// ══════════════════════════════════════════════════════════
private static void FixGameScene()
{
const string path = "Assets/Scenes/Game.unity";
if (!File.Exists(path)) { Debug.LogWarning("[SceneBuilder] Game.unity 없음 — 건너뜀"); return; }
var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
var spawner = Object.FindObjectOfType<Spawner>();
if (spawner == null) { Debug.LogWarning("[SceneBuilder] Spawner 없음"); return; }
// Spawner가 참조하는 AudioSource의 AudioClip 제거 (런타임에 로컬 파일에서 로드)
var sso = new SerializedObject(spawner);
var asProp = sso.FindProperty("audioSource");
if (asProp?.objectReferenceValue is AudioSource audioSrc)
{
var asObj = new SerializedObject(audioSrc);
var clipProp = asObj.FindProperty("m_audioClip");
if (clipProp != null)
{
clipProp.objectReferenceValue = null;
asObj.ApplyModifiedProperties();
Debug.Log("[SceneBuilder] AudioClip 참조 제거 완료");
}
}
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] Game 씬 정리 완료");
}
// ══════════════════════════════════════════════════════════
// 6. Build Settings
// ══════════════════════════════════════════════════════════
private static void RegisterBuildSettings()
{
var paths = new[]
{
"Assets/Scenes/Intro.unity",
"Assets/Scenes/SongSelect.unity",
"Assets/Scenes/SongCreator.unity",
"Assets/Scenes/Game.unity",
"Assets/Scenes/MapEditorScene.unity",
};
var list = new List<EditorBuildSettingsScene>();
foreach (var p in paths)
{
if (File.Exists(p))
list.Add(new EditorBuildSettingsScene(p, true));
else
Debug.LogWarning($"[SceneBuilder] 씬 파일 없음: {p}");
}
EditorBuildSettings.scenes = list.ToArray();
Debug.Log($"[SceneBuilder] Build Settings 등록 완료: {list.Count}개");
}
// ══════════════════════════════════════════════════════════
// UI 헬퍼
// ══════════════════════════════════════════════════════════
private static void MakeCamera(Vector3 pos)
{
var go = new GameObject("Main Camera");
go.AddComponent<Camera>();
go.AddComponent<AudioListener>();
go.tag = "MainCamera";
go.transform.position = pos;
}
private static void MakeEventSystem()
{
var go = new GameObject("EventSystem");
go.AddComponent<EventSystem>();
// New Input System 사용 시 StandaloneInputModule 대신 InputSystemUIInputModule 필요
// 없으면 StandaloneInputModule로 폴백 (Player Settings에서 Both로 설정 시 동작)
var inputModuleType = System.Type.GetType(
"UnityEngine.InputSystem.UI.InputSystemUIInputModule, Unity.InputSystem");
if (inputModuleType != null)
go.AddComponent(inputModuleType);
else
go.AddComponent<StandaloneInputModule>();
}
private static GameObject MakeCanvas(Vector2 size, Vector3 worldPos)
{
var go = new GameObject("Canvas");
var c = go.AddComponent<Canvas>();
c.renderMode = RenderMode.WorldSpace;
go.AddComponent<CanvasScaler>();
// VR(XR Interaction Toolkit)에서는 TrackedDeviceGraphicRaycaster 필요
var trackedRaycasterType = System.Type.GetType(
"UnityEngine.XR.Interaction.Toolkit.UI.TrackedDeviceGraphicRaycaster, Unity.XR.Interaction.Toolkit");
if (trackedRaycasterType != null)
go.AddComponent(trackedRaycasterType);
else
go.AddComponent<GraphicRaycaster>();
var rt = go.GetComponent<RectTransform>();
rt.sizeDelta = size;
go.transform.position = worldPos;
go.transform.localScale = Vector3.one * 0.002f;
return go;
}
private static GameObject MakePanel(string name, Transform parent, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
go.AddComponent<Image>().color = new Color(0.08f, 0.08f, 0.08f, 0.88f);
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos;
rt.sizeDelta = size;
return go;
}
private static GameObject MakeButton(string name, Transform parent, string label, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
go.AddComponent<Image>().color = new Color(0.22f, 0.22f, 0.22f);
go.AddComponent<Button>();
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos;
rt.sizeDelta = size;
var tgo = new GameObject("Text");
tgo.transform.SetParent(go.transform, false);
var tmp = tgo.AddComponent<TextMeshProUGUI>();
tmp.text = label;
tmp.fontSize = 18;
tmp.alignment = TextAlignmentOptions.Center;
FullStretch(tgo.GetComponent<RectTransform>());
return go;
}
private static TMP_Text MakeTMP(string name, Transform parent, string text, Vector2 pos, Vector2 size, int fs)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var tmp = go.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = fs;
tmp.alignment = TextAlignmentOptions.MidlineLeft;
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos;
rt.sizeDelta = size;
return tmp;
}
private static TMP_InputField MakeInputField(string name, Transform parent, string ph, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
go.AddComponent<Image>().color = new Color(0.15f, 0.15f, 0.15f);
var field = go.AddComponent<TMP_InputField>();
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos;
rt.sizeDelta = size;
var area = new GameObject("Text Area");
area.transform.SetParent(go.transform, false);
var aRt = area.AddComponent<RectTransform>();
aRt.anchorMin = Vector2.zero; aRt.anchorMax = Vector2.one;
aRt.sizeDelta = new Vector2(-16, -8);
var phGO = new GameObject("Placeholder");
phGO.transform.SetParent(area.transform, false);
var phTMP = phGO.AddComponent<TextMeshProUGUI>();
phTMP.text = ph; phTMP.color = new Color(0.5f, 0.5f, 0.5f); phTMP.fontSize = 16;
FullStretch(phGO.GetComponent<RectTransform>());
var inGO = new GameObject("Text");
inGO.transform.SetParent(area.transform, false);
var inTMP = inGO.AddComponent<TextMeshProUGUI>();
inTMP.fontSize = 16;
FullStretch(inGO.GetComponent<RectTransform>());
field.textViewport = aRt;
field.placeholder = phTMP;
field.textComponent = inTMP;
return field;
}
private static TMP_Dropdown MakeDropdown(string name, Transform parent, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
go.AddComponent<Image>().color = new Color(0.15f, 0.15f, 0.15f);
var dd = go.AddComponent<TMP_Dropdown>();
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos;
rt.sizeDelta = size;
// Caption label (선택된 항목 표시)
var labelGO = new GameObject("Label");
labelGO.transform.SetParent(go.transform, false);
var labelTMP = labelGO.AddComponent<TextMeshProUGUI>();
labelTMP.text = "";
labelTMP.fontSize = 16;
var labelRt = labelGO.GetComponent<RectTransform>();
labelRt.anchorMin = Vector2.zero; labelRt.anchorMax = Vector2.one;
labelRt.offsetMin = new Vector2(10, 2); labelRt.offsetMax = new Vector2(-28, -2);
// Arrow indicator
var arrowGO = new GameObject("Arrow");
arrowGO.transform.SetParent(go.transform, false);
arrowGO.AddComponent<Image>().color = Color.white;
var arrowRt = arrowGO.GetComponent<RectTransform>();
arrowRt.anchorMin = new Vector2(1, 0.5f);
arrowRt.anchorMax = new Vector2(1, 0.5f);
arrowRt.pivot = new Vector2(1, 0.5f);
arrowRt.anchoredPosition = new Vector2(-6, 0);
arrowRt.sizeDelta = new Vector2(20, 20);
// Template (드롭다운 목록, 비활성 상태로 시작)
var tplGO = new GameObject("Template");
tplGO.transform.SetParent(go.transform, false);
tplGO.AddComponent<Image>().color = new Color(0.12f, 0.12f, 0.12f);
var tplSr = tplGO.AddComponent<ScrollRect>();
var tplRt = tplGO.GetComponent<RectTransform>();
tplRt.anchorMin = new Vector2(0, 0); tplRt.anchorMax = new Vector2(1, 0);
tplRt.pivot = new Vector2(0.5f, 1);
tplRt.anchoredPosition = new Vector2(0, 2);
tplRt.sizeDelta = new Vector2(0, 150);
var vpGO = new GameObject("Viewport");
vpGO.transform.SetParent(tplGO.transform, false);
vpGO.AddComponent<Image>();
vpGO.AddComponent<Mask>().showMaskGraphic = false;
var vpRt = vpGO.GetComponent<RectTransform>();
FullStretch(vpRt);
var contentGO = new GameObject("Content");
contentGO.transform.SetParent(vpGO.transform, false);
var contentRt = contentGO.AddComponent<RectTransform>();
contentRt.anchorMin = new Vector2(0, 1); contentRt.anchorMax = Vector2.one;
contentRt.pivot = new Vector2(0.5f, 1);
contentRt.anchoredPosition = Vector2.zero; contentRt.sizeDelta = Vector2.zero;
tplSr.viewport = vpRt;
tplSr.content = contentRt;
tplSr.horizontal = false;
// Item 템플릿 (Content 안, 드롭다운이 복제해서 사용)
var itemGO = new GameObject("Item");
itemGO.transform.SetParent(contentGO.transform, false);
var itemTgl = itemGO.AddComponent<Toggle>();
var itemRt = itemGO.GetComponent<RectTransform>();
itemRt.anchorMin = new Vector2(0, 0.5f); itemRt.anchorMax = new Vector2(1, 0.5f);
itemRt.sizeDelta = new Vector2(0, 30);
var iBgGO = new GameObject("Item Background");
iBgGO.transform.SetParent(itemGO.transform, false);
var iBgImg = iBgGO.AddComponent<Image>();
iBgImg.color = new Color(0.15f, 0.15f, 0.15f);
FullStretch(iBgGO.GetComponent<RectTransform>());
var iCkGO = new GameObject("Item Checkmark");
iCkGO.transform.SetParent(itemGO.transform, false);
var iCkImg = iCkGO.AddComponent<Image>();
iCkImg.color = new Color(0.2f, 0.9f, 0.4f);
var iCkRt = iCkGO.GetComponent<RectTransform>();
iCkRt.anchorMin = new Vector2(0, 0.5f); iCkRt.anchorMax = new Vector2(0, 0.5f);
iCkRt.sizeDelta = new Vector2(18, 18); iCkRt.anchoredPosition = new Vector2(12, 0);
var iLblGO = new GameObject("Item Label");
iLblGO.transform.SetParent(itemGO.transform, false);
var iLblTMP = iLblGO.AddComponent<TextMeshProUGUI>();
iLblTMP.text = "Option"; iLblTMP.fontSize = 14;
var iLblRt = iLblGO.GetComponent<RectTransform>();
iLblRt.anchorMin = Vector2.zero; iLblRt.anchorMax = Vector2.one;
iLblRt.offsetMin = new Vector2(28, 1); iLblRt.offsetMax = new Vector2(-8, -2);
itemTgl.targetGraphic = iBgImg;
itemTgl.graphic = iCkImg;
dd.template = tplRt;
dd.captionText = labelTMP;
dd.itemText = iLblTMP;
tplGO.SetActive(false);
return dd;
}
private static Toggle MakeToggle(string name, Transform parent, string label, Vector2 pos)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var toggle = go.AddComponent<Toggle>();
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos;
rt.sizeDelta = new Vector2(140, 40);
var bg = new GameObject("Background");
bg.transform.SetParent(go.transform, false);
bg.AddComponent<Image>().color = new Color(0.25f, 0.25f, 0.25f);
var bRt = bg.GetComponent<RectTransform>();
bRt.anchoredPosition = new Vector2(-45, 0);
bRt.sizeDelta = new Vector2(28, 28);
var ck = new GameObject("Checkmark");
ck.transform.SetParent(bg.transform, false);
var ckImg = ck.AddComponent<Image>();
ckImg.color = new Color(0.2f, 0.9f, 0.4f);
FullStretch(ck.GetComponent<RectTransform>());
var lGO = new GameObject("Label");
lGO.transform.SetParent(go.transform, false);
var lTMP = lGO.AddComponent<TextMeshProUGUI>();
lTMP.text = label; lTMP.fontSize = 16;
var lRt = lGO.GetComponent<RectTransform>();
lRt.anchoredPosition = new Vector2(20, 0);
lRt.sizeDelta = new Vector2(95, 30);
toggle.targetGraphic = bg.GetComponent<Image>();
toggle.graphic = ckImg;
toggle.isOn = false;
return toggle;
}
private static Slider MakeSlider(string name, Transform parent, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
var slider = go.AddComponent<Slider>();
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos;
rt.sizeDelta = size;
var bg = new GameObject("Background");
bg.transform.SetParent(go.transform, false);
bg.AddComponent<Image>().color = new Color(0.25f, 0.25f, 0.25f);
var bRt = bg.GetComponent<RectTransform>();
bRt.anchorMin = new Vector2(0, 0.25f); bRt.anchorMax = new Vector2(1, 0.75f);
bRt.sizeDelta = Vector2.zero;
var fa = new GameObject("Fill Area");
fa.transform.SetParent(go.transform, false);
var faRt = fa.AddComponent<RectTransform>();
faRt.anchorMin = new Vector2(0, 0.25f); faRt.anchorMax = new Vector2(1, 0.75f);
faRt.sizeDelta = new Vector2(-20, 0);
var fill = new GameObject("Fill");
fill.transform.SetParent(fa.transform, false);
fill.AddComponent<Image>().color = new Color(0.2f, 0.7f, 1f);
var fRt = fill.GetComponent<RectTransform>();
fRt.anchorMin = Vector2.zero; fRt.anchorMax = new Vector2(0, 1);
fRt.sizeDelta = new Vector2(10, 0);
slider.fillRect = fRt;
slider.value = 0f;
return slider;
}
private static (ScrollRect, Transform) MakeScrollRect(string name, Transform parent, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
go.AddComponent<Image>().color = new Color(0, 0, 0, 0.2f);
var sr = go.AddComponent<ScrollRect>();
var rt = go.GetComponent<RectTransform>();
rt.anchoredPosition = pos; rt.sizeDelta = size;
var vp = new GameObject("Viewport");
vp.transform.SetParent(go.transform, false);
vp.AddComponent<Image>();
vp.AddComponent<Mask>().showMaskGraphic = false;
FullStretch(vp.GetComponent<RectTransform>());
var content = new GameObject("CardContainer");
content.transform.SetParent(vp.transform, false);
var vlg = content.AddComponent<VerticalLayoutGroup>();
vlg.spacing = 8; vlg.childForceExpandWidth = true; vlg.childForceExpandHeight = false;
vlg.padding = new RectOffset(8, 8, 8, 8);
content.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var cRt = content.GetComponent<RectTransform>();
cRt.anchorMin = new Vector2(0, 1); cRt.anchorMax = Vector2.one;
cRt.pivot = new Vector2(0.5f, 1); cRt.sizeDelta = Vector2.zero;
sr.viewport = vp.GetComponent<RectTransform>();
sr.content = cRt;
sr.horizontal = false;
return (sr, content.transform);
}
private static void FullStretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.anchoredPosition = Vector2.zero; rt.sizeDelta = Vector2.zero;
}
private static void Bind(SerializedObject so, string field, Object value)
{
var prop = so.FindProperty(field);
if (prop != null) prop.objectReferenceValue = value;
else Debug.LogWarning($"[SceneBuilder] 필드 없음: {field}");
}
private static void EnsureFolder(string path)
{
if (!Directory.Exists(path))
{
var parts = path.Split('/');
AssetDatabase.CreateFolder(string.Join("/", parts[..^1]), parts[^1]);
}
}
}
#endif
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8041d843681d86e41a11512f66dd9933