diff --git a/Assets/Scripts/SceneSelector.meta b/Assets/Scripts/SceneSelector.meta new file mode 100644 index 000000000..6f9c2f87b --- /dev/null +++ b/Assets/Scripts/SceneSelector.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f8423f63139d466ab69f880af986ca11 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SceneSelector/SceneSelectorMenu.cs b/Assets/Scripts/SceneSelector/SceneSelectorMenu.cs new file mode 100644 index 000000000..572d7d83c --- /dev/null +++ b/Assets/Scripts/SceneSelector/SceneSelectorMenu.cs @@ -0,0 +1,298 @@ +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; + } + } +} diff --git a/Assets/Scripts/SceneSelector/SceneSelectorMenu.cs.meta b/Assets/Scripts/SceneSelector/SceneSelectorMenu.cs.meta new file mode 100644 index 000000000..e13014440 --- /dev/null +++ b/Assets/Scripts/SceneSelector/SceneSelectorMenu.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e7d5b4a60c6d48d8bc0e3f66f934f44a