using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; using TMPro; using VRBeats; using VRBeats.ScriptableEvents; public static class VRBeatSaberSceneBuilder { private const string MenuScene = "Assets/VRBeatsKit/Scenes/Menu.unity"; // ───────────────────────────────────────────── // ④ Build Game Scene // SaberStyle 복제 → Game.unity 생성 // PlayableManager 제거, SongController + 카운트다운 캔버스 추가 // ───────────────────────────────────────────── [MenuItem("Tools/VRBeatSaber/④ Build Game Scene")] public static void BuildGameScene() { const string saberStylePath = "Assets/VRBeatsKit/Scenes/SaberStyle.unity"; const string gamePath = "Assets/Scenes/Game.unity"; // SaberStyle → Game.unity 복제 (이미 있으면 그냥 열기) if (!AssetDatabase.LoadAssetAtPath(gamePath)) { if (!AssetDatabase.CopyAsset(saberStylePath, gamePath)) { Debug.LogError("[SceneBuilder] SaberStyle.unity 복제 실패"); return; } AssetDatabase.Refresh(); } var scene = EditorSceneManager.OpenScene(gamePath, OpenSceneMode.Single); // PlayableManager 제거 (PlayableDirector는 유지) var pm = Object.FindFirstObjectByType(); if (pm != null) Object.DestroyImmediate(pm); // SongController GO 생성 var scGO = new GameObject("SongController"); var songController = scGO.AddComponent(); var cubePrefab = AssetDatabase.LoadAssetAtPath( "Assets/VRBeatsKit/Prefabs/Spawneable/VR_BeatCube.prefab"); var onLevelComplete = AssetDatabase.LoadAssetAtPath( "Assets/VRBeatsKit/GameEvents/OnLevelComplete.asset"); // 카운트다운 캔버스 생성 var canvasGO = new GameObject("CountdownCanvas"); var canvas = canvasGO.AddComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 100; canvasGO.AddComponent(); canvasGO.AddComponent(); var countdownGO = new GameObject("CountdownText"); countdownGO.transform.SetParent(canvasGO.transform, false); var cRect = countdownGO.AddComponent(); cRect.anchorMin = new Vector2(0.5f, 0.5f); cRect.anchorMax = new Vector2(0.5f, 0.5f); cRect.pivot = new Vector2(0.5f, 0.5f); cRect.anchoredPosition = Vector2.zero; cRect.sizeDelta = new Vector2(400f, 200f); var cTmp = countdownGO.AddComponent(); cTmp.text = ""; cTmp.fontSize = 120f; cTmp.color = Color.white; cTmp.alignment = TextAlignmentOptions.Center; cTmp.fontStyle = FontStyles.Bold; countdownGO.SetActive(false); // SongController 필드 연결 var scSO = new SerializedObject(songController); scSO.FindProperty("cubePrefab") .objectReferenceValue = cubePrefab; scSO.FindProperty("onLevelComplete") .objectReferenceValue = onLevelComplete; scSO.FindProperty("countdownText") .objectReferenceValue = cTmp; scSO.ApplyModifiedPropertiesWithoutUndo(); // Build Settings 에 Game.unity 추가 var scenes = EditorBuildSettings.scenes; bool exists = System.Array.Exists(scenes, s => s.path == gamePath); if (!exists) { var newList = new EditorBuildSettingsScene[scenes.Length + 1]; System.Array.Copy(scenes, newList, scenes.Length); newList[scenes.Length] = new EditorBuildSettingsScene(gamePath, true); EditorBuildSettings.scenes = newList; } EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); Debug.Log("[SceneBuilder] ✓ Game.unity 생성 완료"); } // ───────────────────────────────────────────── // ③ Menu — Rebuild SongSelect Panel // // Canvas(SongSelect) size: 105.885 × 68.223 // BG child covers full canvas (stretch anchors) // BG local coord origin = center // X: -52.94 ~ +52.94 // Y: -34.11 ~ +34.11 // ───────────────────────────────────────────── [MenuItem("Tools/VRBeatSaber/③ Menu — Rebuild SongSelect Panel")] public static void RebuildSongSelectPanel() { var scene = EditorSceneManager.OpenScene(MenuScene, OpenSceneMode.Single); var songSelectGO = GameObject.Find("SongSelect"); if (songSelectGO == null) { Debug.LogError("[SceneBuilder] 'SongSelect' not found."); return; } var bgTransform = songSelectGO.transform.Find("BG"); if (bgTransform == null) { Debug.LogError("[SceneBuilder] 'SongSelect/BG' not found."); return; } // Clear BG children for (int i = bgTransform.childCount - 1; i >= 0; i--) Object.DestroyImmediate(bgTransform.GetChild(i).gameObject); // Create/reuse SongSystem root GO for SongLibrary (must be root for DontDestroyOnLoad) var sysGO = GameObject.Find("SongSystem"); if (sysGO == null) sysGO = new GameObject("SongSystem"); var oldLib = sysGO.GetComponent(); if (oldLib != null) Object.DestroyImmediate(oldLib); var songLibrary = sysGO.AddComponent(); // Add/replace SongSelectManager + DownloadManager on SongSelect GO var oldSSM = songSelectGO.GetComponent(); if (oldSSM != null) Object.DestroyImmediate(oldSSM); var oldDM = songSelectGO.GetComponent(); if (oldDM != null) Object.DestroyImmediate(oldDM); var downloadManager = songSelectGO.AddComponent(); var songSelectManager = songSelectGO.AddComponent(); var bg = bgTransform; // ── Header ────────────────────────────────────────── CreateLabel(bg, "Title", "SONG SELECT", new Vector2(0f, 28.5f), new Vector2(100f, 9f), 8.5f, Color.white, TextAlignmentOptions.Center); CreateDivider(bg, "DivHeader", new Vector2(0f, 23.5f), new Vector2(104f, 0.5f)); var tabAllBtn = CreateStyledButton(bg, "TabAll", "ALL", new Vector2(-18f, 19.5f), new Vector2(30f, 7f), 5f); var tabOwnedBtn = CreateStyledButton(bg, "TabOwned", "OWNED", new Vector2( 14f, 19.5f), new Vector2(30f, 7f), 5f); CreateDivider(bg, "DivTabs", new Vector2(0f, 15.5f), new Vector2(104f, 0.5f)); // ── Content area: Y from 15 to -34.11, height ~49 ─── // ListPanel (left half) var listPanelGO = new GameObject("ListPanel"); listPanelGO.transform.SetParent(bg, false); SetRect(listPanelGO, new Vector2(-26.6f, -9.4f), new Vector2(52.7f, 49f)); // Vertical divider CreateDivider(bg, "DivVertical", new Vector2(0.1f, -9.4f), new Vector2(0.5f, 49f)); // DetailPanel (right half, hidden until card clicked) var detailPanelGO = new GameObject("DetailPanel"); detailPanelGO.transform.SetParent(bg, false); SetRect(detailPanelGO, new Vector2(26.6f, -9.4f), new Vector2(52.7f, 49f)); // ── ListPanel contents ─────────────────────────────── RectTransform scrollContent; GameObject loadingOverlay; GameObject errorOverlay; TMP_Text errorText; BuildScrollList(listPanelGO.transform, out scrollContent, out loadingOverlay, out errorOverlay, out errorText); // ── DetailPanel contents ───────────────────────────── var detailPanelComp = detailPanelGO.AddComponent(); Button btnNormal, btnHard, btnExpert, btnExpertPlus; Button downloadBtn, deleteBtn, playBtn, closeBtn; GameObject progressGroup; Slider progressSlider; TMP_Text progressText; TMP_Text titleTmp, artistTmp, infoTmp; BuildDetailPanelUI(detailPanelGO.transform, out titleTmp, out artistTmp, out infoTmp, out btnNormal, out btnHard, out btnExpert, out btnExpertPlus, out downloadBtn, out deleteBtn, out playBtn, out closeBtn, out progressGroup, out progressSlider, out progressText); detailPanelGO.SetActive(false); // ── Wire SongDetailPanel refs ──────────────────────── var dpSO = new SerializedObject(detailPanelComp); dpSO.FindProperty("titleText") .objectReferenceValue = titleTmp; dpSO.FindProperty("artistText") .objectReferenceValue = artistTmp; dpSO.FindProperty("infoText") .objectReferenceValue = infoTmp; dpSO.FindProperty("btnNormal") .objectReferenceValue = btnNormal; dpSO.FindProperty("btnHard") .objectReferenceValue = btnHard; dpSO.FindProperty("btnExpert") .objectReferenceValue = btnExpert; dpSO.FindProperty("btnExpertPlus") .objectReferenceValue = btnExpertPlus; dpSO.FindProperty("downloadButton") .objectReferenceValue = downloadBtn; dpSO.FindProperty("deleteButton") .objectReferenceValue = deleteBtn; dpSO.FindProperty("playButton") .objectReferenceValue = playBtn; dpSO.FindProperty("closeButton") .objectReferenceValue = closeBtn; dpSO.FindProperty("progressGroup") .objectReferenceValue = progressGroup; dpSO.FindProperty("progressSlider") .objectReferenceValue = progressSlider; dpSO.FindProperty("progressText") .objectReferenceValue = progressText; dpSO.FindProperty("gameSceneName") .stringValue = "Game"; dpSO.ApplyModifiedPropertiesWithoutUndo(); // ── Wire SongSelectManager refs ────────────────────── var smSO = new SerializedObject(songSelectManager); smSO.FindProperty("tabAllBtn") .objectReferenceValue = tabAllBtn.GetComponent