590 lines
27 KiB
C#
590 lines
27 KiB
C#
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|