Files
WhaleAdventure_VR/Assets/02_Scripts/Managers/CatsRoom/RhythmManager.cs

150 lines
5.4 KiB
C#

using System;
using System.Collections.Generic;
using Hovl;
using UnityEngine;
using UnityEngine.InputSystem;
public enum Result { Perfect, Good, Bad, Miss }
public class RhythmManager : MonoBehaviour
{
[SerializeField] private AudioSource _audioSource;
[SerializeField] private RhythmChart _currentChart;
[SerializeField] private RhythmNoteSpawner _spawner;
[SerializeField] private float _leadTime = 2f; // 노트가 생성돼 판정선까지 흐르는 시간(초)
[Header("판정 윈도우 (초, 절대 시간차 기준)")]
[SerializeField] private float _perfectWindow = 0.05f;
[SerializeField] private float _goodWindow = 0.10f;
[SerializeField] private float _badWindow = 0.15f; // 이 밖이면 입력 무시(헛침)
public float SongTime => _audioSource.time; // 모든 타이밍의 기준
private List<Note> SongNoteList;
private int _nextNoteIndex; // 다음에 소환할 노트 인덱스
private bool _isPlaying; //곡이 재생 중인지 (종료 감지용)
private Func<float> _songTimeProvider; // 노트에 넘길 시간 제공자 (매 스폰마다 람다 재생성 방지용 캐시)
private readonly List<RhythmNoteInstance> _activeNotes = new(); // 화면에 떠 있는(아직 처리 안 된) 노트들
private void Awake()
{
_songTimeProvider = () => SongTime; // 한 번만 만들어 모든 노트가 공유
}
private void Start()
{
ChangeSong();
}
private void Update()
{
//재생 중이던 곡이 끝나면 주변음 복구
if (_isPlaying && !_audioSource.isPlaying)
{
StopSong();
return;
}
if (!_isPlaying) return;
SpawnDueNotes();
// 테스트용: 스페이스로 판정 (실제 VR에서는 컨트롤러/콜라이더 입력으로 교체)
if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
OnPlayerInput();
}
// SongTime 기준으로 소환할 때가 된 노트들을 순서대로 생성
private void SpawnDueNotes()
{
// 판정 시각보다 _leadTime 먼저 생성해야 박자에 맞춰 판정선에 도달
while (_nextNoteIndex < SongNoteList.Count && //총 노트수보다 노트인덱스가 작고
SongTime >= SongNoteList[_nextNoteIndex].Time - _leadTime) //노래길이가 다음 노트의 노트타임 - 리드타임 보다 작을때까지만
{
Note note = SongNoteList[_nextNoteIndex];
// spawnTime은 실제 프레임 시각이 아니라 이론값(note.Time - _leadTime)으로 줘야 보간이 정확
RhythmNoteInstance instance = _spawner.SpawnNote(note, note.Time - _leadTime, _songTimeProvider, OnNoteMissed);
_activeNotes.Add(instance);
_nextNoteIndex++;
}
}
public void ChangeSong()
{
_audioSource.clip = _currentChart.SongClip;
}
// 곡 재생 + 채보 로드
public void StartSong()
{
SongNoteList = _currentChart.GenerateNotes();
_nextNoteIndex = 0;
_activeNotes.Clear();
_audioSource.Play();
_isPlaying = true;
//BGM·환경음을 낮춰 리듬게임 소리만 들리게
if (SoundManager.Instance != null) SoundManager.Instance.EnterMinigameMode();
}
// 곡 정지 + 주변음 복구
public void StopSong()
{
_audioSource.Stop();
_isPlaying = false;
if (SoundManager.Instance != null) SoundManager.Instance.ExitMinigameMode();
}
// 노트가 판정선을 지나쳐 스스로 Miss 처리될 때 호출 (노트는 이미 자기 파괴됨)
private void OnNoteMissed(RhythmNoteInstance note)
{
_activeNotes.Remove(note);
// TODO: 점수/콤보 시스템 연결 시 Miss 반영
Debug.Log("[Rhythm] Miss (지나침)");
}
public void OnPlayerInput()
{
if (!_isPlaying || _activeNotes.Count == 0) return;
// 판정선에 가장 가까운(시간차 최소) 노트 탐색
RhythmNoteInstance target = null;
float bestDiff = float.MaxValue;
foreach (RhythmNoteInstance note in _activeNotes)
{
float diff = Mathf.Abs(SongTime - note.HitTime);
if (diff < bestDiff)
{
bestDiff = diff;
target = note;
}
}
Result result = Judge(bestDiff);
if (result == Result.Miss) return; // 히트 범위 밖이면 입력 무시 (노트 소비 안 함)
// 성공 판정 → 히트 이펙트 + 노트 소비
if (result == Result.Perfect || result == Result.Good)
{
// 이펙트는 노트에서 분리돼 따로 재생되므로 노트를 바로 파괴해도 됨
if (target.TryGetComponent(out RhythmProjectile projectile))
projectile.Detonate();
}
_activeNotes.Remove(target);
Destroy(target.gameObject);
// TODO: 점수/콤보 시스템 연결 시 result 반영
Debug.Log($"[Rhythm] {result} (diff {bestDiff:F3}s)");
}
// diff = 절대 시간차(초). 윈도우 안이면 Perfect/Good/Bad, 밖이면 Miss(입력 무시)
public Result Judge(float diff)
{
if (diff <= _perfectWindow) return Result.Perfect;
if (diff <= _goodWindow) return Result.Good;
if (diff <= _badWindow) return Result.Bad;
return Result.Miss;
}
}