58c88dafff
- Add DownloadManager, SongLibrary, SongSelectManager, SongDetailPanel scripts - Rebuild SongSelect panel inside Menu.unity using VRBeatsKit style: left scroll list (ALL/OWNED tabs) + right detail panel (diff buttons, Download/Delete/Play) - SceneBuilder replaced: only ③ Menu — Rebuild SongSelect Panel remains - All UI text in English Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
421 lines
20 KiB
C#
421 lines
20 KiB
C#
using UnityEditor;
|
||
using UnityEditor.SceneManagement;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
|
||
public static class VRBeatSaberSceneBuilder
|
||
{
|
||
private const string MenuScene = "Assets/VRBeatsKit/Scenes/Menu.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;
|
||
}
|
||
}
|