Files
BeatSaber/Assets/Editor/VRBeatSaberSceneBuilder.cs
2026-05-26 18:54:56 +09:00

508 lines
24 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Object>(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<PlayableManager>();
if (pm != null)
Object.DestroyImmediate(pm);
// SongController GO 생성
var scGO = new GameObject("SongController");
var songController = scGO.AddComponent<SongController>();
var cubePrefab = AssetDatabase.LoadAssetAtPath<Spawneable>(
"Assets/VRBeatsKit/Prefabs/Spawneable/VR_BeatCube.prefab");
var onLevelComplete = AssetDatabase.LoadAssetAtPath<GameEvent>(
"Assets/VRBeatsKit/GameEvents/OnLevelComplete.asset");
// 카운트다운 캔버스 생성
var canvasGO = new GameObject("CountdownCanvas");
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 100;
canvasGO.AddComponent<CanvasScaler>();
canvasGO.AddComponent<GraphicRaycaster>();
var countdownGO = new GameObject("CountdownText");
countdownGO.transform.SetParent(canvasGO.transform, false);
var cRect = countdownGO.AddComponent<RectTransform>();
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<TextMeshProUGUI>();
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<SongLibrary>();
if (oldLib != null) Object.DestroyImmediate(oldLib);
var songLibrary = sysGO.AddComponent<SongLibrary>();
// Add/replace SongSelectManager + DownloadManager on SongSelect GO
var oldSSM = songSelectGO.GetComponent<SongSelectManager>();
if (oldSSM != null) Object.DestroyImmediate(oldSSM);
var oldDM = songSelectGO.GetComponent<DownloadManager>();
if (oldDM != null) Object.DestroyImmediate(oldDM);
var downloadManager = songSelectGO.AddComponent<DownloadManager>();
var songSelectManager = songSelectGO.AddComponent<SongSelectManager>();
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<SongDetailPanel>();
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<Button>();
smSO.FindProperty("tabOwnedBtn") .objectReferenceValue = tabOwnedBtn.GetComponent<Button>();
smSO.FindProperty("cardContainer") .objectReferenceValue = scrollContent;
smSO.FindProperty("detailPanel") .objectReferenceValue = detailPanelComp;
smSO.FindProperty("downloadManager").objectReferenceValue = downloadManager;
smSO.FindProperty("loadingOverlay") .objectReferenceValue = loadingOverlay;
smSO.FindProperty("errorOverlay") .objectReferenceValue = errorOverlay;
smSO.FindProperty("errorText") .objectReferenceValue = errorText;
smSO.ApplyModifiedPropertiesWithoutUndo();
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] ✓ SongSelect panel rebuilt in Menu.unity");
}
// ─────────────────────────────────────────────
// ListPanel: ScrollRect + overlays
// ─────────────────────────────────────────────
private static void BuildScrollList(Transform parent,
out RectTransform scrollContent,
out GameObject loadingOverlay,
out GameObject errorOverlay,
out TMP_Text errorText)
{
// ScrollRect (fills parent)
var scrollGO = new GameObject("Scroll");
scrollGO.transform.SetParent(parent, false);
StretchFull(scrollGO);
// Viewport with Mask
var vpGO = new GameObject("Viewport");
vpGO.transform.SetParent(scrollGO.transform, false);
StretchFull(vpGO);
var vpImg = vpGO.AddComponent<Image>();
vpImg.color = new Color(0f, 0f, 0f, 0.01f);
vpGO.AddComponent<Mask>().showMaskGraphic = false;
// Content with VerticalLayoutGroup + ContentSizeFitter
var contentGO = new GameObject("Content");
contentGO.transform.SetParent(vpGO.transform, false);
var contentRect = contentGO.AddComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0f, 1f);
contentRect.anchorMax = new Vector2(1f, 1f);
contentRect.pivot = new Vector2(0.5f, 1f);
contentRect.anchoredPosition = Vector2.zero;
contentRect.sizeDelta = Vector2.zero;
var vlg = contentGO.AddComponent<VerticalLayoutGroup>();
vlg.spacing = 1.5f;
vlg.padding = new RectOffset(2, 2, 2, 2);
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
vlg.childControlWidth = true;
vlg.childControlHeight = true;
var csf = contentGO.AddComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
csf.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
// ScrollRect component
var sr = scrollGO.AddComponent<ScrollRect>();
sr.content = contentRect;
sr.viewport = vpGO.GetComponent<RectTransform>();
sr.horizontal = false;
sr.vertical = true;
sr.movementType = ScrollRect.MovementType.Clamped;
sr.scrollSensitivity = 30f;
sr.inertia = true;
sr.decelerationRate = 0.135f;
// Loading overlay
loadingOverlay = new GameObject("LoadingOverlay");
loadingOverlay.transform.SetParent(parent, false);
StretchFull(loadingOverlay);
loadingOverlay.AddComponent<Image>().color = new Color(0.10f, 0.18f, 0.22f, 0.92f);
CreateLabel(loadingOverlay.transform, "Text", "Loading...",
Vector2.zero, new Vector2(40f, 10f), 5f, Color.white, TextAlignmentOptions.Center);
// Error overlay (hidden by default)
errorOverlay = new GameObject("ErrorOverlay");
errorOverlay.transform.SetParent(parent, false);
StretchFull(errorOverlay);
errorOverlay.AddComponent<Image>().color = new Color(0.10f, 0.18f, 0.22f, 0.92f);
var errLblGO = CreateLabel(errorOverlay.transform, "ErrorText", "",
Vector2.zero, new Vector2(48f, 20f), 4.5f, new Color(1f, 0.5f, 0.5f), TextAlignmentOptions.Center);
errorText = errLblGO.GetComponent<TMP_Text>();
errorOverlay.SetActive(false);
scrollContent = contentRect;
}
// ─────────────────────────────────────────────
// DetailPanel UI
// Local space: 52.7 × 49 → X: ±26.35, Y: ±24.5
// ─────────────────────────────────────────────
private static void BuildDetailPanelUI(Transform parent,
out TMP_Text titleTmp, out TMP_Text artistTmp, out TMP_Text infoTmp,
out Button btnNormal, out Button btnHard, out Button btnExpert, out Button btnExpertPlus,
out Button downloadBtn, out Button deleteBtn, out Button playBtn, out Button closeBtn,
out GameObject progressGroup, out Slider progressSlider, out TMP_Text progressText)
{
// Close button (top-right)
var closeBtnGO = CreateStyledButton(parent, "CloseBtn", "✕",
new Vector2(21f, 20.5f), new Vector2(8f, 7f), 5.5f);
closeBtn = closeBtnGO.GetComponent<Button>();
// Song info
var titleGO = CreateLabel(parent, "TitleText", "---",
new Vector2(-3f, 18.5f), new Vector2(38f, 8f), 6.5f,
Color.white, TextAlignmentOptions.MidlineLeft);
titleTmp = titleGO.GetComponent<TMP_Text>();
titleTmp.overflowMode = TextOverflowModes.Ellipsis;
var artistGO = CreateLabel(parent, "ArtistText", "",
new Vector2(0f, 12f), new Vector2(50f, 6f), 5f,
new Color(1f, 1f, 1f, 0.8f), TextAlignmentOptions.Center);
artistTmp = artistGO.GetComponent<TMP_Text>();
var infoGO = CreateLabel(parent, "InfoText", "",
new Vector2(0f, 7f), new Vector2(50f, 5f), 4.2f,
new Color(1f, 1f, 1f, 0.6f), TextAlignmentOptions.Center);
infoTmp = infoGO.GetComponent<TMP_Text>();
CreateDivider(parent, "Div1", new Vector2(0f, 4f), new Vector2(50f, 0.4f));
// Difficulty section
CreateLabel(parent, "LblDifficulty", "DIFFICULTY",
new Vector2(-16f, 1.5f), new Vector2(26f, 5f), 4.2f,
new Color(1f, 1f, 1f, 0.65f), TextAlignmentOptions.MidlineLeft);
var btnNormalGO = CreateStyledButton(parent, "BtnNormal", "Normal", new Vector2(-12f, -5f), new Vector2(22f, 7f), 4.5f);
var btnHardGO = CreateStyledButton(parent, "BtnHard", "Hard", new Vector2( 12f, -5f), new Vector2(22f, 7f), 4.5f);
var btnExpertGO = CreateStyledButton(parent, "BtnExpert", "Expert", new Vector2(-12f, -14f), new Vector2(22f, 7f), 4.5f);
var btnExpertPlusGO = CreateStyledButton(parent, "BtnExpertPlus", "Expert+", new Vector2( 12f, -14f), new Vector2(22f, 7f), 4.5f);
btnNormal = btnNormalGO .GetComponent<Button>();
btnHard = btnHardGO .GetComponent<Button>();
btnExpert = btnExpertGO .GetComponent<Button>();
btnExpertPlus = btnExpertPlusGO.GetComponent<Button>();
CreateDivider(parent, "Div2", new Vector2(0f, -18.5f), new Vector2(50f, 0.4f));
// Action buttons
var downloadBtnGO = CreateStyledButton(parent, "DownloadBtn", "Download",
new Vector2(-6f, -21.5f), new Vector2(34f, 7f), 5f);
var deleteBtnGO = CreateStyledButton(parent, "DeleteBtn", "Delete",
new Vector2(-6f, -21.5f), new Vector2(34f, 7f), 5f);
var playBtnGO = CreateStyledButton(parent, "PlayBtn", "Play",
new Vector2(19f, -21.5f), new Vector2(16f, 7f), 5f);
downloadBtn = downloadBtnGO.GetComponent<Button>();
deleteBtn = deleteBtnGO .GetComponent<Button>();
playBtn = playBtnGO .GetComponent<Button>();
// Make delete button red-tinted
var delImg = deleteBtnGO.GetComponent<Image>();
if (delImg != null) delImg.color = new Color(0.9f, 0.3f, 0.3f, 0.3f);
// Progress group (hidden by default)
progressGroup = new GameObject("ProgressGroup");
progressGroup.transform.SetParent(parent, false);
SetRect(progressGroup, new Vector2(0f, -21.5f), new Vector2(50f, 7f));
var pTextGO = CreateLabel(progressGroup.transform, "ProgressText", "--- 0%",
new Vector2(-13f, 0f), new Vector2(22f, 6f), 4f,
new Color(1f, 1f, 1f, 0.85f), TextAlignmentOptions.MidlineLeft);
progressText = pTextGO.GetComponent<TMP_Text>();
progressSlider = CreateSlider(progressGroup.transform, "ProgressSlider",
new Vector2(18f, 0f), new Vector2(18f, 4.5f));
progressGroup.SetActive(false);
}
// ─────────────────────────────────────────────
// Helpers — UI factory
// ─────────────────────────────────────────────
private static GameObject CreateStyledButton(Transform parent, string goName, string label,
Vector2 pos, Vector2 size, float fontSize)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var img = go.AddComponent<Image>();
img.color = new Color(1f, 1f, 1f, 0.12f);
var btn = go.AddComponent<Button>();
btn.targetGraphic = img;
var c = btn.colors;
c.normalColor = Color.white;
c.highlightedColor = new Color(0.96f, 0.96f, 0.96f, 1f);
c.pressedColor = new Color(0.78f, 0.78f, 0.78f, 1f);
c.selectedColor = new Color(0.96f, 0.96f, 0.96f, 1f);
c.fadeDuration = 0.1f;
btn.colors = c;
var textGO = new GameObject("Text");
textGO.transform.SetParent(go.transform, false);
StretchFull(textGO);
var tmp = textGO.AddComponent<TextMeshProUGUI>();
tmp.text = label;
tmp.alignment = TextAlignmentOptions.Center;
tmp.fontSize = fontSize;
tmp.color = Color.white;
return go;
}
private static GameObject CreateLabel(Transform parent, string goName, string text,
Vector2 pos, Vector2 size, float fontSize,
Color? color = null, TextAlignmentOptions align = TextAlignmentOptions.MidlineLeft)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var tmp = go.AddComponent<TextMeshProUGUI>();
tmp.text = text;
tmp.fontSize = fontSize;
tmp.color = color ?? Color.white;
tmp.alignment = align;
return go;
}
private static void CreateDivider(Transform parent, string goName, Vector2 pos, Vector2 size)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var img = go.AddComponent<Image>();
img.color = new Color(1f, 1f, 1f, 0.18f);
img.raycastTarget = false;
}
private static Slider CreateSlider(Transform parent, string goName, Vector2 pos, Vector2 size)
{
var go = new GameObject(goName);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var bgGO = new GameObject("Background");
bgGO.transform.SetParent(go.transform, false);
StretchFull(bgGO);
bgGO.AddComponent<Image>().color = new Color(1f, 1f, 1f, 0.15f);
var fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(go.transform, false);
StretchFull(fillArea);
var fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
StretchFull(fill);
fill.AddComponent<Image>().color = new Color(0.3f, 0.8f, 0.3f, 0.9f);
var slider = go.AddComponent<Slider>();
slider.fillRect = fill.GetComponent<RectTransform>();
slider.minValue = 0f;
slider.maxValue = 1f;
slider.value = 0f;
slider.interactable = false;
return slider;
}
// ─────────────────────────────────────────────
// Utils
// ─────────────────────────────────────────────
private static void SetRect(GameObject go, Vector2 pos, Vector2 size)
{
var r = go.GetComponent<RectTransform>();
if (r == null) r = go.AddComponent<RectTransform>();
r.anchorMin = new Vector2(0.5f, 0.5f);
r.anchorMax = new Vector2(0.5f, 0.5f);
r.pivot = new Vector2(0.5f, 0.5f);
r.anchoredPosition = pos;
r.sizeDelta = size;
}
private static void StretchFull(GameObject go)
{
var r = go.GetComponent<RectTransform>();
if (r == null) r = go.AddComponent<RectTransform>();
r.anchorMin = Vector2.zero;
r.anchorMax = Vector2.one;
r.offsetMin = r.offsetMax = Vector2.zero;
}
}