UI 수정, 토글 기본값, Spawner 오디오 버그 수정

[SceneBuilder]
- 사용자 수동 조정 위치 반영 (AudioDropdown, RefreshBtn, FilePickerBtn, UrlInput, UrlDownloadBtn, 난이도 토글 4개, BackButton)
- MakeTMP 텍스트 정렬: MidlineLeft → Center
- MakePanel/Button/InputField/Dropdown: sprite = null 설정으로 흰색 배경 제거
- 버튼 텍스트 특수문자 → ASCII 대체 (→ > / ▼▶✕ 제거): NanumGothic SDF 미지원 문자 경고 해결
- 난이도 토글 4개 기본값 모두 true로 변경

[SongCreatorManager]
- Start()에서 토글 전체 꺼진 경우 자동으로 4개 모두 켜기 (씬 재빌드 없이 즉시 적용)

[Spawner]
- Awake(): playOnAwake = false, clip = null 설정 — Inspector 하드코딩 클립 자동 재생 차단
- InitGame(): AudioSource/GameSession null 체크, 파일 존재 확인, 경로 로그 추가
- audioSource.clip == null 후 yield break로 로드 실패 시 재생 방지

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 01:05:04 +09:00
parent c73ff7f412
commit 16bc037093
3 changed files with 456 additions and 47 deletions
+138 -5
View File
@@ -2,8 +2,10 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using TMPro;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
@@ -12,7 +14,15 @@ public class SongCreatorManager : MonoBehaviour
[Header("음원 선택")]
[SerializeField] private TMP_Dropdown audioDropdown;
[SerializeField] private Button refreshBtn;
[SerializeField] private TMP_Text inputPathHint; // 파일 넣는 경로 안내
[SerializeField] private TMP_Text inputPathHint;
[Header("음원 추가 — 로컬 파일")]
[SerializeField] private Button filePickerBtn; // 파일 탐색 버튼
[SerializeField] private TMP_Text addStatusText; // 추가/다운로드 상태
[Header("음원 추가 — URL")]
[SerializeField] private TMP_InputField urlInput; // MP3 직접 URL
[SerializeField] private Button urlDownloadBtn; // URL 다운로드 시작
[Header("메타데이터")]
[SerializeField] private TMP_InputField titleInput;
@@ -48,6 +58,7 @@ public class SongCreatorManager : MonoBehaviour
Path.Combine(Application.persistentDataPath, "input");
private readonly List<string> audioFiles = new();
private string _pendingFilePath; // 파일 다이얼로그 결과 (백그라운드 스레드 → 메인 스레드 전달)
// ── Unity ────────────────────────────────────────────────
@@ -58,15 +69,40 @@ public class SongCreatorManager : MonoBehaviour
if (inputPathHint != null)
inputPathHint.text = $"음원 경로: {InputPath}";
// 씬에서 아무 토글도 안 선택되어 있으면 전부 켜기
bool anyOn = (toggleNormal != null && toggleNormal.isOn)
|| (toggleHard != null && toggleHard.isOn)
|| (toggleExpert != null && toggleExpert.isOn)
|| (toggleExpertPlus != null && toggleExpertPlus.isOn);
if (!anyOn)
{
if (toggleNormal != null) toggleNormal.isOn = true;
if (toggleHard != null) toggleHard.isOn = true;
if (toggleExpert != null) toggleExpert.isOn = true;
if (toggleExpertPlus != null) toggleExpertPlus.isOn = true;
}
refreshBtn.onClick.AddListener(RefreshAudioList);
generateButton.onClick.AddListener(OnGenerateClicked);
manualEditorButton.onClick.AddListener(() => SceneManager.LoadScene(mapEditorScene));
backButton?.onClick.AddListener(() => SceneManager.LoadScene(introSceneName));
if (filePickerBtn != null) filePickerBtn.onClick.AddListener(OnFilePickerClicked);
if (urlDownloadBtn != null) urlDownloadBtn.onClick.AddListener(OnUrlDownloadClicked);
progressGroup.SetActive(false);
RefreshAudioList();
}
private void Update()
{
// 파일 다이얼로그는 STA 스레드에서 실행되므로 결과를 메인 스레드에서 처리
if (_pendingFilePath != null)
{
CopyToInput(_pendingFilePath);
_pendingFilePath = null;
}
}
// ── 음원 목록 갱신 ───────────────────────────────────────
private void RefreshAudioList()
@@ -99,10 +135,10 @@ public class SongCreatorManager : MonoBehaviour
{ SetStatus("BPM을 올바르게 입력해주세요."); return; }
var diffs = new List<string>();
if (toggleNormal.isOn) diffs.Add("normal");
if (toggleHard.isOn) diffs.Add("hard");
if (toggleExpert.isOn) diffs.Add("expert");
if (toggleExpertPlus.isOn) diffs.Add("expertplus");
if (toggleNormal != null && toggleNormal.isOn) diffs.Add("normal");
if (toggleHard != null && toggleHard.isOn) diffs.Add("hard");
if (toggleExpert != null && toggleExpert.isOn) diffs.Add("expert");
if (toggleExpertPlus != null && toggleExpertPlus.isOn) diffs.Add("expertplus");
if (diffs.Count == 0) { SetStatus("난이도를 하나 이상 선택해주세요."); return; }
@@ -218,5 +254,102 @@ public class SongCreatorManager : MonoBehaviour
manualEditorButton.interactable = value;
audioDropdown.interactable = value;
refreshBtn.interactable = value;
if (filePickerBtn != null) filePickerBtn.interactable = value;
if (urlDownloadBtn != null) urlDownloadBtn.interactable = value;
}
// ── 로컬 파일 선택 ────────────────────────────────────────
private void OnFilePickerClicked()
{
#if UNITY_EDITOR
string path = UnityEditor.EditorUtility.OpenFilePanel("음원 파일 선택", "", "mp3");
if (!string.IsNullOrEmpty(path)) CopyToInput(path);
#elif UNITY_STANDALONE_WIN
var t = new Thread(() =>
{
var dlg = new System.Windows.Forms.OpenFileDialog
{
Filter = "MP3 파일|*.mp3",
Title = "음원 파일 선택"
};
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
_pendingFilePath = dlg.FileName;
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
#else
SetAddStatus($"ADB로 파일을 추가하세요:\n{InputPath}");
#endif
}
private void CopyToInput(string srcPath)
{
try
{
string dest = Path.Combine(InputPath, Path.GetFileName(srcPath));
File.Copy(srcPath, dest, overwrite: true);
RefreshAudioList();
string nameNoExt = Path.GetFileNameWithoutExtension(srcPath);
int idx = audioFiles.FindIndex(f => Path.GetFileNameWithoutExtension(f) == nameNoExt);
if (idx >= 0) audioDropdown.value = idx;
SetAddStatus($"추가됨: {Path.GetFileName(srcPath)}");
}
catch (Exception e)
{
SetAddStatus($"파일 추가 실패: {e.Message}");
}
}
// ── URL 다운로드 ──────────────────────────────────────────
private void OnUrlDownloadClicked()
{
string url = urlInput != null ? urlInput.text.Trim() : "";
if (string.IsNullOrEmpty(url)) { SetAddStatus("URL을 입력해주세요."); return; }
StartCoroutine(DownloadFromUrl(url));
}
private IEnumerator DownloadFromUrl(string url)
{
SetAddStatus("다운로드 중...");
if (urlDownloadBtn != null) urlDownloadBtn.interactable = false;
string fileName;
try
{
string uriPath = new Uri(url).AbsolutePath;
fileName = Path.GetFileName(uriPath);
if (string.IsNullOrEmpty(fileName) || !fileName.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase))
fileName = "download.mp3";
}
catch { fileName = "download.mp3"; }
string savePath = Path.GetFullPath(Path.Combine(InputPath, fileName));
using var req = UnityWebRequest.Get(url);
req.downloadHandler = new DownloadHandlerFile(savePath);
yield return req.SendWebRequest();
if (urlDownloadBtn != null) urlDownloadBtn.interactable = true;
if (req.result == UnityWebRequest.Result.Success)
{
RefreshAudioList();
string nameNoExt = Path.GetFileNameWithoutExtension(fileName);
int idx = audioFiles.FindIndex(f => Path.GetFileNameWithoutExtension(f) == nameNoExt);
if (idx >= 0) audioDropdown.value = idx;
SetAddStatus($"다운로드 완료: {fileName}");
}
else
{
if (File.Exists(savePath)) File.Delete(savePath);
SetAddStatus($"다운로드 실패: {req.error}");
}
}
private void SetAddStatus(string msg)
{
if (addStatusText != null) addStatusText.text = msg;
}
}
+66 -6
View File
@@ -2,6 +2,7 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
@@ -21,6 +22,10 @@ public class Spawner : MonoBehaviour
[Header("씬 설정")]
public string songSelectSceneName = "SongSelect";
[Header("뒤로가기 입력")]
[Tooltip("Quest B/Y 버튼을 뒤로가기로 쓸 InputAction (선택)")]
[SerializeField] private InputActionReference backAction;
private List<NoteData> mapNotes = new List<NoteData>();
private int nextNoteIndex = 0;
private float travelTime;
@@ -28,6 +33,16 @@ public class Spawner : MonoBehaviour
// ── Unity ────────────────────────────────────────────────
void Awake()
{
// PlayOnAwake가 OnEnable에서 발동하기 전에 차단
if (audioSource != null)
{
audioSource.playOnAwake = false;
audioSource.clip = null;
}
}
void Start()
{
travelTime = distanceToHit / noteSpeed;
@@ -44,6 +59,10 @@ public class Spawner : MonoBehaviour
void Update()
{
// 뒤로가기: Quest B/Y 버튼 (PC ESC는 DesktopUIMode가 처리)
bool goBack = backAction != null && backAction.action.WasPressedThisFrame();
if (goBack) { SceneManager.LoadScene(songSelectSceneName); return; }
if (!isReady || audioSource == null || !audioSource.isPlaying) return;
float currentTime = audioSource.time;
@@ -62,25 +81,66 @@ public class Spawner : MonoBehaviour
SongInfo song = GameSession.SelectedSong;
string difficulty = GameSession.SelectedDifficulty;
// 맵 JSON 로드
// ── 기본 연결 확인 ───────────────────────────────────────
if (audioSource == null)
{
Debug.LogError("[Spawner] AudioSource 미연결 — Inspector에서 AudioSource 연결 필요");
yield break;
}
if (song == null)
{
Debug.LogError("[Spawner] GameSession.SelectedSong == null — SongSelect에서 곡을 선택하고 Play를 눌러야 합니다");
yield break;
}
if (string.IsNullOrEmpty(difficulty))
{
Debug.LogError("[Spawner] GameSession.SelectedDifficulty 비어있음");
yield break;
}
Debug.Log($"[Spawner] 시작: song={song.title}({song.id}), diff={difficulty}");
// ── 맵 JSON 로드 ─────────────────────────────────────────
string mapPath = GetMapPath(song, difficulty);
Debug.Log($"[Spawner] 맵 경로: {mapPath}");
if (string.IsNullOrEmpty(mapPath))
{
Debug.LogError($"[Spawner] songs.json에 '{difficulty}' mapFile 없음 — NAS 재업로드 필요");
yield break;
}
if (!File.Exists(mapPath))
{
Debug.LogError($"[Spawner] 맵 파일 없음: {mapPath}");
Debug.LogError($"[Spawner] 맵 파일 없음: {mapPath}\n→ SongSelect에서 다운로드하세요");
yield break;
}
LoadMapJson(mapPath);
// 오디오 로드 (로컬 파일 → AudioClip)
// ── 오디오 로드 ──────────────────────────────────────────
audioSource.Stop();
audioSource.clip = null;
string audioPath = GetAudioPath(song.id);
Debug.Log($"[Spawner] 오디오 경로: {audioPath}");
if (!File.Exists(audioPath))
{
Debug.LogError($"[Spawner] 오디오 파일 없음: {audioPath}\n→ SongSelect에서 다운로드하세요");
yield break;
}
yield return LoadAudioClip(audioPath);
// LRU 갱신
SongLibrary.Instance?.TouchSong(song.id);
if (audioSource.clip == null)
{
Debug.LogError("[Spawner] 오디오 클립 로드 실패 (파일 손상 또는 형식 오류)");
yield break;
}
SongLibrary.Instance?.TouchSong(song.id);
isReady = true;
audioSource.Play();
Debug.Log($"[Spawner] 시작: {song.title} ({difficulty})");
Debug.Log($"[Spawner] 재생 시작: {song.title} / {difficulty} / 노트:{mapNotes.Count}개");
}
private void LoadMapJson(string path)