299 lines
7.6 KiB
C#
299 lines
7.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|