Files
BeatSaber/Assets/Script/SongController.cs
T

156 lines
5.0 KiB
C#
Raw Normal View History

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<AudioManager>();
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<MapData>(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<NoteData> 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();
}
}