Files
BeatSaber/Assets/Editor/VRBeatSaberSceneBuilder.cs
T
whdwo798 4dad9e5d5b feat: SongCreator 씬 완성 — Beat Sage URL 지원, info.dat 메타데이터 자동 추출
- BeatSageUploader: audio_url 지원(UploadFromUrl), PollAndDownload 공통화, ZIP 500 오류 3회 재시도
- BeatSageConverter: info.dat 파싱(SongMetadata), BPM 자동 감지 → 노트 타이밍 변환에 적용
- SongCreatorManager: title/BPM 필수 입력 제거, 난이도 4개 자동 선택, GenerateFlowFromUrl 버그 수정
- NasPublisher: audioPath null 허용(URL 흐름에서 로컬 파일 없는 경우 스킵)
- .gitignore/.gitattributes 초기 설정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 23:37:34 +09:00

590 lines
27 KiB
C#
Raw 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.Rendering;
using UnityEngine.UI;
using TMPro;
public static class VRBeatSaberSceneBuilder
{
private const string MenuScene = "Assets/VRBeatsKit/Scenes/Menu.unity";
private const string SongCreatorDest = "Assets/Scenes/SongCreator.unity";
// ─────────────────────────────────────────────
// ⓪ Fix — Set Graphics API to D3D11 (Oculus requirement)
// ─────────────────────────────────────────────
[MenuItem("Tools/VRBeatSaber/⓪ Fix — Set Graphics API to D3D11")]
public static void FixGraphicsAPI()
{
var d3d11 = new[] { GraphicsDeviceType.Direct3D11 };
PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneWindows, false);
PlayerSettings.SetUseDefaultGraphicsAPIs(BuildTarget.StandaloneWindows64, false);
PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneWindows, d3d11);
PlayerSettings.SetGraphicsAPIs(BuildTarget.StandaloneWindows64, d3d11);
Debug.Log("[SceneBuilder] ✓ Graphics API set to Direct3D11 for Windows.");
}
[MenuItem("Tools/VRBeatSaber/⓪ Fix — Allow HTTP connections")]
public static void FixAllowHttp()
{
PlayerSettings.insecureHttpOption = InsecureHttpOption.AlwaysAllowed;
Debug.Log("[SceneBuilder] ✓ Insecure HTTP connections allowed.");
}
// ─────────────────────────────────────────────
// Fix — Remove missing script components from open scene
// ─────────────────────────────────────────────
[MenuItem("Tools/VRBeatSaber/Fix — Remove Missing Scripts (open scene)")]
public static void RemoveMissingScripts()
{
int removed = 0;
foreach (var go in Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None))
{
var so = new SerializedObject(go);
var components = so.FindProperty("m_Component");
for (int i = components.arraySize - 1; i >= 0; i--)
{
var comp = components.GetArrayElementAtIndex(i)
.FindPropertyRelative("component")
.objectReferenceValue;
if (comp == null)
{
components.DeleteArrayElementAtIndex(i);
removed++;
}
}
so.ApplyModifiedPropertiesWithoutUndo();
}
var activeScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
EditorSceneManager.MarkSceneDirty(activeScene);
Debug.Log($"[SceneBuilder] ✓ Removed {removed} missing script(s) from '{activeScene.name}'.");
}
// ─────────────────────────────────────────────
// ① Menu — Add Song Creator Button
// ─────────────────────────────────────────────
[MenuItem("Tools/VRBeatSaber/① Menu — Add Song Creator Button")]
public static void PatchMenuAddSongCreatorButton()
{
var scene = EditorSceneManager.OpenScene(MenuScene, OpenSceneMode.Single);
var settings = GameObject.Find("Settings");
if (settings == null) { Debug.LogError("[SceneBuilder] 'Settings' not found."); return; }
if (settings.transform.Find("BG/Song Creator") != null)
{ Debug.LogWarning("[SceneBuilder] Button already exists."); return; }
var bg = settings.transform.Find("BG");
if (bg == null) { Debug.LogError("[SceneBuilder] 'Settings/BG' not found."); return; }
var btnGO = CreateStyledButton(bg, "Song Creator", new Vector2(0f, -20f), new Vector2(80f, 14f), 8f);
var loader = btnGO.AddComponent<VRBeats.LoadSceneButton>();
InjectPrivate(loader, "button", btnGO.GetComponent<Button>());
InjectPrivate(loader, "sceneName", "SongCreator");
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] ✓ 'Song Creator' button added to Menu > Settings > BG.");
}
// ─────────────────────────────────────────────
// ② Build SongCreator Scene
// ─────────────────────────────────────────────
[MenuItem("Tools/VRBeatSaber/② Build SongCreator Scene")]
public static void BuildSongCreatorScene()
{
if (AssetDatabase.LoadAssetAtPath<Object>(SongCreatorDest) != null)
AssetDatabase.DeleteAsset(SongCreatorDest);
if (!AssetDatabase.CopyAsset(MenuScene, SongCreatorDest))
{
Debug.LogError("[SceneBuilder] Failed to copy Menu.unity → SongCreator.unity");
return;
}
AssetDatabase.Refresh();
var scene = EditorSceneManager.OpenScene(SongCreatorDest, OpenSceneMode.Single);
// Remove unneeded objects
foreach (var name in new[] { "Logo", "SaberSelect", "SongSelect", "Sabers" })
{
var go = GameObject.Find(name);
if (go != null) Object.DestroyImmediate(go);
}
// Repurpose Settings panel
var panel = GameObject.Find("Settings");
if (panel == null) { Debug.LogError("[SceneBuilder] 'Settings' not found."); return; }
panel.name = "SongCreatorPanel";
// Face forward, position in front of player, larger canvas
var panelRect = panel.GetComponent<RectTransform>();
panelRect.localRotation = Quaternion.identity;
panelRect.localPosition = new Vector3(0f, 1.4f, 3.5f);
panelRect.sizeDelta = new Vector2(180f, 145f);
// Clear BG children and rebuild UI
var bg = panel.transform.Find("BG");
if (bg != null)
{
for (int i = bg.childCount - 1; i >= 0; i--)
Object.DestroyImmediate(bg.GetChild(i).gameObject);
}
else
{
var bgGO = new GameObject("BG");
bgGO.transform.SetParent(panel.transform, false);
var bgRect = bgGO.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.offsetMin = bgRect.offsetMax = Vector2.zero;
var bgImg = bgGO.AddComponent<Image>();
bgImg.color = new Color(0.22f, 0.40f, 0.49f, 0.49f);
bg = bgGO.transform;
}
// Add SongCreatorManager + dependencies to panel
var manager = panel.AddComponent<SongCreatorManager>();
var uploader = panel.AddComponent<BeatSageUploader>();
var publisher = panel.AddComponent<NasPublisher>();
// Build all UI and wire references
BuildSongCreatorUI(bg, manager, uploader, publisher);
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
Debug.Log("[SceneBuilder] ✓ SongCreator scene built: " + SongCreatorDest);
}
// ─────────────────────────────────────────────
// Song Creator UI (canvas 180 × 145)
// X: -90 ~ +90 / Y: -72.5 ~ +72.5
// ─────────────────────────────────────────────
private static void BuildSongCreatorUI(Transform bg,
SongCreatorManager manager, BeatSageUploader uploader, NasPublisher publisher)
{
var so = new SerializedObject(manager);
// ── Title ──
CreateLabel(bg, "Title", "SONG CREATOR", new Vector2(0, 64f), new Vector2(170f, 12f), 11f, Color.white, TextAlignmentOptions.Center);
CreateLabel(bg, "Subtitle", "Create beatmaps and upload to NAS",
new Vector2(0, 55f), new Vector2(170f, 7f), 5f, new Color(1,1,1,0.55f), TextAlignmentOptions.Center);
CreateDivider(bg, "Div0", new Vector2(0, 50f), new Vector2(168f, 0.5f));
// ── Audio Source ──
CreateLabel(bg, "LblAudio", "AUDIO SOURCE", new Vector2(-62f, 44f), new Vector2(40f, 6f), 4.5f, new Color(1,1,1,0.65f));
var dropdown = CreateDropdown(bg, "AudioDropdown", new Vector2(-8f, 36f), new Vector2(120f, 9f));
var refreshBtn = CreateStyledButton(bg, "Refresh", new Vector2(70f, 36f), new Vector2(24f, 9f), 4.5f);
var pathHint = CreateLabel(bg, "InputPathHint", "Path: ...", new Vector2(0f, 27f), new Vector2(168f, 6f), 3.8f, new Color(1,1,1,0.4f));
CreateDivider(bg, "Div1", new Vector2(0, 22f), new Vector2(168f, 0.5f));
// ── Add Audio ──
CreateLabel(bg, "LblAdd", "ADD AUDIO", new Vector2(-66f, 17f), new Vector2(34f, 6f), 4.5f, new Color(1,1,1,0.65f));
var fileBtn = CreateStyledButton(bg, "Browse File", new Vector2(-48f, 9f), new Vector2(44f, 9f), 5f);
var addStatus = CreateLabel(bg, "AddStatusText", "No file selected.", new Vector2(28f, 9f), new Vector2(88f, 9f), 4f, new Color(1,1,1,0.5f));
CreateLabel(bg, "LblOr", "— or —", new Vector2(0f, 1f), new Vector2(168f, 6f), 4f, new Color(1,1,1,0.4f), TextAlignmentOptions.Center);
var urlInput = CreateInputField(bg, "UrlInput", "https://example.com/song.mp3", new Vector2(-16f, -7f), new Vector2(120f, 9f));
var urlDlBtn = CreateStyledButton(bg, "Download", new Vector2(68f, -7f), new Vector2(28f, 9f), 4.5f);
CreateDivider(bg, "Div2", new Vector2(0, -12f), new Vector2(168f, 0.5f));
// ── Metadata ──
CreateLabel(bg, "LblMeta", "METADATA", new Vector2(-67f, -17f), new Vector2(30f, 6f), 4.5f, new Color(1,1,1,0.65f));
CreateLabel(bg, "LblTitle", "Title", new Vector2(-72f, -25f), new Vector2(18f, 7f), 4.2f, new Color(1,1,1,0.7f));
var titleInput = CreateInputField(bg, "TitleInput", "Song title", new Vector2(14f, -25f), new Vector2(130f, 8f));
CreateLabel(bg, "LblArtist", "Artist", new Vector2(-72f, -34f), new Vector2(18f, 7f), 4.2f, new Color(1,1,1,0.7f));
var artistInput = CreateInputField(bg, "ArtistInput", "Artist name", new Vector2(14f, -34f), new Vector2(130f, 8f));
CreateLabel(bg, "LblBpm", "BPM", new Vector2(-72f, -43f), new Vector2(18f, 7f), 4.2f, new Color(1,1,1,0.7f));
var bpmInput = CreateInputField(bg, "BpmInput", "120", new Vector2(14f, -43f), new Vector2(130f, 8f), TMP_InputField.ContentType.DecimalNumber);
CreateDivider(bg, "Div3", new Vector2(0, -48f), new Vector2(168f, 0.5f));
// ── Difficulty ──
CreateLabel(bg, "LblDiff", "DIFFICULTY", new Vector2(-64f, -53f), new Vector2(36f, 6f), 4.5f, new Color(1,1,1,0.65f));
var group = bg.gameObject.AddComponent<ToggleGroup>();
group.allowSwitchOff = true;
var tNormal = CreateToggle(bg, "ToggleNormal", "Normal", new Vector2(-60f, -61f), group, true);
var tHard = CreateToggle(bg, "ToggleHard", "Hard", new Vector2(-20f, -61f), group, true);
var tExpert = CreateToggle(bg, "ToggleExpert", "Expert", new Vector2( 20f, -61f), group, true);
var tExpertPlus= CreateToggle(bg, "ToggleExpertPlus","Expert+", new Vector2( 60f, -61f), group, true);
// ── Actions ──
var generateBtn = CreateStyledButton(bg, "Create & Upload", new Vector2(-20f, -69f), new Vector2(98f, 10f), 6f);
var backBtn = CreateStyledButton(bg, "Back to Menu", new Vector2( 65f, -69f), new Vector2(36f, 10f), 4.5f);
// ── Progress (hidden by default) ──
var progressGO = new GameObject("ProgressGroup");
progressGO.transform.SetParent(bg, false);
SetRect(progressGO, new Vector2(0f, -69f), new Vector2(170f, 10f));
progressGO.SetActive(false);
var statusTxt = CreateLabel(progressGO.transform, "StatusText", "Ready.",
new Vector2(-10f, 0f), new Vector2(120f, 8f), 4f, new Color(1,1,1,0.8f));
var sliderGO = CreateSlider(progressGO.transform, "ProgressSlider",
new Vector2(65f, 0f), new Vector2(50f, 6f));
// ── Wire SerializeField references ──
so.FindProperty("audioDropdown") .objectReferenceValue = dropdown;
so.FindProperty("refreshBtn") .objectReferenceValue = refreshBtn.GetComponent<Button>();
so.FindProperty("inputPathHint") .objectReferenceValue = pathHint.GetComponent<TMP_Text>();
so.FindProperty("filePickerBtn") .objectReferenceValue = fileBtn.GetComponent<Button>();
so.FindProperty("addStatusText") .objectReferenceValue = addStatus.GetComponent<TMP_Text>();
so.FindProperty("urlInput") .objectReferenceValue = urlInput;
so.FindProperty("urlDownloadBtn") .objectReferenceValue = urlDlBtn.GetComponent<Button>();
so.FindProperty("titleInput") .objectReferenceValue = titleInput;
so.FindProperty("artistInput") .objectReferenceValue = artistInput;
so.FindProperty("bpmInput") .objectReferenceValue = bpmInput;
so.FindProperty("toggleNormal") .objectReferenceValue = tNormal;
so.FindProperty("toggleHard") .objectReferenceValue = tHard;
so.FindProperty("toggleExpert") .objectReferenceValue = tExpert;
so.FindProperty("toggleExpertPlus") .objectReferenceValue = tExpertPlus;
so.FindProperty("generateButton") .objectReferenceValue = generateBtn.GetComponent<Button>();
so.FindProperty("backButton") .objectReferenceValue = backBtn.GetComponent<Button>();
so.FindProperty("progressGroup") .objectReferenceValue = progressGO;
so.FindProperty("statusText") .objectReferenceValue = statusTxt.GetComponent<TMP_Text>();
so.FindProperty("progressSlider") .objectReferenceValue = sliderGO;
so.FindProperty("beatSageUploader") .objectReferenceValue = uploader;
so.FindProperty("nasPublisher") .objectReferenceValue = publisher;
so.ApplyModifiedPropertiesWithoutUndo();
// Back button → Menu scene
var backLoader = backBtn.AddComponent<VRBeats.LoadSceneButton>();
InjectPrivate(backLoader, "button", backBtn.GetComponent<Button>());
InjectPrivate(backLoader, "sceneName", "Menu");
}
// ─────────────────────────────────────────────
// Helpers — UI factory
// ─────────────────────────────────────────────
private static GameObject CreateStyledButton(Transform parent, string label,
Vector2 pos, Vector2 size, float fontSize)
{
var go = new GameObject(label);
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.961f, 0.961f, 0.961f, 1f);
c.pressedColor = new Color(0.784f, 0.784f, 0.784f, 1f);
c.selectedColor = new Color(0.961f, 0.961f, 0.961f, 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 name, string text,
Vector2 pos, Vector2 size, float fontSize,
Color? color = null, TextAlignmentOptions align = TextAlignmentOptions.MidlineLeft)
{
var go = new GameObject(name);
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 name, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
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 TMP_Dropdown CreateDropdown(Transform parent, string name, Vector2 pos, Vector2 size)
{
// Root
var go = new GameObject(name);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var bgImg = go.AddComponent<Image>();
bgImg.color = new Color(1f, 1f, 1f, 0.1f);
var dd = go.AddComponent<TMP_Dropdown>();
// Caption label
var lbl = new GameObject("Label");
lbl.transform.SetParent(go.transform, false);
var lblRect = lbl.AddComponent<RectTransform>();
lblRect.anchorMin = Vector2.zero;
lblRect.anchorMax = Vector2.one;
lblRect.offsetMin = new Vector2(4f, 2f);
lblRect.offsetMax = new Vector2(-14f, -2f);
var lblTmp = lbl.AddComponent<TextMeshProUGUI>();
lblTmp.fontSize = 4.5f;
lblTmp.color = Color.white;
lblTmp.alignment = TextAlignmentOptions.MidlineLeft;
dd.captionText = lblTmp;
// Arrow
var arrow = new GameObject("Arrow");
arrow.transform.SetParent(go.transform, false);
SetRect(arrow, new Vector2(size.x * 0.5f - 6f, 0f), new Vector2(8f, 8f));
var arrowTmp = arrow.AddComponent<TextMeshProUGUI>();
arrowTmp.text = "▼";
arrowTmp.fontSize = 4f;
arrowTmp.color = new Color(1f, 1f, 1f, 0.7f);
arrowTmp.alignment = TextAlignmentOptions.Center;
// ── Template (must be inactive) ──────────────────────
var tmpl = new GameObject("Template");
tmpl.transform.SetParent(go.transform, false);
var tmplRect = tmpl.AddComponent<RectTransform>();
tmplRect.anchorMin = new Vector2(0f, 0f);
tmplRect.anchorMax = new Vector2(1f, 0f);
tmplRect.pivot = new Vector2(0.5f, 1f);
tmplRect.anchoredPosition = new Vector2(0f, 2f);
tmplRect.sizeDelta = new Vector2(0f, 60f);
var tmplImg = tmpl.AddComponent<Image>();
tmplImg.color = new Color(0.15f, 0.15f, 0.15f, 0.97f);
var scroll = tmpl.AddComponent<ScrollRect>();
// Viewport
var vp = new GameObject("Viewport");
vp.transform.SetParent(tmpl.transform, false);
var vpRect = vp.AddComponent<RectTransform>();
vpRect.anchorMin = Vector2.zero;
vpRect.anchorMax = Vector2.one;
vpRect.offsetMin = vpRect.offsetMax = Vector2.zero;
vpRect.pivot = new Vector2(0f, 1f);
vp.AddComponent<Image>().color = new Color(0f, 0f, 0f, 0.01f);
vp.AddComponent<Mask>().showMaskGraphic = false;
// Content
var content = new GameObject("Content");
content.transform.SetParent(vp.transform, false);
var contentRect = content.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 = new Vector2(0f, 28f);
scroll.content = contentRect;
scroll.viewport = vpRect;
scroll.horizontal = false;
scroll.movementType = ScrollRect.MovementType.Clamped;
scroll.scrollSensitivity = 10f;
// Item (Toggle) — the row template
var item = new GameObject("Item");
item.transform.SetParent(content.transform, false);
var itemRect = item.AddComponent<RectTransform>();
itemRect.anchorMin = new Vector2(0f, 0.5f);
itemRect.anchorMax = new Vector2(1f, 0.5f);
itemRect.pivot = new Vector2(0.5f, 0.5f);
itemRect.sizeDelta = new Vector2(0f, 9f);
itemRect.anchoredPosition = Vector2.zero;
var itemToggle = item.AddComponent<Toggle>();
var itemBg = item.AddComponent<Image>();
itemBg.color = new Color(1f, 1f, 1f, 0f);
itemToggle.targetGraphic = itemBg;
var tc = itemToggle.colors;
tc.highlightedColor = new Color(0.3f, 0.6f, 1f, 0.4f);
tc.selectedColor = new Color(0.3f, 0.6f, 1f, 0.4f);
itemToggle.colors = tc;
// Item Label
var itemLbl = new GameObject("Item Label");
itemLbl.transform.SetParent(item.transform, false);
var ilRect = itemLbl.AddComponent<RectTransform>();
ilRect.anchorMin = Vector2.zero;
ilRect.anchorMax = Vector2.one;
ilRect.offsetMin = new Vector2(4f, 1f);
ilRect.offsetMax = new Vector2(-4f, -1f);
var itemTmp = itemLbl.AddComponent<TextMeshProUGUI>();
itemTmp.fontSize = 4.5f;
itemTmp.color = Color.white;
itemTmp.alignment = TextAlignmentOptions.MidlineLeft;
dd.itemText = itemTmp;
dd.template = tmplRect;
tmpl.SetActive(false);
return dd;
}
private static TMP_InputField CreateInputField(Transform parent, string name,
string placeholder, Vector2 pos, Vector2 size,
TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var bg = go.AddComponent<Image>();
bg.color = new Color(1f, 1f, 1f, 0.08f);
var area = new GameObject("Text Area");
area.transform.SetParent(go.transform, false);
var ar = area.AddComponent<RectTransform>();
ar.anchorMin = Vector2.zero;
ar.anchorMax = Vector2.one;
ar.offsetMin = new Vector2(3f, 1f);
ar.offsetMax = new Vector2(-3f, -1f);
area.AddComponent<RectMask2D>();
var ph = new GameObject("Placeholder");
ph.transform.SetParent(area.transform, false);
StretchFull(ph);
var phTmp = ph.AddComponent<TextMeshProUGUI>();
phTmp.text = placeholder;
phTmp.fontSize = 4.5f;
phTmp.color = new Color(1f, 1f, 1f, 0.3f);
phTmp.alignment = TextAlignmentOptions.MidlineLeft;
var txt = new GameObject("Text");
txt.transform.SetParent(area.transform, false);
StretchFull(txt);
var txtTmp = txt.AddComponent<TextMeshProUGUI>();
txtTmp.fontSize = 4.5f;
txtTmp.color = Color.white;
txtTmp.alignment = TextAlignmentOptions.MidlineLeft;
var field = go.AddComponent<TMP_InputField>();
field.textComponent = txtTmp;
field.placeholder = phTmp;
field.textViewport = ar;
field.contentType = contentType;
return field;
}
private static Toggle CreateToggle(Transform parent, string name, string label,
Vector2 pos, ToggleGroup group, bool isOn)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
SetRect(go, pos, new Vector2(36f, 8f));
var img = go.AddComponent<Image>();
img.color = new Color(1f, 1f, 1f, 0.1f);
var toggle = go.AddComponent<Toggle>();
toggle.targetGraphic = img;
toggle.group = group;
toggle.isOn = isOn;
var tc = toggle.colors;
tc.normalColor = new Color(1f, 1f, 1f, 0.1f);
tc.highlightedColor = new Color(0.4f, 0.7f, 1f, 0.5f);
tc.pressedColor = new Color(0.2f, 0.5f, 0.9f, 0.8f);
tc.selectedColor = new Color(0.3f, 0.6f, 1f, 0.6f);
toggle.colors = tc;
var lbl = new GameObject("Label");
lbl.transform.SetParent(go.transform, false);
StretchFull(lbl);
var tmp = lbl.AddComponent<TextMeshProUGUI>();
tmp.text = label;
tmp.fontSize = 4f;
tmp.color = Color.white;
tmp.alignment = TextAlignmentOptions.Center;
return toggle;
}
private static Slider CreateSlider(Transform parent, string name, Vector2 pos, Vector2 size)
{
var go = new GameObject(name);
go.transform.SetParent(parent, false);
SetRect(go, pos, size);
var bg = new GameObject("Background");
bg.transform.SetParent(go.transform, false);
StretchFull(bg);
var bgImg = bg.AddComponent<Image>();
bgImg.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);
var fillImg = fill.AddComponent<Image>();
fillImg.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;
}
private static void InjectPrivate(object target, string fieldName, object value)
{
target.GetType()
.GetField(fieldName,
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
?.SetValue(target, value);
}
}