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 { /// /// 音乐鉴赏室播放区:碟片区 + 各种文字信息 + 封面/背景 /// 每个文字字段需要两个对象:Root(父GO,决定是否显示) + Text(TMP) /// 缺数据/空值时整组Root隐藏 /// 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); } /// /// 立刻播放该曲(并刷新显示),鉴赏室里循环播放 /// 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; } /// /// 离开Music sheet时调用:停掉当前在播曲目并清旋转标记(不走Hide动画) /// public void OnLeaveSheet() { if (!string.IsNullOrEmpty(_currentPlayingMusicName)) { AudioManager.Instance.StopMusic(); _currentPlayingMusicName = null; } } /// /// 切换猜歌模式 /// 进入猜歌:MusicInfo隐藏 / GuessArea显示 /// 退出猜歌:MusicInfo显示 / GuessArea隐藏 /// 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(); 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(); } } }