2026-05-21 23:37:34 +09:00
|
|
|
|
using UnityEditor;
|
|
|
|
|
|
using UnityEditor.SceneManagement;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
using TMPro;
|
2026-05-22 14:32:25 +09:00
|
|
|
|
using VRBeats;
|
|
|
|
|
|
using VRBeats.ScriptableEvents;
|
2026-05-21 23:37:34 +09:00
|
|
|
|
|
|
|
|
|
|
public static class VRBeatSaberSceneBuilder
|
|
|
|
|
|
{
|
2026-05-22 01:22:16 +09:00
|
|
|
|
private const string MenuScene = "Assets/VRBeatsKit/Scenes/Menu.unity";
|
2026-05-21 23:37:34 +09:00
|
|
|
|
|
2026-05-22 14:32:25 +09:00
|
|
|
|
// ─────────────────────────────────────────────
|
|
|
|
|
|
// ④ 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.FindObjectOfType<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 생성 완료");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:37:34 +09:00
|
|
|
|
// ─────────────────────────────────────────────
|
2026-05-22 01:22:16 +09:00
|
|
|
|
// ③ 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
|
2026-05-21 23:37:34 +09:00
|
|
|
|
// ─────────────────────────────────────────────
|
2026-05-22 01:22:16 +09:00
|
|
|
|
[MenuItem("Tools/VRBeatSaber/③ Menu — Rebuild SongSelect Panel")]
|
|
|
|
|
|
public static void RebuildSongSelectPanel()
|
2026-05-21 23:37:34 +09:00
|
|
|
|
{
|
|
|
|
|
|
var scene = EditorSceneManager.OpenScene(MenuScene, OpenSceneMode.Single);
|
|
|
|
|
|
|
2026-05-22 01:22:16 +09:00
|
|
|
|
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();
|
2026-05-21 23:37:34 +09:00
|
|
|
|
|
|
|
|
|
|
EditorSceneManager.MarkSceneDirty(scene);
|
|
|
|
|
|
EditorSceneManager.SaveScene(scene);
|
2026-05-22 01:22:16 +09:00
|
|
|
|
Debug.Log("[SceneBuilder] ✓ SongSelect panel rebuilt in Menu.unity");
|
2026-05-21 23:37:34 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────
|
2026-05-22 01:22:16 +09:00
|
|
|
|
// ListPanel: ScrollRect + overlays
|
2026-05-21 23:37:34 +09:00
|
|
|
|
// ─────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-22 01:22:16 +09:00
|
|
|
|
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;
|
2026-05-21 23:37:34 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────
|
2026-05-22 01:22:16 +09:00
|
|
|
|
// DetailPanel UI
|
|
|
|
|
|
// Local space: 52.7 × 49 → X: ±26.35, Y: ±24.5
|
2026-05-21 23:37:34 +09:00
|
|
|
|
// ─────────────────────────────────────────────
|
2026-05-22 01:22:16 +09:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-21 23:37:34 +09:00
|
|
|
|
{
|
2026-05-22 01:22:16 +09:00
|
|
|
|
// 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);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────
|
|
|
|
|
|
// Helpers — UI factory
|
|
|
|
|
|
// ─────────────────────────────────────────────
|
|
|
|
|
|
|
2026-05-22 01:22:16 +09:00
|
|
|
|
private static GameObject CreateStyledButton(Transform parent, string goName, string label,
|
2026-05-21 23:37:34 +09:00
|
|
|
|
Vector2 pos, Vector2 size, float fontSize)
|
|
|
|
|
|
{
|
2026-05-22 01:22:16 +09:00
|
|
|
|
var go = new GameObject(goName);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
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;
|
2026-05-22 01:22:16 +09:00
|
|
|
|
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);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 01:22:16 +09:00
|
|
|
|
private static GameObject CreateLabel(Transform parent, string goName, string text,
|
2026-05-21 23:37:34 +09:00
|
|
|
|
Vector2 pos, Vector2 size, float fontSize,
|
|
|
|
|
|
Color? color = null, TextAlignmentOptions align = TextAlignmentOptions.MidlineLeft)
|
|
|
|
|
|
{
|
2026-05-22 01:22:16 +09:00
|
|
|
|
var go = new GameObject(goName);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 01:22:16 +09:00
|
|
|
|
private static void CreateDivider(Transform parent, string goName, Vector2 pos, Vector2 size)
|
2026-05-21 23:37:34 +09:00
|
|
|
|
{
|
2026-05-22 01:22:16 +09:00
|
|
|
|
var go = new GameObject(goName);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
go.transform.SetParent(parent, false);
|
|
|
|
|
|
SetRect(go, pos, size);
|
|
|
|
|
|
var img = go.AddComponent<Image>();
|
2026-05-22 01:22:16 +09:00
|
|
|
|
img.color = new Color(1f, 1f, 1f, 0.18f);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
img.raycastTarget = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 01:22:16 +09:00
|
|
|
|
private static Slider CreateSlider(Transform parent, string goName, Vector2 pos, Vector2 size)
|
2026-05-21 23:37:34 +09:00
|
|
|
|
{
|
2026-05-22 01:22:16 +09:00
|
|
|
|
var go = new GameObject(goName);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
go.transform.SetParent(parent, false);
|
|
|
|
|
|
SetRect(go, pos, size);
|
|
|
|
|
|
|
2026-05-22 01:22:16 +09:00
|
|
|
|
var bgGO = new GameObject("Background");
|
|
|
|
|
|
bgGO.transform.SetParent(go.transform, false);
|
|
|
|
|
|
StretchFull(bgGO);
|
|
|
|
|
|
bgGO.AddComponent<Image>().color = new Color(1f, 1f, 1f, 0.15f);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-05-22 01:22:16 +09:00
|
|
|
|
fill.AddComponent<Image>().color = new Color(0.3f, 0.8f, 0.3f, 0.9f);
|
2026-05-21 23:37:34 +09:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|