Files
BeatSaber/Assets/Script/SongController.cs
T

202 lines
6.7 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;
2026-05-26 18:54:56 +09:00
private const float LaneSpacing = 0.42f;
private const float LayerSpacing = 0.38f;
private const float HorizontalCenter = 1.5f;
private const float VerticalCenter = 1f;
private AudioManager _audio;
2026-05-28 19:01:20 +09:00
private ScoreManager _scoreManager;
private float _clipLength;
private static string CacheRoot =>
2026-05-28 19:01:20 +09:00
Path.Combine(Application.persistentDataPath, "beatsaber");
private void Start()
{
2026-05-26 18:54:56 +09:00
_audio = FindFirstObjectByType<AudioManager>();
2026-05-28 19:01:20 +09:00
_scoreManager = FindFirstObjectByType<ScoreManager>();
StartCoroutine(LoadAndPlay());
}
2026-05-28 19:01:20 +09:00
private void Update()
{
if (_audio != null && _scoreManager != null && _clipLength > 0.0f)
_scoreManager.SetSongProgress(_audio.CurrentTime, _clipLength);
}
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);
}
2026-05-28 19:01:20 +09:00
_clipLength = clip.length;
// 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;
}
2026-05-26 18:54:56 +09:00
map.target.Sort(CompareNotes);
2026-05-28 19:01:20 +09:00
_scoreManager?.SetTotalNotes(map.target.Count);
yield return StartCoroutine(Countdown());
_audio.PlayClip(clip);
StartCoroutine(SpawnRoutine(map.target));
2026-05-28 19:01:20 +09:00
yield return StartCoroutine(WaitForCompletion(clip.length, map.target));
}
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)
{
2026-05-28 19:01:20 +09:00
float adjustedNoteTime = note.time + GlobalSyncSettings.AudioOffsetSeconds;
float spawnAt = Mathf.Max(0f, adjustedNoteTime - travelTime);
yield return new WaitUntil(() => _audio.CurrentTime >= spawnAt);
SpawnNote(note);
}
}
private void SpawnNote(NoteData note)
{
2026-05-26 18:54:56 +09:00
float x = MapLaneX(note.position);
float y = MapLayerY(note.lineLayer);
// 스폰 시점의 실제 남은 시간으로 역산 → 동시 노트가 프레임 차이 나도 같은 타이밍에 도착
2026-05-28 19:01:20 +09:00
float remaining = note.time + GlobalSyncSettings.AudioOffsetSeconds - _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),
2026-05-28 19:01:20 +09:00
useSpark = false,
speed = 2f,
travelTimeOverride = travelTime,
};
VR_BeatManager.instance.Spawn(cubePrefab, info);
}
2026-05-26 18:54:56 +09:00
private static int CompareNotes(NoteData a, NoteData b)
{
int timeCompare = a.time.CompareTo(b.time);
if (timeCompare != 0)
return timeCompare;
int positionCompare = a.position.CompareTo(b.position);
if (positionCompare != 0)
return positionCompare;
return a.lineLayer.CompareTo(b.lineLayer);
}
private static float MapLaneX(int position)
{
int lane = Mathf.Clamp(position, 0, 3);
return (lane - HorizontalCenter) * LaneSpacing;
}
private static float MapLayerY(int lineLayer)
{
int layer = Mathf.Clamp(lineLayer, 0, 2);
return (layer - VerticalCenter) * LayerSpacing;
}
// 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;
2026-05-28 19:01:20 +09:00
private IEnumerator WaitForCompletion(float clipLength, List<NoteData> notes)
{
2026-05-28 19:01:20 +09:00
float lastNoteTime = notes.Count > 0 ? notes[notes.Count - 1].time : 0.0f;
float resultTime = Mathf.Min(clipLength, lastNoteTime + GlobalSyncSettings.AudioOffsetSeconds + 0.35f);
yield return new WaitUntil(() => _audio.CurrentTime >= resultTime);
yield return new WaitForSeconds(0.35f);
_scoreManager?.CompleteSong();
onLevelComplete?.Invoke();
}
}