420 lines
17 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|