2026-05-20 16:44:28 +09:00
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using UnityEngine;
|
2026-05-21 01:05:04 +09:00
|
|
|
using UnityEngine.InputSystem;
|
2026-05-20 16:44:28 +09:00
|
|
|
using UnityEngine.Networking;
|
|
|
|
|
using UnityEngine.SceneManagement;
|
|
|
|
|
|
|
|
|
|
public class Spawner : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
[Header("오디오 및 파일 설정")]
|
|
|
|
|
public AudioSource audioSource;
|
|
|
|
|
|
|
|
|
|
[Header("노트 프리팹 & 위치")]
|
|
|
|
|
public GameObject[] cubePrefabs;
|
|
|
|
|
public Transform[] spawnPoints;
|
|
|
|
|
|
|
|
|
|
[Header("타이밍 설정")]
|
|
|
|
|
public float noteSpeed = 2.0f;
|
|
|
|
|
public float distanceToHit = 10.0f;
|
|
|
|
|
|
|
|
|
|
[Header("씬 설정")]
|
|
|
|
|
public string songSelectSceneName = "SongSelect";
|
|
|
|
|
|
2026-05-21 01:05:04 +09:00
|
|
|
[Header("뒤로가기 입력")]
|
|
|
|
|
[Tooltip("Quest B/Y 버튼을 뒤로가기로 쓸 InputAction (선택)")]
|
|
|
|
|
[SerializeField] private InputActionReference backAction;
|
|
|
|
|
|
2026-05-20 16:44:28 +09:00
|
|
|
private List<NoteData> mapNotes = new List<NoteData>();
|
|
|
|
|
private int nextNoteIndex = 0;
|
|
|
|
|
private float travelTime;
|
|
|
|
|
private bool isReady = false;
|
|
|
|
|
|
|
|
|
|
// ── Unity ────────────────────────────────────────────────
|
|
|
|
|
|
2026-05-21 01:05:04 +09:00
|
|
|
void Awake()
|
|
|
|
|
{
|
|
|
|
|
// PlayOnAwake가 OnEnable에서 발동하기 전에 차단
|
|
|
|
|
if (audioSource != null)
|
|
|
|
|
{
|
|
|
|
|
audioSource.playOnAwake = false;
|
|
|
|
|
audioSource.clip = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 16:44:28 +09:00
|
|
|
void Start()
|
|
|
|
|
{
|
|
|
|
|
travelTime = distanceToHit / noteSpeed;
|
|
|
|
|
|
|
|
|
|
if (GameSession.SelectedSong == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogWarning("[Spawner] 선택된 곡 없음 → 곡 선택 화면으로 이동");
|
|
|
|
|
SceneManager.LoadScene(songSelectSceneName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StartCoroutine(InitGame());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Update()
|
|
|
|
|
{
|
2026-05-21 01:05:04 +09:00
|
|
|
// 뒤로가기: Quest B/Y 버튼 (PC ESC는 DesktopUIMode가 처리)
|
|
|
|
|
bool goBack = backAction != null && backAction.action.WasPressedThisFrame();
|
|
|
|
|
if (goBack) { SceneManager.LoadScene(songSelectSceneName); return; }
|
|
|
|
|
|
2026-05-20 16:44:28 +09:00
|
|
|
if (!isReady || audioSource == null || !audioSource.isPlaying) return;
|
|
|
|
|
|
|
|
|
|
float currentTime = audioSource.time;
|
|
|
|
|
while (nextNoteIndex < mapNotes.Count &&
|
|
|
|
|
currentTime + travelTime >= mapNotes[nextNoteIndex].time)
|
|
|
|
|
{
|
|
|
|
|
SpawnNote(mapNotes[nextNoteIndex]);
|
|
|
|
|
nextNoteIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 초기화 ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private IEnumerator InitGame()
|
|
|
|
|
{
|
|
|
|
|
SongInfo song = GameSession.SelectedSong;
|
|
|
|
|
string difficulty = GameSession.SelectedDifficulty;
|
|
|
|
|
|
2026-05-21 01:05:04 +09:00
|
|
|
// ── 기본 연결 확인 ───────────────────────────────────────
|
|
|
|
|
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 로드 ─────────────────────────────────────────
|
2026-05-20 16:44:28 +09:00
|
|
|
string mapPath = GetMapPath(song, difficulty);
|
2026-05-21 01:05:04 +09:00
|
|
|
Debug.Log($"[Spawner] 맵 경로: {mapPath}");
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(mapPath))
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError($"[Spawner] songs.json에 '{difficulty}' mapFile 없음 — NAS 재업로드 필요");
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
2026-05-20 16:44:28 +09:00
|
|
|
if (!File.Exists(mapPath))
|
|
|
|
|
{
|
2026-05-21 01:05:04 +09:00
|
|
|
Debug.LogError($"[Spawner] 맵 파일 없음: {mapPath}\n→ SongSelect에서 다운로드하세요");
|
2026-05-20 16:44:28 +09:00
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
LoadMapJson(mapPath);
|
|
|
|
|
|
2026-05-21 01:05:04 +09:00
|
|
|
// ── 오디오 로드 ──────────────────────────────────────────
|
|
|
|
|
audioSource.Stop();
|
|
|
|
|
audioSource.clip = null;
|
|
|
|
|
|
2026-05-20 16:44:28 +09:00
|
|
|
string audioPath = GetAudioPath(song.id);
|
2026-05-21 01:05:04 +09:00
|
|
|
Debug.Log($"[Spawner] 오디오 경로: {audioPath}");
|
|
|
|
|
|
|
|
|
|
if (!File.Exists(audioPath))
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError($"[Spawner] 오디오 파일 없음: {audioPath}\n→ SongSelect에서 다운로드하세요");
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 16:44:28 +09:00
|
|
|
yield return LoadAudioClip(audioPath);
|
|
|
|
|
|
2026-05-21 01:05:04 +09:00
|
|
|
if (audioSource.clip == null)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError("[Spawner] 오디오 클립 로드 실패 (파일 손상 또는 형식 오류)");
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
2026-05-20 16:44:28 +09:00
|
|
|
|
2026-05-21 01:05:04 +09:00
|
|
|
SongLibrary.Instance?.TouchSong(song.id);
|
2026-05-20 16:44:28 +09:00
|
|
|
isReady = true;
|
|
|
|
|
audioSource.Play();
|
2026-05-21 01:05:04 +09:00
|
|
|
Debug.Log($"[Spawner] 재생 시작: {song.title} / {difficulty} / 노트:{mapNotes.Count}개");
|
2026-05-20 16:44:28 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void LoadMapJson(string path)
|
|
|
|
|
{
|
|
|
|
|
string json = File.ReadAllText(path);
|
|
|
|
|
MapData data = JsonUtility.FromJson<MapData>(json);
|
|
|
|
|
mapNotes = data?.target ?? new List<NoteData>();
|
|
|
|
|
mapNotes.Sort((a, b) => a.time.CompareTo(b.time));
|
|
|
|
|
Debug.Log($"[Spawner] 노트 로드: {mapNotes.Count}개");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private IEnumerator LoadAudioClip(string path)
|
|
|
|
|
{
|
|
|
|
|
string uri = "file://" + path;
|
|
|
|
|
using var req = UnityWebRequestMultimedia.GetAudioClip(uri, AudioType.MPEG);
|
|
|
|
|
yield return req.SendWebRequest();
|
|
|
|
|
|
|
|
|
|
if (req.result != UnityWebRequest.Result.Success)
|
|
|
|
|
{
|
|
|
|
|
Debug.LogError($"[Spawner] 오디오 로드 실패: {req.error}");
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
audioSource.clip = DownloadHandlerAudioClip.GetContent(req);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 노트 스폰 ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private void SpawnNote(NoteData data)
|
|
|
|
|
{
|
|
|
|
|
if (data.colorType >= cubePrefabs.Length || data.position >= spawnPoints.Length) return;
|
|
|
|
|
|
|
|
|
|
GameObject obj = Instantiate(
|
|
|
|
|
cubePrefabs[data.colorType],
|
|
|
|
|
spawnPoints[data.position].position,
|
|
|
|
|
spawnPoints[data.position].rotation);
|
|
|
|
|
obj.transform.Rotate(transform.forward, 90 * Random.Range(0, 4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 경로 헬퍼 ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
private static string CacheRoot =>
|
|
|
|
|
Path.Combine(Application.temporaryCachePath, "beatsaber");
|
|
|
|
|
|
|
|
|
|
private static string GetAudioPath(string songId) =>
|
|
|
|
|
Path.Combine(CacheRoot, songId, $"{songId}.mp3");
|
|
|
|
|
|
|
|
|
|
private static string GetMapPath(SongInfo song, string difficulty)
|
|
|
|
|
{
|
|
|
|
|
DifficultyInfo info = song.difficulties.Get(difficulty);
|
|
|
|
|
if (info == null) return string.Empty;
|
|
|
|
|
return Path.Combine(CacheRoot, song.id, Path.GetFileName(info.mapFile));
|
|
|
|
|
}
|
|
|
|
|
}
|