add manual scene selector menu script
This commit is contained in:
298
Assets/Scripts/SceneSelector/SceneSelectorMenu.cs
Normal file
298
Assets/Scripts/SceneSelector/SceneSelectorMenu.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user