TH1/Unity/Assets/Scripts/TH1_UI/View/Outside/UIOutsideLibraryMusicPanelMono.cs
2026-05-11 01:06:16 +08:00

420 lines
17 KiB
C#

using System.Collections.Generic;
using Animancer;
using Logic.Audio;
using Logic.Multilingual;
using RuntimeData;
using TH1Resource;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace TH1_UI.View.Outside
{
/// <summary>
/// 音乐鉴赏室播放区:碟片区 + 各种文字信息 + 封面/背景
/// 每个文字字段需要两个对象:Root(父GO,决定是否显示) + Text(TMP)
/// 缺数据/空值时整组Root隐藏
/// </summary>
public class UIOutsideLibraryMusicPanelMono : MonoBehaviour
{
[Header("碟片区(无具体数据,常驻显示)")]
public GameObject DiscRoot;
[Tooltip("旋转的碟片RectTransform,Update中沿Z轴旋转")]
public RectTransform DiscRotator;
[Tooltip("碟片每秒旋转角度(负值=顺时针)")]
public float DiscRotateSpeed = -60f;
[Header("Tag共享父Layout(Civ/Force横排容器,会被强制刷新)")]
[Tooltip("Civ和Force两个Tag的共同父RectTransform,自身带HorizontalLayoutGroup/ContentSizeFitter,内容变化后需rebuild")]
public RectTransform TagLayoutRoot;
[Header("Title共享父Layout(标题/原曲名等共用容器,会被强制刷新)")]
[Tooltip("Title/OriginalTitle 等所在的父RectTransform,内容变化后需rebuild")]
public RectTransform TitleLayoutRoot;
[Header("曲名")]
public GameObject TitleRoot;
public TextMeshProUGUI TitleText;
[Header("原曲名")]
public GameObject OriginalTitleRoot;
public TextMeshProUGUI OriginalTitleText;
[Header("作曲")]
public GameObject ComposerRoot;
public TextMeshProUGUI ComposerText;
[Header("编曲")]
public GameObject ArrangerRoot;
public TextMeshProUGUI ArrangerText;
[Header("混音")]
public GameObject MixerRoot;
public TextMeshProUGUI MixerText;
[Header("演唱")]
public GameObject VocalistRoot;
public TextMeshProUGUI VocalistText;
[Header("所属阵营")]
public GameObject ForceRoot;
public TextMeshProUGUI ForceText;
[Header("所属文明")]
public GameObject CivRoot;
public TextMeshProUGUI CivText;
[Header("曲绘")]
public GameObject IllustratorRoot;
public TextMeshProUGUI IllustratorText;
[Header("封面图片")]
public GameObject CoverRoot;
public Image CoverImage;
[Header("背景图片")]
public GameObject BackgroundRoot;
public Image BackgroundImage;
[Header("猜歌模式")]
[Tooltip("MusicInfo整个信息区父对象(包含标题/作曲/Tag/封面等所有信息),猜歌模式下隐藏")]
public GameObject MusicInfoRoot;
[Tooltip("MusicInfo淡入淡出用的Animancer(挂在MusicInfoRoot同一个GO上)")]
public AnimancerComponent MusicInfoAnimancer;
[Tooltip("猜歌区父对象,猜歌模式下显示")]
public GameObject GuessArea;
[Tooltip("GuessArea淡入淡出用的Animancer(挂在GuessArea同一个GO上)")]
public AnimancerComponent GuessAreaAnimancer;
[Tooltip("GuessArea内的揭晓按钮:点击后GuessArea隐藏、MusicInfo显示")]
public Button GuessButton;
// 当前正在播放的曲目MusicName(空=没在播,Update中据此停止旋转)
private string _currentPlayingMusicName;
// 当前是否在猜歌模式
private bool _isGuessMode;
private void Awake()
{
if (GuessButton != null)
{
GuessButton.onClick.RemoveAllListeners();
GuessButton.onClick.AddListener(OnGuessButtonClick);
}
}
public void SetContent(MusicInfo info)
{
if (info == null)
{
HideAll();
return;
}
// 猜歌模式下切歌:必须先把MusicInfo立即打成不可见(无动画),再写入新内容,
// 否则FadeOut动画是异步的,这一帧CanvasGroup.alpha还是1,下面写入的Title/Composer等
// 会以alpha=1渲染一帧才被GuessArea盖住,把答案露给玩家
if (_isGuessMode)
{
SnapMusicInfoHidden();
}
// 切歌时:猜歌模式下默认 MusicInfo 隐藏 + GuessArea 显示;非猜歌模式相反
ApplyGuessAreaVisibility(_isGuessMode);
// 碟片区:常驻显示(只要面板显示)
if (DiscRoot != null) DiscRoot.SetActive(true);
// MusicData 自身字段:走多语言ID
SetMultilingualField(TitleRoot, TitleText, info.Title);
// 原曲名:Multilingual表里所有语言列填的就是日文原文,正常走多语言系统即可
SetMultilingualField(OriginalTitleRoot, OriginalTitleText, info.OriginalTitle);
SetMultilingualField(ComposerRoot, ComposerText, info.Composer);
SetMultilingualField(ArrangerRoot, ArrangerText, info.Arranger);
SetMultilingualField(MixerRoot, MixerText, info.Mixer);
SetMultilingualField(VocalistRoot, VocalistText, info.Vocalist);
SetMultilingualField(IllustratorRoot, IllustratorText, info.Illustrator);
// 阵营文字:Force == Common 视为缺省;否则尝试查 PlayerInfo 拿领袖名,失败回退枚举名
SetForceField(info.Force, info.Civ);
// 文明文字:Civ == Common 视为缺省;否则查 CivDataAssets,失败回退枚举名
SetCivField(info.Civ);
// 封面:cover-fit(保持比例撑满,需父级有Mask/RectMask2D裁切溢出)
SetCoverSpriteField(CoverRoot, CoverImage, info.CoverSprite);
// 背景:同样cover-fit
SetCoverSpriteField(BackgroundRoot, BackgroundImage, info.BackgroundSprite);
RebuildMusicInfoLayouts();
}
// 强制刷新MusicInfo相关的所有Layout,顺序非常关键:
// 1) Civ/Force两个Tag自己(内含ContentSizeFitter,文字宽度变了要先 rebuild 自己)
// 2) Tag共享的父Layout(HorizontalLayoutGroup,需要重排子项)
// 3) Title共享的父Layout(标题/原曲等容器)
// 4) 整个MusicPanel(向上层层 rebuild)
// 切歌时(SetContent末尾)和MusicInfo淡入显示时都要调用,否则文字宽度/位置错乱
private void RebuildMusicInfoLayouts()
{
RebuildIfActive(CivRoot);
RebuildIfActive(ForceRoot);
if (TagLayoutRoot != null) LayoutRebuilder.ForceRebuildLayoutImmediate(TagLayoutRoot);
if (TitleLayoutRoot != null) LayoutRebuilder.ForceRebuildLayoutImmediate(TitleLayoutRoot);
var rt = transform as RectTransform;
if (rt != null) LayoutRebuilder.ForceRebuildLayoutImmediate(rt);
}
/// <summary>
/// 立刻播放该曲(并刷新显示),鉴赏室里循环播放
/// </summary>
public void PlayMusic(MusicInfo info)
{
if (info == null || string.IsNullOrEmpty(info.MusicName)) return;
// 鉴赏室循环播放,不接续上次进度,1秒淡入/2秒淡出
AudioManager.Instance.PlayMusic(info.MusicName, 1f, 2f, true, false);
_currentPlayingMusicName = info.MusicName;
}
/// <summary>
/// 离开Music sheet时调用:停掉当前在播曲目并清旋转标记(不走Hide动画)
/// </summary>
public void OnLeaveSheet()
{
if (!string.IsNullOrEmpty(_currentPlayingMusicName))
{
AudioManager.Instance.StopMusic();
_currentPlayingMusicName = null;
}
}
/// <summary>
/// 切换猜歌模式
/// 进入猜歌:MusicInfo隐藏 / GuessArea显示
/// 退出猜歌:MusicInfo显示 / GuessArea隐藏
/// </summary>
public void SetGuessMode(bool guess)
{
_isGuessMode = guess;
ApplyGuessAreaVisibility(guess);
}
// GuessButton 点击:揭晓答案,GuessArea 隐藏 + MusicInfo 显示
// (保持 _isGuessMode=true,只是当前这一首被揭晓了)
private void OnGuessButtonClick()
{
ApplyGuessAreaVisibility(false);
}
// showGuess=true: GuessArea淡入显示, MusicInfo淡出隐藏
// showGuess=false: GuessArea淡出隐藏, MusicInfo淡入显示
private void ApplyGuessAreaVisibility(bool showGuess)
{
if (showGuess)
{
FadeOut(MusicInfoRoot, MusicInfoAnimancer);
FadeIn(GuessArea, GuessAreaAnimancer);
}
else
{
FadeOut(GuessArea, GuessAreaAnimancer);
FadeIn(MusicInfoRoot, MusicInfoAnimancer);
// MusicInfo 重新激活后,layout需要立刻rebuild
// (隐藏期间设置的Tag/Title内容没参与过排版,直接显示会错位)
RebuildMusicInfoLayouts();
}
}
// 立即把MusicInfoRoot打成完全不可见(无动画):停Animancer + CanvasGroup.alpha=0 + SetActive(false)
// 用途:猜歌模式切歌时,必须在写入新内容前彻底切断MusicInfo渲染,避免FadeOut动画期间露出新答案
private void SnapMusicInfoHidden()
{
if (MusicInfoRoot == null) return;
// 停掉可能正在播的FadeIn/FadeOut,避免动画继续把alpha往上拉或触发已过期的OnEnd回调
if (MusicInfoAnimancer != null) MusicInfoAnimancer.Stop();
var cg = MusicInfoRoot.GetComponent<CanvasGroup>();
if (cg != null) cg.alpha = 0f;
MusicInfoRoot.SetActive(false);
}
// 标准淡入:激活GO并播放FadeIn动画
private static void FadeIn(GameObject go, AnimancerComponent animancer)
{
if (go == null) return;
go.SetActive(true);
if (animancer == null)
{
Debug.LogWarning($"[MusicPanel] FadeIn 跳过: '{go.name}' 没有拖 AnimancerComponent 字段");
return;
}
if (animancer.Animator == null)
{
Debug.LogWarning($"[MusicPanel] FadeIn 跳过: '{go.name}' 上的 AnimancerComponent 没有 Animator 组件(必须挂Animator)");
return;
}
var clip = ResourceCache.Instance?.AnimCache?.UICommonPanelFadeIn;
if (clip == null)
{
Debug.LogWarning($"[MusicPanel] FadeIn 跳过: AnimCache.UICommonPanelFadeIn 为空");
return;
}
animancer.Play(clip);
}
// 标准淡出:播放FadeOut动画,结束后SetActive(false);若Animancer不可用则直接Active(false)
private static void FadeOut(GameObject go, AnimancerComponent animancer)
{
if (go == null) return;
if (!go.activeSelf) return;
if (animancer == null)
{
Debug.LogWarning($"[MusicPanel] FadeOut 跳过动画: '{go.name}' 没有拖 AnimancerComponent 字段");
go.SetActive(false);
return;
}
if (animancer.Animator == null)
{
Debug.LogWarning($"[MusicPanel] FadeOut 跳过动画: '{go.name}' 上的 AnimancerComponent 没有 Animator 组件");
go.SetActive(false);
return;
}
var clip = ResourceCache.Instance?.AnimCache?.UICommonPanelFadeOut;
if (clip == null)
{
Debug.LogWarning($"[MusicPanel] FadeOut 跳过动画: AnimCache.UICommonPanelFadeOut 为空");
go.SetActive(false);
return;
}
var state = animancer.Play(clip);
state.Events.OnEnd = () => { if (go != null) go.SetActive(false); };
}
// 碟片每帧旋转,只在当前曲目正在播放时转
private void Update()
{
if (string.IsNullOrEmpty(_currentPlayingMusicName)) return;
// DiscRotator 优先;没拖时退化为 DiscRoot 的 RectTransform
var rotator = DiscRotator;
if (rotator == null && DiscRoot != null) rotator = DiscRoot.transform as RectTransform;
if (rotator == null) return;
rotator.Rotate(0f, 0f, DiscRotateSpeed * Time.deltaTime);
}
// ---- 私有辅助 ----
private void HideAll()
{
if (DiscRoot != null) DiscRoot.SetActive(false);
if (TitleRoot != null) TitleRoot.SetActive(false);
if (OriginalTitleRoot != null) OriginalTitleRoot.SetActive(false);
if (ComposerRoot != null) ComposerRoot.SetActive(false);
if (ArrangerRoot != null) ArrangerRoot.SetActive(false);
if (MixerRoot != null) MixerRoot.SetActive(false);
if (VocalistRoot != null) VocalistRoot.SetActive(false);
if (ForceRoot != null) ForceRoot.SetActive(false);
if (CivRoot != null) CivRoot.SetActive(false);
if (IllustratorRoot != null) IllustratorRoot.SetActive(false);
if (CoverRoot != null) CoverRoot.SetActive(false);
if (BackgroundRoot != null) BackgroundRoot.SetActive(false);
}
// MusicData 自身的多语言字段:走 SetUIText(传入的是数字ID字符串)
private static void SetMultilingualField(GameObject root, TextMeshProUGUI text, string value)
{
bool has = !string.IsNullOrEmpty(value);
if (root != null) root.SetActive(has);
if (has && text != null) MultilingualManager.Instance.SetUIText(text, value);
}
// 仅当对象 active 时,强制刷新它的 RectTransform 布局
private static void RebuildIfActive(GameObject root)
{
if (root == null || !root.activeInHierarchy) return;
var rt = root.transform as RectTransform;
if (rt != null) LayoutRebuilder.ForceRebuildLayoutImmediate(rt);
}
private static void SetSpriteField(GameObject root, Image image, Sprite sprite)
{
bool has = sprite != null;
if (root != null) root.SetActive(has);
if (has && image != null) image.sprite = sprite;
}
// cover-fit 模式:保持比例撑满父容器,溢出部分由父级 Mask/RectMask2D 裁切
private static void SetCoverSpriteField(GameObject root, Image image, Sprite sprite)
{
bool has = sprite != null;
if (root != null) root.SetActive(has);
if (!has || image == null) return;
image.sprite = sprite;
// prefab上原本m_PreserveAspect=1(等比缩小,会留白),cover-fit需要关闭让Image铺满整个RectTransform
image.preserveAspect = false;
FitCover(image);
}
// 把Image的RectTransform改成: anchor=中心,pivot=中心,absolute sizeDelta=目标像素尺寸
// 目标尺寸 = sprite按比例放大到"短边匹配父容器"的尺寸,长边溢出由父级Mask裁切
private static void FitCover(Image image)
{
var rt = image.rectTransform;
var parent = rt.parent as RectTransform;
if (parent == null || image.sprite == null) return;
var parentRect = parent.rect;
float pw = parentRect.width;
float ph = parentRect.height;
if (pw <= 0 || ph <= 0) return;
var spr = image.sprite.rect;
float sw = spr.width;
float sh = spr.height;
if (sw <= 0 || sh <= 0) return;
rt.anchorMin = new Vector2(0.5f, 0.5f);
rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.pivot = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = Vector2.zero;
// 短边贴父容器,长边溢出
float scale = Mathf.Max(pw / sw, ph / sh);
rt.sizeDelta = new Vector2(sw * scale, sh * scale);
}
private void SetForceField(ForceEnum force, CivEnum civ)
{
bool show = force != ForceEnum.Common;
if (ForceRoot != null) ForceRoot.SetActive(show);
if (!show || ForceText == null) return;
// 按 (Civ, Force) 查 PlayerInfo,取 ForceName(帝国名,多语言ID)
if (Table.Instance.PlayerDataAssets != null
&& Table.Instance.PlayerDataAssets.GetPlayerInfo(civ, force, out var playerInfo)
&& !string.IsNullOrEmpty(playerInfo.ForceName))
{
MultilingualManager.Instance.SetUIText(ForceText, playerInfo.ForceName);
return;
}
// 回退:显示英文枚举名
ForceText.text = force.ToString();
}
private void SetCivField(CivEnum civ)
{
bool show = civ != CivEnum.Common;
if (CivRoot != null) CivRoot.SetActive(show);
if (!show || CivText == null) return;
// 通过 CivDataAssets 取多语言 CivName
if (Table.Instance.CivDataAssets != null
&& Table.Instance.CivDataAssets.GetCivInfo(civ, out var civInfo)
&& !string.IsNullOrEmpty(civInfo.CivName))
{
MultilingualManager.Instance.SetUIText(CivText, civInfo.CivName);
return;
}
// 回退:显示英文枚举名
CivText.text = civ.ToString();
}
}
}