using System.Collections; using System.Collections.Generic; using System.IO; using TMPro; using UnityEngine; using UnityEngine.Networking; using VRBeats; using VRBeats.ScriptableEvents; public class SongController : MonoBehaviour { [SerializeField] private Spawneable cubePrefab; [SerializeField] private GameEvent onLevelComplete; [SerializeField] private TMP_Text countdownText; private AudioManager _audio; private static string CacheRoot => Path.Combine(Application.temporaryCachePath, "beatsaber"); private void Start() { _audio = FindObjectOfType(); StartCoroutine(LoadAndPlay()); } private IEnumerator LoadAndPlay() { SongInfo song = GameSession.SelectedSong; string diff = GameSession.SelectedDifficulty; if (song == null || string.IsNullOrEmpty(diff)) { Debug.LogError("[SongController] No song/difficulty selected"); yield break; } // Load audio clip from local cache string audioPath = Path.Combine(CacheRoot, song.id, song.id + ".mp3"); AudioClip clip; using (var req = UnityWebRequestMultimedia.GetAudioClip("file://" + audioPath, AudioType.MPEG)) { yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) { Debug.LogError($"[SongController] Audio load failed: {req.error}"); yield break; } clip = DownloadHandlerAudioClip.GetContent(req); } // Load and parse map DifficultyInfo diffInfo = song.difficulties.Get(diff); if (diffInfo == null) { Debug.LogError($"[SongController] Difficulty '{diff}' not found"); yield break; } string mapPath = Path.Combine(CacheRoot, song.id, Path.GetFileName(diffInfo.mapFile)); if (!File.Exists(mapPath)) { Debug.LogError($"[SongController] Map file missing: {mapPath}"); yield break; } MapData map = JsonUtility.FromJson(File.ReadAllText(mapPath)); if (map?.target == null) { Debug.LogError("[SongController] Map parse failed"); yield break; } map.target.Sort((a, b) => a.time.CompareTo(b.time)); yield return StartCoroutine(Countdown()); _audio.PlayClip(clip); StartCoroutine(SpawnRoutine(map.target)); yield return StartCoroutine(WaitForCompletion(clip.length)); } private IEnumerator Countdown() { if (countdownText == null) yield break; countdownText.gameObject.SetActive(true); string[] labels = { "3", "2", "1", "GO!" }; float[] durations = { 1f, 1f, 1f, 0.6f }; for (int i = 0; i < labels.Length; i++) { countdownText.text = labels[i]; yield return new WaitForSeconds(durations[i]); } countdownText.gameObject.SetActive(false); } private IEnumerator SpawnRoutine(List notes) { float travelTime = VR_BeatManager.instance.GameSettings.TargetTravelTime; foreach (NoteData note in notes) { float spawnAt = Mathf.Max(0f, note.time - travelTime); yield return new WaitUntil(() => _audio.CurrentTime >= spawnAt); SpawnNote(note); } } private void SpawnNote(NoteData note) { float x = -0.375f + note.position * 0.25f; float y = -0.333f + note.lineLayer * 0.333f; // 스폰 시점의 실제 남은 시간으로 역산 → 동시 노트가 프레임 차이 나도 같은 타이밍에 도착 float remaining = note.time - _audio.CurrentTime; float travelTime = Mathf.Max(0.05f, remaining); var info = new SpawnEventInfo { position = new Vector3(x, y, 0f), colorSide = note.colorType == 0 ? ColorSide.Left : ColorSide.Right, hitDirection = MapCutDirection(note.cutDirection), useSpark = true, speed = 2f, travelTimeOverride = travelTime, }; VR_BeatManager.instance.Spawn(cubePrefab, info); } // Beat Saber cutDirection → VRBeats Direction // BS: 0=Up 1=Down 2=Left 3=Right 4=UpperLeft 5=UpperRight 6=LowerLeft 7=LowerRight 8=Any private static readonly Direction[] CutDirMap = { Direction.Up, Direction.Down, Direction.Left, Direction.Right, Direction.UpperLeft, Direction.UpperRight, Direction.LowerLeft, Direction.LowerRight, Direction.Center, }; private static Direction MapCutDirection(int cut) => (cut >= 0 && cut < CutDirMap.Length) ? CutDirMap[cut] : Direction.Center; private IEnumerator WaitForCompletion(float clipLength) { yield return new WaitUntil(() => _audio.CurrentTime >= clipLength - 0.5f); onLevelComplete?.Invoke(); } }