Files
ichni_Official/Assets/Scripts/UI/SongSelection/SongListControllerUI.cs
SoulliesOfficial 810d019619 剧情+对话完善
2026-07-21 15:24:42 -04:00

431 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Sirenix.OdinInspector;
using SLSUtilities.WwiseAssistance;
namespace Ichni.Menu
{
// 一个完全自定义的列表控制器,实现了拖拽、惯性、边界和吸附
public partial class SongListControllerUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Title("核心组件")]
[SerializeField] public RectTransform content;
[SerializeField] public RectTransform viewport;
[Title("预制体")]
[SerializeField]
private GameObject songItemPrefab;
[Title("对齐与动画")]
[SerializeField] public RectTransform centerPoint;
[SerializeField] private float snapSpeed = 10f;
[SerializeField] private float decelerationRate = 0.15f;
[Title("平滑度优化")]
[SerializeField] [Range(1f, 20f)]
private float dragSmoothing = 16f; // 拖拽平滑度的阻尼值可在Inspector中调节
[SerializeField][Range(1f, 20f)]
private float releaseSmoothing = 4f; // 松手后平滑度的阻尼值可在Inspector中调节
[Title("甩动判定")]
[Tooltip("当松手时的速度大于此值,才会被判定为一次“甩动”并产生惯性")]
[SerializeField] private float flickThreshold = 50f;
public SongSelectionTab selectedTab;
// 内部变量
public List<RectTransform> songItems = new List<RectTransform>();
private Vector2 velocity;
private bool isDragging = false;
public float topBound;
public float bottomBound = 0f;
private Vector2 targetPosition; // 【新增】内容的目标位置
private float targetX = 1500f;
public bool isDuringSnap = false;
public RectTransform closestTab;
// 仅供 Story SongBlock 在“下一次初始化列表”时指定初始歌曲;不写入章节缓存,
// 以免剧情入口覆盖玩家平时在该章节记录的歌曲/难度选择。
private SongItemData _initialSongSelectionOverride;
/// <summary>
/// 请求下一次 <see cref="InitializeList"/> 自动选中指定歌曲。
/// 此请求是一次性的:无论目标是否仍存在,初始化完成后都会清除。
/// </summary>
public void RequestInitialSongSelection(SongItemData song)
{
_initialSongSelectionOverride = song;
}
public void InitializeList()
{
// FadeOut 通常已经清理过旧 Tab但这里仍保持幂等防止重复初始化时残留旧引用或重复 UI。
ClearSongTabs();
GenerateSongTabs();
if (songItems.Count == 0)
{
// 空 Chapter 是内容异常;运行时只进入安全状态,不访问 songItems[0],也不允许进入游戏。
selectedTab = null;
closestTab = null;
topBound = content.anchoredPosition.y;
bottomBound = content.anchoredPosition.y;
targetPosition = content.anchoredPosition;
MenuManager.instance.songSelectionUIPage.selectedSong = null;
MenuManager.instance.songSelectionUIPage.selectedSave = null;
MenuManager.instance.songSelectionUIPage.difficultySelectionContainer.SetUp(null);
_initialSongSelectionOverride = null;
return;
}
Canvas.ForceUpdateCanvases();
topBound = (songItems.Count * 144f + (songItems.Count - 1) * 60f) - 72f; //topBound中144为tab高度60为tab间距72为tab高度的一半
bottomBound = 72f; //bottomBound中72为tab高度的一半
int songIndex = 0;
int preferredDifficultyIndex = 0;
ChapterSelectionUnit currentChapter = ChapterSelectionManager.instance.currentChapter;
MenuInformationRecorder menuInformationRecorder = MenuInformationRecorder.instance;
if (menuInformationRecorder.TryGetRecordOfThisChapter(out SongSelectionRecord record))
{
preferredDifficultyIndex = record.difficultyListIndex;
SongItemData cachedSong = record.song;
bool cacheNeedsRepair = false;
// 优先按对象引用恢复;若缓存来自内容热更新/重建,再按 songName 进行兼容匹配。
songIndex = songItems.FindIndex(item => item != null && item.GetComponent<SongSelectionTab>()?.connectedSong == cachedSong);
if (songIndex < 0 && !string.IsNullOrEmpty(cachedSong?.songName))
{
songIndex = songItems.FindIndex(item => item != null &&
item.GetComponent<SongSelectionTab>()?.connectedSong?.songName == cachedSong.songName);
cacheNeedsRepair = songIndex >= 0;
}
// 缓存歌曲已经被删除或改名时,立即修正缓存;难度由 SnapToItem 内的统一回退机制继续解析。
if (songIndex < 0)
{
songIndex = 0;
cacheNeedsRepair = true;
}
if (cacheNeedsRepair)
{
SongItemData resolvedSong = songItems[songIndex].GetComponent<SongSelectionTab>().connectedSong;
menuInformationRecorder.SetRecordForChapter(currentChapter, resolvedSong, preferredDifficultyIndex);
}
}
// Story SongBlock 的目标歌曲优先于常规章节缓存,但只影响本次页面打开。
// 先比对对象引用,再按稳定 songName 兼容热更新或重建后的 SongItemData 实例。
if (_initialSongSelectionOverride != null)
{
int overrideIndex = songItems.FindIndex(item => item != null &&
item.GetComponent<SongSelectionTab>()?.connectedSong == _initialSongSelectionOverride);
if (overrideIndex < 0 && !string.IsNullOrEmpty(_initialSongSelectionOverride.songName))
{
overrideIndex = songItems.FindIndex(item => item != null &&
item.GetComponent<SongSelectionTab>()?.connectedSong?.songName ==
_initialSongSelectionOverride.songName);
}
if (overrideIndex >= 0)
{
songIndex = overrideIndex;
}
else
{
Debug.LogWarning($"[SongListControllerUI] 请求自动选中的歌曲 '{_initialSongSelectionOverride.songName}' 不在当前章节列表中,已保留常规缓存选择。");
}
_initialSongSelectionOverride = null;
}
StartCoroutine(SnapToItem(songItems[songIndex], true));
closestTab = songItems[songIndex];
targetPosition = content.anchoredPosition;
targetX = -1500f;
}
private void Update()
{
RectTransform closestItem = closestTab;
float cloesestY = Mathf.Infinity;
foreach (RectTransform item in songItems)
{
float distance = Mathf.Abs(item.position.y - centerPoint.position.y);
if (distance < cloesestY)
{
cloesestY = distance;
closestItem = item;
}
}
if (closestItem != null && closestTab != closestItem)
{
closestTab = closestItem;
AudioManager.Post(AK.EVENTS.SWITCHTAB);
}
}
// 使用LateUpdate来处理所有位置更新防止抖动
void LateUpdate()
{
// 如果不在拖拽,则处理惯性
// 注意:我们只在 isDragging 为 false 时处理惯性,
// OnEndDrag中已经对速度进行了判断所以这里的逻辑依然适用
if (!isDragging && Mathf.Abs(velocity.y) > 0.1f)
{
// 将速度施加于目标位置
targetPosition.y += velocity.y * Time.deltaTime;
// 速度衰减
velocity *= (1 - decelerationRate);
// 当速度足够小时,停止惯性并触发吸附
if (Mathf.Abs(velocity.y) <= 0.1f)
{
velocity = Vector2.zero;
StartCoroutine(SnapToClosest());
}
}
// 无论何时都将Content的实际位置平滑地Lerp到目标位置
float damping = isDragging ? dragSmoothing : releaseSmoothing;
Vector2 finalPosition = Vector2.Lerp(content.anchoredPosition, targetPosition, Time.deltaTime * damping);
finalPosition.x = targetX;
content.anchoredPosition = finalPosition;
// 每次更新后都施加边界限制
ClampContentPosition();
}
public void OnBeginDrag(PointerEventData eventData)
{
isDragging = true;
velocity = Vector2.zero;
StopAllCoroutines();
// 开始拖拽时,将目标位置与当前位置同步
targetPosition = content.anchoredPosition;
selectedTab?.SetSelection(false);
selectedTab = null; // 清除当前选中的Tab
//DOTween.To(x=>targetX = x, targetX, -1550f, 0.2f).SetEase(Ease.OutQuad).Play();
//songItems.ForEach(item => item.DOScale(1.1f,0.2f).SetEase(Ease.OutQuad).Play());
}
public void OnDrag(PointerEventData eventData)
{
// 拖拽时只更新目标位置而不是直接移动Content
targetPosition += new Vector2(0, eventData.delta.y);
// 【核心修正 #1】计算速度时只使用Y轴分量
velocity = new Vector2(0, eventData.delta.y / Time.deltaTime);
}
public void OnEndDrag(PointerEventData eventData)
{
isDragging = false;
//DOTween.To(x => targetX = x, targetX, -1500f, 0.2f).SetEase(Ease.OutQuad).Play();
//songItems.ForEach(item => item.DOScale(1,0.2f).SetEase(Ease.OutQuad).Play());
// 【核心修正】在这里根据速度决定下一步做什么
if (Mathf.Abs(velocity.y) > flickThreshold)
{
// 速度足够大,是“甩动”,让 LateUpdate 中的惯性逻辑接管
// 我们什么都不用做LateUpdate会自动处理
}
else
{
// 速度很小,是“慢速拖拽”,立即开始吸附
velocity = Vector2.zero; // 清除残余速度
StartCoroutine(SnapToClosest());
}
}
private void ClampContentPosition()
{
// 【核心修正 #2】现在我们限制的是目标位置让Lerp去处理实际位置
targetPosition.y = Mathf.Clamp(targetPosition.y, bottomBound, topBound);
}
}
public partial class SongListControllerUI
{
public void ClearSongTabs()
{
StopAllCoroutines();
foreach (RectTransform item in songItems)
{
if (item != null)
{
Destroy(item.gameObject);
}
}
songItems.Clear();
selectedTab = null;
closestTab = null;
velocity = Vector2.zero;
isDragging = false;
SnapCoroutine = null;
targetPosition = content.anchoredPosition;
isDuringSnap = false;
}
public void GenerateSongTabs()
{
ChapterSelectionUnit chapterUnit = ChapterSelectionManager.instance.currentChapter;
if (chapterUnit == null || chapterUnit.songs == null)
{
return;
}
foreach (SongItemData song in chapterUnit.songs)
{
if (song == null)
{
Debug.LogWarning("Skipped an empty song entry while generating song selection tabs.");
continue;
}
SongSelectionTab tab = Instantiate(songItemPrefab, content).GetComponent<SongSelectionTab>();
songItems.Add(tab.GetComponent<RectTransform>());
tab.SetUpTab(song);
}
}
}
public partial class SongListControllerUI
{
public void GoToFormerTab()
{
if (closestTab != null)
{
int currentIndex = songItems.IndexOf(closestTab);
if (currentIndex > 0)
{
SongSelectionTab formerTab = songItems[currentIndex - 1].GetComponent<SongSelectionTab>();
StartCoroutine(SnapToTab(formerTab));
}
}
}
public void GoToNextTab()
{
if (closestTab != null)
{
int currentIndex = songItems.IndexOf(closestTab);
if (currentIndex < songItems.Count - 1)
{
SongSelectionTab nextTab = songItems[currentIndex + 1].GetComponent<SongSelectionTab>();
StartCoroutine(SnapToTab(nextTab));
}
}
}
}
public partial class SongListControllerUI
{
private IEnumerator SnapCoroutine;
// SnapToClosest 和 SnapToItem 这两个协程也需要微调,以确保它们能正确地与新的平滑系统协作
private IEnumerator SnapToClosest()
{
// ... (寻找最近项的逻辑不变) ...
yield return new WaitForEndOfFrame();
float minDistance = float.MaxValue;
RectTransform closestItem = null;
foreach (RectTransform item in songItems)
{
float distance = Mathf.Abs(item.position.y - centerPoint.position.y);
if (distance < minDistance)
{
minDistance = distance;
closestItem = item;
}
}
if (closestItem != null)
{
yield return SnapToItem(closestItem, false);
}
}
public IEnumerator SnapToTab(SongSelectionTab tab)
{
selectedTab?.SetSelection(false);
selectedTab = null; // 清除当前选中的Tab
if (isDuringSnap && SnapCoroutine != null)
{
StopCoroutine(SnapCoroutine);
}
SnapCoroutine = SnapToItem(tab.GetComponent<RectTransform>(), false);
yield return StartCoroutine(SnapCoroutine);
}
private IEnumerator SnapToItem(RectTransform targetItem, bool immediate)
{
if (!immediate)
{
yield return new WaitForEndOfFrame();
}
Debug.Log("开始对齐到: " + targetItem.name);
selectedTab = targetItem.GetComponent<SongSelectionTab>();
selectedTab.SetSelection(true);
SongItemData connectedSong = selectedTab.connectedSong;
MenuManager.instance.songSelectionUIPage.selectedSong = connectedSong;
MenuManager.instance.songSelectionUIPage.selectedSave = GameSaveManager.instance.SongSaveModule.GetSongStatusSave(connectedSong.songName);
Vector3 closestItemLocalPos = viewport.InverseTransformPoint(targetItem.position);
Vector3 centerPointLocalPos = viewport.InverseTransformPoint(centerPoint.position);
float localOffsetY = centerPointLocalPos.y - closestItemLocalPos.y;
Vector2 finalTargetPosition = content.anchoredPosition + new Vector2(0, localOffsetY);
finalTargetPosition.y = Mathf.Clamp(finalTargetPosition.y, bottomBound, topBound);
if (immediate)
{
// 立即模式直接设置Content和Target
targetPosition = finalTargetPosition;
content.anchoredPosition = finalTargetPosition;
}
else
{
isDuringSnap = true;
// 动画模式只更新Target让LateUpdate中的Lerp来完成动画
targetPosition = finalTargetPosition;
// 我们也可以在这里保留一个独立的Lerp循环以使用不同的snapSpeed
velocity = Vector2.zero;
while (Mathf.Abs(content.anchoredPosition.y - targetPosition.y) > 1f)
{
content.anchoredPosition = Vector2.Lerp(content.anchoredPosition, targetPosition, Time.deltaTime * snapSpeed);
yield return null;
}
content.anchoredPosition = targetPosition;
}
Debug.Log($"已对齐到: {targetItem.GetComponent<SongSelectionTab>().songNameText.text}");
SongSelectionManager.instance.SetPreview(connectedSong, selectedTab.isLocked);
MenuManager.instance.songSelectionUIPage.difficultySelectionContainer.SetUp(connectedSong.difficultyDataList);
MenuManager.instance.songSelectionUIPage.songInfoUI.SetIllustration(connectedSong.illustration, connectedSong.illustratorName);
isDuringSnap = false;
}
}
}