2026-06-15 리듬게임 프로토타입
This commit is contained in:
@@ -114,6 +114,13 @@ public static List<Note> Parse(byte[] data, float offset, float bpmOverride, Lis
|
||||
p = trackEnd; // 트랙 경계 보정
|
||||
}
|
||||
|
||||
// [임시 디버그] MIDI에 실제로 들어있는 고유 음높이 확인 (LanePitches 번호 맞춘 뒤 삭제)
|
||||
var uniquePitches = new HashSet<int>();
|
||||
foreach (var raw in rawNotes) uniquePitches.Add(raw.pitch);
|
||||
var sortedPitches = new List<int>(uniquePitches);
|
||||
sortedPitches.Sort();
|
||||
Debug.Log($"[MidiChartParser] 발견된 고유 pitch: {string.Join(", ", sortedPitches)} (노트 {rawNotes.Count}개)");
|
||||
|
||||
// ---- 틱 → 초 변환 ----
|
||||
double usPerQuarter = bpmOverride > 0f
|
||||
? 60000000.0 / bpmOverride
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Hovl;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
@@ -10,20 +9,26 @@ public class RhythmManager : MonoBehaviour
|
||||
[SerializeField] private AudioSource _audioSource;
|
||||
[SerializeField] private RhythmChart _currentChart;
|
||||
[SerializeField] private RhythmNoteSpawner _spawner;
|
||||
[SerializeField] private float _leadTime = 2f; // 노트가 생성돼 판정선까지 흐르는 시간(초)
|
||||
[SerializeField] private float _leadTime = 1f; // 노트가 생성돼 판정선까지 흐르는 시간(초)
|
||||
|
||||
[Header("판정 윈도우 (초, 절대 시간차 기준)")]
|
||||
[SerializeField] private float _perfectWindow = 0.05f;
|
||||
[SerializeField] private float _goodWindow = 0.10f;
|
||||
[SerializeField] private float _badWindow = 0.15f; // 이 밖이면 입력 무시(헛침)
|
||||
|
||||
[Header("효과음")]
|
||||
[SerializeField] private AudioClip _hitSfx; // 내려칠 때마다 재생 (판정과 무관, 헛침 포함)
|
||||
|
||||
[SerializeField] private GameObject StartButtonObj;
|
||||
|
||||
public float SongTime => _audioSource.time; // 모든 타이밍의 기준
|
||||
// 모든 타이밍의 기준. 오디오 클럭(dspTime) 기반이라 리드인 동안 음수(-leadTime→0)로 흐른다
|
||||
public float SongTime => (float)(AudioSettings.dspTime - _dspSongStart);
|
||||
|
||||
private List<Note> SongNoteList;
|
||||
private int _nextNoteIndex; // 다음에 소환할 노트 인덱스
|
||||
private bool _isPlaying; //곡이 재생 중인지 (종료 감지용)
|
||||
private double _dspSongStart; // 오디오가 실제 시작되는 dsp 시각 (= SongTime 0 지점)
|
||||
private float _clipLength; // 곡 길이 캐시 (종료 감지용)
|
||||
private Func<float> _songTimeProvider; // 노트에 넘길 시간 제공자 (매 스폰마다 람다 재생성 방지용 캐시)
|
||||
private readonly List<RhythmNoteInstance> _activeNotes = new(); // 화면에 떠 있는(아직 처리 안 된) 노트들
|
||||
|
||||
@@ -43,24 +48,23 @@ private void Awake()
|
||||
private void Start()
|
||||
{
|
||||
ChangeSong();
|
||||
|
||||
InputManager.Instance.OnKey_Left_Event += OnPlayerInput;
|
||||
InputManager.Instance.OnKey_Right_Event += OnPlayerInput;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
//재생 중이던 곡이 끝나면 주변음 복구
|
||||
if (_isPlaying && !_audioSource.isPlaying)
|
||||
if (!_isPlaying) return;
|
||||
|
||||
//곡이 끝나면(리드인 지나 곡 길이 도달) 정지·주변음 복구
|
||||
if (SongTime >= _clipLength)
|
||||
{
|
||||
StopSong();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isPlaying) return;
|
||||
|
||||
SpawnDueNotes();
|
||||
|
||||
// 테스트용: 스페이스로 판정 (실제 VR에서는 컨트롤러/콜라이더 입력으로 교체)
|
||||
if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
|
||||
OnPlayerInput();
|
||||
}
|
||||
|
||||
// SongTime 기준으로 소환할 때가 된 노트들을 순서대로 생성
|
||||
@@ -95,7 +99,13 @@ public void StartSong()
|
||||
OnScoreChanged?.Invoke(Score); // HUD 초기화(0점)
|
||||
StartButtonObj.SetActive(false);
|
||||
|
||||
_audioSource.Play();
|
||||
_clipLength = _audioSource.clip != null ? _audioSource.clip.length : 0f;
|
||||
|
||||
// 리드인: 지금부터 _leadTime 뒤에 오디오 시작.
|
||||
// 그 사이 SongTime이 -_leadTime→0으로 흘러 첫 노트(0초 부근)도 충분히 날아온다
|
||||
_dspSongStart = AudioSettings.dspTime + _leadTime;
|
||||
_audioSource.PlayScheduled(_dspSongStart);
|
||||
|
||||
_isPlaying = true;
|
||||
|
||||
//BGM·환경음을 낮춰 리듬게임 소리만 들리게
|
||||
@@ -125,7 +135,13 @@ private void OnNoteMissed(RhythmNoteInstance note)
|
||||
|
||||
public void OnPlayerInput()
|
||||
{
|
||||
if (!_isPlaying || _activeNotes.Count == 0) return;
|
||||
if (!_isPlaying) return;
|
||||
|
||||
// 판정과 무관하게 내려칠 때마다 효과음 (헛침 포함)
|
||||
if (_hitSfx != null && SoundManager.Instance != null)
|
||||
SoundManager.Instance.PlaySFX(_hitSfx);
|
||||
|
||||
if (_activeNotes.Count == 0) return;
|
||||
|
||||
// 판정선에 가장 가까운(시간차 최소) 노트 탐색
|
||||
RhythmNoteInstance target = null;
|
||||
|
||||
@@ -11,6 +11,10 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions
|
||||
|
||||
// ─── 입력 이벤트들 (PlayerController 등이 구독) ──────────────────────
|
||||
public event Action OnJump_Event; // 한 번씩 (눌렀을 때)
|
||||
|
||||
//키보드로 테스트용
|
||||
public event Action OnKey_Left_Event;
|
||||
public event Action OnKey_Right_Event;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -39,4 +43,16 @@ public void OnJump(InputAction.CallbackContext ctx)
|
||||
if (ctx.phase == InputActionPhase.Started)
|
||||
OnJump_Event?.Invoke();
|
||||
}
|
||||
|
||||
public void OnKey_Left(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (ctx.phase == InputActionPhase.Started)
|
||||
OnKey_Left_Event?.Invoke();
|
||||
}
|
||||
|
||||
public void OnKey_Right(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (ctx.phase == InputActionPhase.Started)
|
||||
OnKey_Right_Event?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,38 @@
|
||||
|
||||
public class RhythmNoteSpawner : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private RhythmNoteInstance _notePrefab; // 생성할 노트 프리팹
|
||||
// 레인(손)별 설정 - 전용 프리팹 + 가로 위치 오프셋
|
||||
[Serializable]
|
||||
public class LaneVisual
|
||||
{
|
||||
public RhythmNoteInstance Prefab; // 이 레인 전용 노트 프리팹 (왼손/오른손 다른 모양)
|
||||
public Vector3 Offset; // 스폰/판정 위치 가로 오프셋 (왼손 -x, 오른손 +x 등)
|
||||
}
|
||||
|
||||
[SerializeField] private RhythmNoteInstance _notePrefab; // 기본 프리팹 (레인 전용 미지정 시 사용)
|
||||
[SerializeField] private Transform _spawnPoint; // 노트가 생겨나는 위치(미지정 시 자기 위치)
|
||||
[SerializeField] private Transform _judgmentLine; // 목적지(판정선)
|
||||
[SerializeField] private LaneVisual[] _lanes; // 인덱스 = Note.Lane (RhythmChart.LanePitches 순서와 일치)
|
||||
|
||||
// 노트 프리팹 생성, 목적지·타이밍 주입
|
||||
// 노트 생성, 목적지·타이밍 주입. 레인에 따라 프리팹/위치만 다르게(판정은 동일)
|
||||
public RhythmNoteInstance SpawnNote(Note note, float spawnTime,
|
||||
Func<float> songTime, Action<RhythmNoteInstance> onMiss = null)
|
||||
{
|
||||
Transform origin = _spawnPoint != null ? _spawnPoint : transform;
|
||||
RhythmNoteInstance instance = Instantiate(_notePrefab, origin.position, origin.rotation, transform);
|
||||
instance.Setup(origin.position, _judgmentLine.position, spawnTime, note.Time, songTime, onMiss);
|
||||
|
||||
// 레인 범위 밖이면 기본 프리팹/오프셋 0
|
||||
LaneVisual lane = (_lanes != null && note.Lane >= 0 && note.Lane < _lanes.Length)
|
||||
? _lanes[note.Lane] : null;
|
||||
|
||||
RhythmNoteInstance prefab = (lane != null && lane.Prefab != null) ? lane.Prefab : _notePrefab;
|
||||
|
||||
// 오프셋을 origin 방향 기준으로 적용해 레인을 평행 이동(start·target 동일 오프셋이라 경로가 곧음)
|
||||
Vector3 worldOffset = lane != null ? origin.rotation * lane.Offset : Vector3.zero;
|
||||
Vector3 start = origin.position + worldOffset;
|
||||
Vector3 target = _judgmentLine.position + worldOffset;
|
||||
|
||||
RhythmNoteInstance instance = Instantiate(prefab, start, origin.rotation, transform);
|
||||
instance.Setup(start, target, spawnTime, note.Time, songTime, onMiss);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user