feat: SongSelect UI polish — marquee title, button states, font
- MarqueeText: scrolling title for long song names (RectMask2D clipped) - SongDetailPanel: difficulty selected = green (img.color direct set) - SongSelectManager: ALL/OWNED tab active state via img.color - Card layout: DestroyImmediate + correct rebuild order to fix zero-size title bug - NanumGothic SDF fallback configured on LiberationSans for Korean support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
[RequireComponent(typeof(TMP_Text))]
|
||||||
|
public class MarqueeText : MonoBehaviour
|
||||||
|
{
|
||||||
|
public float speed = 35f;
|
||||||
|
public float pauseStart = 1.5f;
|
||||||
|
public float pauseEnd = 0.6f;
|
||||||
|
|
||||||
|
private TMP_Text _label;
|
||||||
|
private RectTransform _rect;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_label = GetComponent<TMP_Text>();
|
||||||
|
_rect = GetComponent<RectTransform>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator Start()
|
||||||
|
{
|
||||||
|
yield return null; // layout 완료 후 실행
|
||||||
|
|
||||||
|
_label.ForceMeshUpdate();
|
||||||
|
float textW = _label.preferredWidth;
|
||||||
|
float containerW = ((RectTransform)transform.parent).rect.width;
|
||||||
|
float dist = textW - containerW;
|
||||||
|
|
||||||
|
if (dist > 1f)
|
||||||
|
StartCoroutine(ScrollLoop(dist));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ScrollLoop(float dist)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
SetX(0f);
|
||||||
|
yield return new WaitForSeconds(pauseStart);
|
||||||
|
|
||||||
|
float x = 0f;
|
||||||
|
while (x > -dist)
|
||||||
|
{
|
||||||
|
x = Mathf.MoveTowards(x, -dist, speed * Time.deltaTime);
|
||||||
|
SetX(x);
|
||||||
|
yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(pauseEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetX(float x) =>
|
||||||
|
_rect.anchoredPosition = new Vector2(x, _rect.anchoredPosition.y);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: aec41476b82385047a8cec63612a6698
|
||||||
@@ -33,8 +33,8 @@ public class SongDetailPanel : MonoBehaviour
|
|||||||
[Header("씬 이름")]
|
[Header("씬 이름")]
|
||||||
[SerializeField] private string gameSceneName = "Game";
|
[SerializeField] private string gameSceneName = "Game";
|
||||||
|
|
||||||
private static readonly Color SelectedColor = new Color(0.2f, 0.78f, 0.4f);
|
private static readonly Color SelectedColor = new Color(0.2f, 0.78f, 0.4f);
|
||||||
private static readonly Color DeselectedColor = Color.white;
|
private static readonly Color DeselectedImgColor = new Color(1f, 1f, 1f, 0.12f); // original button alpha
|
||||||
|
|
||||||
private SongInfo currentSong;
|
private SongInfo currentSong;
|
||||||
private string selectedDifficulty;
|
private string selectedDifficulty;
|
||||||
@@ -122,10 +122,17 @@ public class SongDetailPanel : MonoBehaviour
|
|||||||
{
|
{
|
||||||
foreach (var (key, getBtn) in diffSlots)
|
foreach (var (key, getBtn) in diffSlots)
|
||||||
{
|
{
|
||||||
Button btn = getBtn(this);
|
Button btn = getBtn(this);
|
||||||
var colors = btn.colors;
|
bool selected = key == selectedDifficulty;
|
||||||
colors.normalColor = (key == selectedDifficulty) ? SelectedColor : DeselectedColor;
|
|
||||||
btn.colors = colors;
|
if (btn.targetGraphic is Image img)
|
||||||
|
img.color = selected ? SelectedColor : DeselectedImgColor;
|
||||||
|
|
||||||
|
var cb = btn.colors;
|
||||||
|
cb.normalColor = Color.white;
|
||||||
|
cb.highlightedColor = selected ? new Color(0.3f, 0.95f, 0.55f) : new Color(1f, 1f, 1f, 0.25f);
|
||||||
|
cb.pressedColor = selected ? new Color(0.15f, 0.6f, 0.3f) : new Color(1f, 1f, 1f, 0.35f);
|
||||||
|
btn.colors = cb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,25 @@ public class SongSelectManager : MonoBehaviour
|
|||||||
[SerializeField] private GameObject loadingOverlay;
|
[SerializeField] private GameObject loadingOverlay;
|
||||||
[SerializeField] private GameObject errorOverlay;
|
[SerializeField] private GameObject errorOverlay;
|
||||||
[SerializeField] private TMP_Text errorText;
|
[SerializeField] private TMP_Text errorText;
|
||||||
[SerializeField] private TMP_FontAsset cardFont;
|
|
||||||
|
|
||||||
private static readonly Color TabActive = Color.white;
|
|
||||||
private static readonly Color TabInactive = new Color(0.6f, 0.6f, 0.6f);
|
private static readonly Color TabActive = new Color(1f, 1f, 1f, 0.45f);
|
||||||
|
private static readonly Color TabInactive = new Color(1f, 1f, 1f, 0.12f);
|
||||||
|
|
||||||
private static string CachePath =>
|
private static string CachePath =>
|
||||||
Path.Combine(Application.persistentDataPath, "songs_cache.json");
|
Path.Combine(Application.persistentDataPath, "songs_cache.json");
|
||||||
|
|
||||||
private List<SongInfo> allSongs = new List<SongInfo>();
|
private List<SongInfo> allSongs = new List<SongInfo>();
|
||||||
private bool showingOwned = false;
|
private bool showingOwned = false;
|
||||||
|
private TMP_FontAsset _cardFont;
|
||||||
|
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
|
// NanumGothic SDF를 직접 로드 — Resources 경로에 있어야 함
|
||||||
|
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/NanumGothic SDF");
|
||||||
|
if (_cardFont == null)
|
||||||
|
_cardFont = Resources.Load<TMP_FontAsset>("Fonts & Materials/LiberationSans SDF");
|
||||||
|
|
||||||
tabAllBtn .onClick.AddListener(() => SwitchTab(false));
|
tabAllBtn .onClick.AddListener(() => SwitchTab(false));
|
||||||
tabOwnedBtn.onClick.AddListener(() => SwitchTab(true));
|
tabOwnedBtn.onClick.AddListener(() => SwitchTab(true));
|
||||||
detailPanel.gameObject.SetActive(false);
|
detailPanel.gameObject.SetActive(false);
|
||||||
@@ -49,9 +55,11 @@ public class SongSelectManager : MonoBehaviour
|
|||||||
|
|
||||||
private static void ApplyTabColor(Button btn, Color c)
|
private static void ApplyTabColor(Button btn, Color c)
|
||||||
{
|
{
|
||||||
var colors = btn.colors;
|
if (btn.targetGraphic is Image img)
|
||||||
colors.normalColor = c;
|
img.color = c;
|
||||||
btn.colors = colors;
|
var colors = btn.colors;
|
||||||
|
colors.normalColor = Color.white;
|
||||||
|
btn.colors = colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FetchSongs()
|
private void FetchSongs()
|
||||||
@@ -87,8 +95,9 @@ public class SongSelectManager : MonoBehaviour
|
|||||||
|
|
||||||
public void RefreshCards()
|
public void RefreshCards()
|
||||||
{
|
{
|
||||||
|
// DestroyImmediate to avoid deferred-destroy interfering with layout
|
||||||
for (int i = cardContainer.childCount - 1; i >= 0; i--)
|
for (int i = cardContainer.childCount - 1; i >= 0; i--)
|
||||||
Destroy(cardContainer.GetChild(i).gameObject);
|
DestroyImmediate(cardContainer.GetChild(i).gameObject);
|
||||||
|
|
||||||
List<SongInfo> songs = showingOwned
|
List<SongInfo> songs = showingOwned
|
||||||
? allSongs.FindAll(s => SongLibrary.Instance.IsSongDownloaded(s.id))
|
? allSongs.FindAll(s => SongLibrary.Instance.IsSongDownloaded(s.id))
|
||||||
@@ -96,6 +105,10 @@ public class SongSelectManager : MonoBehaviour
|
|||||||
|
|
||||||
foreach (SongInfo song in songs)
|
foreach (SongInfo song in songs)
|
||||||
SpawnCard(song);
|
SpawnCard(song);
|
||||||
|
|
||||||
|
// Order matters: layout first → card gets size → then canvas update → anchored children recalculate
|
||||||
|
LayoutRebuilder.ForceRebuildLayoutImmediate(cardContainer);
|
||||||
|
Canvas.ForceUpdateCanvases();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SpawnCard(SongInfo song)
|
private void SpawnCard(SongInfo song)
|
||||||
@@ -121,21 +134,33 @@ public class SongSelectManager : MonoBehaviour
|
|||||||
bc.fadeDuration = 0.1f;
|
bc.fadeDuration = 0.1f;
|
||||||
btn.colors = bc;
|
btn.colors = bc;
|
||||||
|
|
||||||
// Title
|
// Title — RectMask2D 컨테이너 안에서 마퀴 스크롤
|
||||||
|
var titleMask = new GameObject("TitleMask");
|
||||||
|
titleMask.transform.SetParent(card.transform, false);
|
||||||
|
var tmr = titleMask.AddComponent<RectTransform>();
|
||||||
|
tmr.anchorMin = new Vector2(0f, 0.5f);
|
||||||
|
tmr.anchorMax = new Vector2(1f, 1f);
|
||||||
|
tmr.offsetMin = new Vector2(5f, 0f);
|
||||||
|
tmr.offsetMax = new Vector2(downloaded ? -20f : -3f, 0f);
|
||||||
|
titleMask.AddComponent<RectMask2D>();
|
||||||
|
|
||||||
var titleGO = new GameObject("Title");
|
var titleGO = new GameObject("Title");
|
||||||
titleGO.transform.SetParent(card.transform, false);
|
titleGO.transform.SetParent(titleMask.transform, false);
|
||||||
var tr = titleGO.AddComponent<RectTransform>();
|
var tr = titleGO.AddComponent<RectTransform>();
|
||||||
tr.anchorMin = new Vector2(0f, 0.5f);
|
tr.anchorMin = new Vector2(0f, 0f);
|
||||||
tr.anchorMax = new Vector2(1f, 1f);
|
tr.anchorMax = new Vector2(0f, 1f);
|
||||||
tr.offsetMin = new Vector2(5f, 0f);
|
tr.pivot = new Vector2(0f, 0.5f);
|
||||||
tr.offsetMax = new Vector2(downloaded ? -18f : -3f, -1f);
|
tr.anchoredPosition = Vector2.zero;
|
||||||
|
tr.sizeDelta = new Vector2(500f, 0f);
|
||||||
var tTmp = titleGO.AddComponent<TextMeshProUGUI>();
|
var tTmp = titleGO.AddComponent<TextMeshProUGUI>();
|
||||||
if (cardFont != null) tTmp.font = cardFont;
|
if (_cardFont != null) tTmp.font = _cardFont;
|
||||||
tTmp.text = song.title;
|
tTmp.text = song.title;
|
||||||
tTmp.fontSize = 5f;
|
tTmp.fontSize = 5f;
|
||||||
tTmp.color = Color.white;
|
tTmp.color = Color.white;
|
||||||
tTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
tTmp.alignment = TextAlignmentOptions.MidlineLeft;
|
||||||
tTmp.overflowMode = TextOverflowModes.Ellipsis;
|
tTmp.overflowMode = TextOverflowModes.Overflow;
|
||||||
|
tTmp.enableWordWrapping = false;
|
||||||
|
titleGO.AddComponent<MarqueeText>();
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
var artistGO = new GameObject("Artist");
|
var artistGO = new GameObject("Artist");
|
||||||
@@ -146,7 +171,7 @@ public class SongSelectManager : MonoBehaviour
|
|||||||
ar.offsetMin = new Vector2(5f, 1f);
|
ar.offsetMin = new Vector2(5f, 1f);
|
||||||
ar.offsetMax = new Vector2(-3f, 0f);
|
ar.offsetMax = new Vector2(-3f, 0f);
|
||||||
var aTmp = artistGO.AddComponent<TextMeshProUGUI>();
|
var aTmp = artistGO.AddComponent<TextMeshProUGUI>();
|
||||||
if (cardFont != null) aTmp.font = cardFont;
|
if (_cardFont != null) aTmp.font = _cardFont;
|
||||||
aTmp.text = song.artist;
|
aTmp.text = song.artist;
|
||||||
aTmp.fontSize = 4f;
|
aTmp.fontSize = 4f;
|
||||||
aTmp.color = new Color(1f, 1f, 1f, 0.6f);
|
aTmp.color = new Color(1f, 1f, 1f, 0.6f);
|
||||||
|
|||||||
@@ -257917,7 +257917,8 @@ MonoBehaviour:
|
|||||||
m_MarkToBaseAdjustmentRecords: []
|
m_MarkToBaseAdjustmentRecords: []
|
||||||
m_MarkToMarkAdjustmentRecords: []
|
m_MarkToMarkAdjustmentRecords: []
|
||||||
m_ShouldReimportFontFeatures: 0
|
m_ShouldReimportFontFeatures: 0
|
||||||
m_FallbackFontAssetTable: []
|
m_FallbackFontAssetTable:
|
||||||
|
- {fileID: 11400000}
|
||||||
m_FontWeightTable:
|
m_FontWeightTable:
|
||||||
- regularTypeface: {fileID: 0}
|
- regularTypeface: {fileID: 0}
|
||||||
italicTypeface: {fileID: 0}
|
italicTypeface: {fileID: 0}
|
||||||
@@ -258046,9 +258047,9 @@ Material:
|
|||||||
- _OutlineWidth: 0
|
- _OutlineWidth: 0
|
||||||
- _PerspectiveFilter: 0.875
|
- _PerspectiveFilter: 0.875
|
||||||
- _Reflectivity: 10
|
- _Reflectivity: 10
|
||||||
- _ScaleRatioA: 1
|
- _ScaleRatioA: 0
|
||||||
- _ScaleRatioB: 1
|
- _ScaleRatioB: 0
|
||||||
- _ScaleRatioC: 1
|
- _ScaleRatioC: 0
|
||||||
- _ScaleX: 1
|
- _ScaleX: 1
|
||||||
- _ScaleY: 1
|
- _ScaleY: 1
|
||||||
- _ShaderFlags: 0
|
- _ShaderFlags: 0
|
||||||
|
|||||||
+9
-36
File diff suppressed because one or more lines are too long
+3347
-3347
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user