using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; using UnityEngine.UI; #if UNITY_EDITOR using UnityEditor; #endif [DisallowMultipleComponent] public sealed class SceneSelectorMenu : MonoBehaviour { [Serializable] public sealed class SceneButtonBinding { [Tooltip("Optional UI label override. If empty, scene name from path is used.")] public string displayName; [SerializeField] private string scenePath; #if UNITY_EDITOR [Tooltip("Editor helper for selecting a scene asset. scenePath is synced from this field.")] public SceneAsset sceneAsset; #endif [Tooltip("UI Button for this scene.")] public Button button; [Tooltip("Optional text component for the button label.")] public TMP_Text buttonLabel; [Tooltip("Optional text component for availability hints.")] public TMP_Text availabilityLabel; public string ScenePath => scenePath; public string EffectiveLabel { get { if (!string.IsNullOrWhiteSpace(displayName)) { return displayName; } if (string.IsNullOrWhiteSpace(scenePath)) { return ""; } int slash = scenePath.LastIndexOf('/'); int dot = scenePath.LastIndexOf('.'); if (dot <= slash) { dot = scenePath.Length; } return scenePath.Substring(slash + 1, dot - slash - 1); } } #if UNITY_EDITOR public void SyncScenePathFromAsset() { if (sceneAsset == null) { return; } string path = AssetDatabase.GetAssetPath(sceneAsset); if (!string.IsNullOrWhiteSpace(path)) { scenePath = path; } } #endif } [SerializeField] private List sceneButtons = new(); [SerializeField] private TMP_Text statusLabel; [SerializeField] private bool disableButtonsWhileLoading = true; [SerializeField] private string missingPathText = "No scene assigned"; [SerializeField] private string missingBuildSettingsText = "Not in Build Settings"; [SerializeField] private string loadingPrefix = "Loading "; private readonly List clickActions = new(); private bool isLoading; private void OnEnable() { BindButtons(); RefreshBindings(); } private void OnDisable() { UnbindButtons(); } private void OnValidate() { #if UNITY_EDITOR for (int i = 0; i < sceneButtons.Count; i++) { sceneButtons[i].SyncScenePathFromAsset(); } #endif RefreshBindingVisuals(); } public void RefreshBindings() { RefreshBindingVisuals(); SetStatus(string.Empty); } public void LoadSceneByPath(string scenePath) { int buildIndex = SceneUtility.GetBuildIndexByScenePath(scenePath); if (buildIndex < 0) { SetStatus($"{missingBuildSettingsText}: {scenePath}"); Debug.LogWarning($"[SceneSelector] Scene not in build settings: {scenePath}"); return; } StartLoad(buildIndex, scenePath); } private void BindButtons() { UnbindButtons(); clickActions.Clear(); for (int i = 0; i < sceneButtons.Count; i++) { SceneButtonBinding binding = sceneButtons[i]; if (binding.button == null) { clickActions.Add(null); continue; } int capturedIndex = i; UnityAction action = () => OnSceneButtonPressed(capturedIndex); clickActions.Add(action); binding.button.onClick.AddListener(action); } } private void UnbindButtons() { int count = Mathf.Min(sceneButtons.Count, clickActions.Count); for (int i = 0; i < count; i++) { SceneButtonBinding binding = sceneButtons[i]; UnityAction action = clickActions[i]; if (binding.button != null && action != null) { binding.button.onClick.RemoveListener(action); } } clickActions.Clear(); } private void OnSceneButtonPressed(int index) { if (isLoading) { return; } if (index < 0 || index >= sceneButtons.Count) { return; } SceneButtonBinding binding = sceneButtons[index]; string scenePath = binding.ScenePath; string label = binding.EffectiveLabel; if (string.IsNullOrWhiteSpace(scenePath)) { SetStatus($"{missingPathText}: {label}"); return; } int buildIndex = SceneUtility.GetBuildIndexByScenePath(scenePath); if (buildIndex < 0) { SetStatus($"{missingBuildSettingsText}: {label}"); Debug.LogWarning($"[SceneSelector] Scene not in build settings: {scenePath}"); RefreshBindingVisuals(); return; } StartLoad(buildIndex, label); } private void StartLoad(int buildIndex, string label) { if (isLoading) { return; } StartCoroutine(LoadSceneRoutine(buildIndex, label)); } private IEnumerator LoadSceneRoutine(int buildIndex, string label) { isLoading = true; RefreshBindingVisuals(); SetStatus($"{loadingPrefix}{label}..."); AsyncOperation op = SceneManager.LoadSceneAsync(buildIndex, LoadSceneMode.Single); if (op == null) { isLoading = false; SetStatus($"Load failed: {label}"); RefreshBindingVisuals(); yield break; } while (!op.isDone) { yield return null; } } private void RefreshBindingVisuals() { for (int i = 0; i < sceneButtons.Count; i++) { SceneButtonBinding binding = sceneButtons[i]; string label = binding.EffectiveLabel; bool hasPath = !string.IsNullOrWhiteSpace(binding.ScenePath); bool inBuildSettings = hasPath && SceneUtility.GetBuildIndexByScenePath(binding.ScenePath) >= 0; bool available = hasPath && inBuildSettings; if (binding.buttonLabel != null) { binding.buttonLabel.text = label; } if (binding.availabilityLabel != null) { if (!hasPath) { binding.availabilityLabel.text = missingPathText; } else if (!inBuildSettings) { binding.availabilityLabel.text = missingBuildSettingsText; } else { binding.availabilityLabel.text = string.Empty; } } if (binding.button != null) { bool interactable = available; if (disableButtonsWhileLoading && isLoading) { interactable = false; } binding.button.interactable = interactable; } } } private void SetStatus(string message) { if (statusLabel != null) { statusLabel.text = message; } } }