add manual scene selector menu script

This commit is contained in:
2026-02-11 02:36:15 +01:00
parent b10ddf9762
commit e9294e3f93
3 changed files with 308 additions and 0 deletions

View File

@@ -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 "<missing scene>";
}
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<SceneButtonBinding> 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<UnityAction> 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;
}
}
}