475 lines
14 KiB
C#
475 lines
14 KiB
C#
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
using UnityEngine.SceneManagement;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using DG.Tweening;
|
|
|
|
public class SongSelectUI : MonoBehaviour
|
|
{
|
|
private static readonly bool VerboseLogs = false;
|
|
public SongList songList; // Documentation text normalized.
|
|
public GameObject songButtonPrefab; // Documentation text normalized.
|
|
public Transform contentPanel; // Documentation text normalized.
|
|
|
|
[SerializeField] Button button_Main;
|
|
[SerializeField] string ui_Main_Scene_Name = "UI_UI";
|
|
[Header("Enter Animation")]
|
|
[SerializeField] bool play_Enter_Anim = true;
|
|
[SerializeField] float enter_Anim_Delay = 0.05f;
|
|
[Header("Async Build")]
|
|
[SerializeField] bool useAsyncBuild = true;
|
|
[SerializeField] int buildBatch = 8;
|
|
[Header("Switch Animations")]
|
|
[SerializeField] bool play_Switch_Anim = true;
|
|
[SerializeField] RectTransform list_Anim_Root;
|
|
[SerializeField] float list_FadeOut_Time = 0.12f;
|
|
[SerializeField] float list_FadeIn_Time = 0.18f;
|
|
[SerializeField] float list_Move_Offset = 24f;
|
|
[SerializeField] Ease list_Move_Ease = Ease.OutCubic;
|
|
[SerializeField] bool list_Use_Unscaled = true;
|
|
|
|
// Current DLC key used for per-DLC saved song selection
|
|
public static string CurrentDlcKey = "dlc_default_0";
|
|
public static bool ForceFirstSongOnNextRestore;
|
|
|
|
// Track pending restore to avoid multiple overlapping restores
|
|
private Coroutine restoreCoroutine;
|
|
private Coroutine buildCoroutine;
|
|
private Coroutine switchCoroutine;
|
|
private CanvasGroup listCanvasGroup;
|
|
private Vector2 listBasePos;
|
|
private bool listBaseCached;
|
|
|
|
void Start()
|
|
{
|
|
// Defensive reset: prevent select-scene enter animations from freezing invisible.
|
|
if (Time.timeScale <= 0f)
|
|
{
|
|
Time.timeScale = 1f;
|
|
}
|
|
|
|
if (songList == null || songButtonPrefab == null || contentPanel == null)
|
|
{
|
|
Debug.LogError("请在 Inspector 中分配所有字段!");
|
|
return;
|
|
}
|
|
|
|
button_Main.onClick.AddListener(
|
|
() => StartCoroutine(LoadSceneAsync(ui_Main_Scene_NAME())));
|
|
// Do not populate the song list at start. DLC buttons will request songs when clicked.
|
|
|
|
if (play_Enter_Anim)
|
|
{
|
|
UI_SelectSong_EnterAnim anim = GetComponent<UI_SelectSong_EnterAnim>();
|
|
if (anim == null)
|
|
{
|
|
anim = gameObject.AddComponent<UI_SelectSong_EnterAnim>();
|
|
}
|
|
anim.SetDelay(enter_Anim_Delay);
|
|
anim.Play();
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
KillActiveTweensAndCoroutines();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
KillActiveTweensAndCoroutines();
|
|
}
|
|
|
|
private void KillActiveTweensAndCoroutines()
|
|
{
|
|
if (buildCoroutine != null)
|
|
{
|
|
StopCoroutine(buildCoroutine);
|
|
buildCoroutine = null;
|
|
}
|
|
|
|
if (switchCoroutine != null)
|
|
{
|
|
StopCoroutine(switchCoroutine);
|
|
switchCoroutine = null;
|
|
}
|
|
|
|
if (restoreCoroutine != null)
|
|
{
|
|
StopCoroutine(restoreCoroutine);
|
|
restoreCoroutine = null;
|
|
}
|
|
|
|
RectTransform root = GetListAnimRoot();
|
|
if (root != null)
|
|
{
|
|
root.DOKill();
|
|
}
|
|
|
|
if (listCanvasGroup != null)
|
|
{
|
|
listCanvasGroup.DOKill();
|
|
}
|
|
}
|
|
|
|
private IEnumerator LoadSceneAsync(string sceneName)
|
|
{
|
|
if (Bansonic.gTransition.LoadScene(sceneName, LoadSceneMode.Single))
|
|
{
|
|
while (Bansonic.gTransition.IsBusy)
|
|
{
|
|
yield return null;
|
|
}
|
|
yield break;
|
|
}
|
|
|
|
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
|
|
while (asyncLoad != null && !asyncLoad.isDone)
|
|
{
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
// Documentation text normalized.
|
|
public void DisplaySongs()
|
|
{
|
|
DisplaySongsFromList(songList != null ? songList.songs : null);
|
|
}
|
|
|
|
// Instantiate from a provided list (used by dlcButton to show DLC songs)
|
|
public void DisplaySongsFromList(List<SongData> list)
|
|
{
|
|
if (buildCoroutine != null)
|
|
{
|
|
StopCoroutine(buildCoroutine);
|
|
}
|
|
if (switchCoroutine != null)
|
|
{
|
|
StopCoroutine(switchCoroutine);
|
|
switchCoroutine = null;
|
|
}
|
|
if (play_Switch_Anim)
|
|
{
|
|
switchCoroutine = StartCoroutine(SwitchSongsRoutine(list));
|
|
}
|
|
else
|
|
{
|
|
buildCoroutine = StartCoroutine(BuildSongsAsync(list));
|
|
}
|
|
}
|
|
|
|
private IEnumerator SwitchSongsRoutine(List<SongData> list)
|
|
{
|
|
yield return PlayListSwitchOut();
|
|
yield return BuildSongsAsync(list);
|
|
yield return PlayListSwitchIn();
|
|
switchCoroutine = null;
|
|
}
|
|
|
|
private IEnumerator BuildSongsAsync(List<SongData> list)
|
|
{
|
|
ClearSongList();
|
|
if (list == null)
|
|
{
|
|
buildCoroutine = null;
|
|
yield break;
|
|
}
|
|
|
|
// allow UI to render a frame before heavy instantiation
|
|
yield return null;
|
|
|
|
int count = 0;
|
|
foreach (var song in list)
|
|
{
|
|
InstantiateSongButton(song);
|
|
count++;
|
|
if (useAsyncBuild && buildBatch > 0 && (count % buildBatch) == 0)
|
|
{
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
// After populating, attempt to restore per-DLC selected song (next frame to avoid stale objects)
|
|
if (restoreCoroutine != null) StopCoroutine(restoreCoroutine);
|
|
restoreCoroutine = StartCoroutine(RestoreSelectedSongNextFrame());
|
|
buildCoroutine = null;
|
|
}
|
|
|
|
private RectTransform GetListAnimRoot()
|
|
{
|
|
if (list_Anim_Root != null)
|
|
{
|
|
return list_Anim_Root;
|
|
}
|
|
return contentPanel as RectTransform;
|
|
}
|
|
|
|
private CanvasGroup EnsureListCanvasGroup(RectTransform root)
|
|
{
|
|
if (root == null) return null;
|
|
if (listCanvasGroup == null || listCanvasGroup.gameObject != root.gameObject)
|
|
{
|
|
listCanvasGroup = root.GetComponent<CanvasGroup>();
|
|
if (listCanvasGroup == null)
|
|
{
|
|
listCanvasGroup = root.gameObject.AddComponent<CanvasGroup>();
|
|
}
|
|
}
|
|
return listCanvasGroup;
|
|
}
|
|
|
|
private void CacheListBase(RectTransform root)
|
|
{
|
|
if (root == null) return;
|
|
if (!listBaseCached)
|
|
{
|
|
listBasePos = root.anchoredPosition;
|
|
listBaseCached = true;
|
|
}
|
|
}
|
|
|
|
private IEnumerator PlayListSwitchOut()
|
|
{
|
|
if (!play_Switch_Anim)
|
|
{
|
|
yield break;
|
|
}
|
|
RectTransform root = GetListAnimRoot();
|
|
if (root == null)
|
|
{
|
|
yield break;
|
|
}
|
|
CacheListBase(root);
|
|
CanvasGroup cg = EnsureListCanvasGroup(root);
|
|
if (cg == null)
|
|
{
|
|
yield break;
|
|
}
|
|
root.DOKill();
|
|
cg.DOKill();
|
|
float moveOffset = list_Move_Offset;
|
|
bool canMove = !UI_OrderedEntryAnimator.IsLayoutSensitive(root);
|
|
if (!canMove)
|
|
{
|
|
moveOffset = 0f;
|
|
}
|
|
Sequence seq = DOTween.Sequence().SetUpdate(list_Use_Unscaled);
|
|
seq.Join(cg.DOFade(0f, list_FadeOut_Time));
|
|
if (canMove && Mathf.Abs(moveOffset) > 0.01f)
|
|
{
|
|
seq.Join(root.DOAnchorPos(listBasePos + new Vector2(-moveOffset, 0f), list_FadeOut_Time).SetEase(list_Move_Ease));
|
|
}
|
|
yield return seq.WaitForCompletion();
|
|
}
|
|
|
|
private IEnumerator PlayListSwitchIn()
|
|
{
|
|
if (!play_Switch_Anim)
|
|
{
|
|
yield break;
|
|
}
|
|
RectTransform root = GetListAnimRoot();
|
|
if (root == null)
|
|
{
|
|
yield break;
|
|
}
|
|
CacheListBase(root);
|
|
CanvasGroup cg = EnsureListCanvasGroup(root);
|
|
if (cg == null)
|
|
{
|
|
yield break;
|
|
}
|
|
root.DOKill();
|
|
cg.DOKill();
|
|
float moveOffset = list_Move_Offset;
|
|
bool canMove = !UI_OrderedEntryAnimator.IsLayoutSensitive(root);
|
|
if (!canMove)
|
|
{
|
|
moveOffset = 0f;
|
|
}
|
|
if (canMove && Mathf.Abs(moveOffset) > 0.01f)
|
|
{
|
|
root.anchoredPosition = listBasePos + new Vector2(moveOffset, 0f);
|
|
}
|
|
else if (canMove)
|
|
{
|
|
root.anchoredPosition = listBasePos;
|
|
}
|
|
cg.alpha = 0f;
|
|
Sequence seq = DOTween.Sequence().SetUpdate(list_Use_Unscaled);
|
|
seq.Join(cg.DOFade(1f, list_FadeIn_Time));
|
|
if (canMove && Mathf.Abs(moveOffset) > 0.01f)
|
|
{
|
|
seq.Join(root.DOAnchorPos(listBasePos, list_FadeIn_Time).SetEase(list_Move_Ease));
|
|
}
|
|
yield return seq.WaitForCompletion();
|
|
}
|
|
|
|
private IEnumerator RestoreSelectedSongNextFrame()
|
|
{
|
|
// Wait one frame so Destroy() in ClearSongList fully completes and stale SongButtons are gone.
|
|
yield return null;
|
|
RestoreSelectedSongForCurrentDlc();
|
|
restoreCoroutine = null;
|
|
}
|
|
|
|
private void RestoreSelectedSongForCurrentDlc()
|
|
{
|
|
// read saved index using CurrentDlcKey
|
|
string dlcKey = (string.IsNullOrEmpty(CurrentDlcKey) ? "default_0" : CurrentDlcKey);
|
|
string key = SongButton.BuildSongSelectedIndexPrefsKey(dlcKey);
|
|
string songIdKey = SongButton.BuildSongSelectedIdPrefsKey(dlcKey);
|
|
|
|
// collect song buttons under contentPanel
|
|
var allSongButtons = contentPanel.GetComponentsInChildren<SongButton>(true);
|
|
if (allSongButtons == null || allSongButtons.Length == 0)
|
|
{
|
|
if (VerboseLogs) Debug.LogWarning("SongSelectUI.RestoreSelectedSongForCurrentDlc: no song buttons found to restore selection");
|
|
return;
|
|
}
|
|
|
|
if (ForceFirstSongOnNextRestore)
|
|
{
|
|
SongDataHolder.SelectedSongData = null;
|
|
SongButton firstButton = allSongButtons[0];
|
|
ForceFirstSongOnNextRestore = false;
|
|
if (firstButton != null)
|
|
{
|
|
firstButton.OnSongButtonClick();
|
|
if (VerboseLogs) Debug.Log("SongSelectUI: forced fallback to the first song because no last-entered record was found.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Ensure every DLC has a default persisted value (0)
|
|
int savedIndex;
|
|
if (!PlayerPrefs.HasKey(key))
|
|
{
|
|
PlayerPrefs.SetInt(key, 0);
|
|
PlayerPrefs.Save();
|
|
savedIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
savedIndex = PlayerPrefs.GetInt(key, 0);
|
|
}
|
|
|
|
if (savedIndex < 0) savedIndex = 0;
|
|
if (savedIndex >= allSongButtons.Length) savedIndex = 0;
|
|
|
|
SongButton btn = null;
|
|
if (PlayerPrefs.HasKey(songIdKey))
|
|
{
|
|
int savedSongId = PlayerPrefs.GetInt(songIdKey, 0);
|
|
if (savedSongId > 0)
|
|
{
|
|
for (int i = 0; i < allSongButtons.Length; i++)
|
|
{
|
|
SongButton candidate = allSongButtons[i];
|
|
if (candidate == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (candidate.GetSongSerialId() == savedSongId)
|
|
{
|
|
btn = candidate;
|
|
savedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (btn == null)
|
|
{
|
|
btn = allSongButtons[savedIndex];
|
|
}
|
|
|
|
if (btn == null)
|
|
{
|
|
// hard fallback to first
|
|
btn = allSongButtons[0];
|
|
savedIndex = 0;
|
|
}
|
|
|
|
if (btn != null)
|
|
{
|
|
// simulate click
|
|
btn.OnSongButtonClick();
|
|
if (VerboseLogs) Debug.Log($"SongSelectUI: restored and clicked song index={savedIndex} for key={key}, totalButtons={allSongButtons.Length}");
|
|
}
|
|
}
|
|
|
|
private void ClearSongList()
|
|
{
|
|
if (contentPanel == null) return;
|
|
for (int i = contentPanel.childCount - 1; i >= 0; i--)
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying) DestroyImmediate(contentPanel.GetChild(i).gameObject);
|
|
else Destroy(contentPanel.GetChild(i).gameObject);
|
|
#else
|
|
Destroy(contentPanel.GetChild(i).gameObject);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private void InstantiateSongButton(SongData song)
|
|
{
|
|
if (songButtonPrefab == null || contentPanel == null) return;
|
|
|
|
GameObject songButtonObj = Instantiate(songButtonPrefab, contentPanel);
|
|
songButtonObj.transform.SetParent(contentPanel, false);
|
|
UI_OrderedEntryAnimator.PlayFadeOnly(songButtonObj, contentPanel.childCount - 1, 0.16f, 0.01f, true);
|
|
|
|
SongData sd = song;
|
|
if (SongDataLibrary.Instance != null && SongDataLibrary.Instance.IsLoaded)
|
|
{
|
|
sd = SongDataLibrary.Instance.GetSongDataByID(song.songID) ?? song;
|
|
}
|
|
int bestTotal = sd != null ? sd.personalRecord : song.personalRecord;
|
|
int bestDiff = -1;
|
|
string bestDiffName = "";
|
|
if (sd != null)
|
|
{
|
|
sd.GetAbsoluteHighestScore(out bestTotal, out bestDiff, out bestDiffName);
|
|
}
|
|
|
|
SongButton songButton = songButtonObj.GetComponent<SongButton>();
|
|
if (songButton != null)
|
|
{
|
|
songButton.thisSong_so = sd != null ? sd : song;
|
|
int diffForDisplay = bestDiff >= 0 ? bestDiff : song.difficultyID;
|
|
Sprite resolvedIllustration = sd != null ? sd.GetResolvedIllustration() : song.GetResolvedIllustration();
|
|
songButton.SetButtonData(song.songName, bestTotal, diffForDisplay, resolvedIllustration, song.songID);
|
|
}
|
|
|
|
TMP_Text[] texts = songButtonObj.GetComponentsInChildren<TMP_Text>();
|
|
|
|
if (texts.Length >= 4)
|
|
{
|
|
texts[0].text = song.songName;
|
|
texts[1].text = "Record : " + bestTotal;
|
|
if (bestTotal <= 0)
|
|
{
|
|
texts[3].text = "";
|
|
}
|
|
else
|
|
{
|
|
texts[3].text = string.IsNullOrEmpty(bestDiffName) ? bestDiff.ToString() : bestDiffName;
|
|
}
|
|
texts[2].text = "SongID : " + song.songID.ToString();
|
|
}
|
|
|
|
Button button = songButtonObj.GetComponent<Button>();
|
|
// SongButton.Awake already binds its own click handler. Avoid double invocation here.
|
|
}
|
|
|
|
// helper to avoid hardcoding scene name string in delegate - keeps serializer-friendly field
|
|
private string ui_Main_Scene_NAME() => ui_Main_Scene_Name;
|
|
}
|
|
|