2026-06-15 리듬게임 프로토타입
This commit is contained in:
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Assets/04_Models/Note/Prefabs/MusicNote_L.prefab
LFS
Normal file
BIN
Assets/04_Models/Note/Prefabs/MusicNote_L.prefab
LFS
Normal file
Binary file not shown.
8
Assets/04_Models/Note/Prefabs/MusicNote_L.prefab.meta
Normal file
8
Assets/04_Models/Note/Prefabs/MusicNote_L.prefab.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24bc7742b39f0084ba058c0192831c60
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 100100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/04_Models/Note/Prefabs/MusicNote_R.prefab
LFS
Normal file
BIN
Assets/04_Models/Note/Prefabs/MusicNote_R.prefab
LFS
Normal file
Binary file not shown.
8
Assets/04_Models/Note/Prefabs/MusicNote_R.prefab.meta
Normal file
8
Assets/04_Models/Note/Prefabs/MusicNote_R.prefab.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ef8d383193e61348839e50435858283
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 100100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/07_Data/Rhythm/Song1.meta
Normal file
8
Assets/07_Data/Rhythm/Song1.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be648995dc31d5b4c99f30f39c048eca
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/07_Data/Rhythm/Song1/RhythmSong1.asset
LFS
Normal file
BIN
Assets/07_Data/Rhythm/Song1/RhythmSong1.asset
LFS
Normal file
Binary file not shown.
BIN
Assets/07_Data/Rhythm/Song1/Song1_Mid.bytes
Normal file
BIN
Assets/07_Data/Rhythm/Song1/Song1_Mid.bytes
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a4a85f29dd93ba4cbb7bbfc3aedcfee
|
||||
guid: 4b989a765a23e6748b978c76d50ab315
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
BIN
Assets/10_FX/SFX/SFX_Snare_Edit.wav
LFS
Normal file
BIN
Assets/10_FX/SFX/SFX_Snare_Edit.wav
LFS
Normal file
Binary file not shown.
23
Assets/10_FX/SFX/SFX_Snare_Edit.wav.meta
Normal file
23
Assets/10_FX/SFX/SFX_Snare_Edit.wav.meta
Normal file
@@ -0,0 +1,23 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e08aeecbf5318f48b313c36c73fec53
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 8
|
||||
defaultSettings:
|
||||
serializedVersion: 2
|
||||
loadType: 0
|
||||
sampleRateSetting: 0
|
||||
sampleRateOverride: 44100
|
||||
compressionFormat: 0
|
||||
quality: 1
|
||||
conversionMode: 0
|
||||
preloadAudioData: 1
|
||||
platformSettingOverrides: {}
|
||||
forceToMono: 0
|
||||
normalize: 1
|
||||
loadInBackground: 0
|
||||
ambisonic: 0
|
||||
3D: 1
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1 +0,0 @@
|
||||
지울것
|
||||
BIN
Assets/11_Audio/Source/Music/낭만고양이_채보_84.wav
LFS
Normal file
BIN
Assets/11_Audio/Source/Music/낭만고양이_채보_84.wav
LFS
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6b71773bcf77be4bbf06915c1cabb06
|
||||
guid: bfc25b7b5f6b3bd49a801d28d4367354
|
||||
AudioImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 8
|
||||
@@ -100,6 +100,24 @@ public @GameInput()
|
||||
""processors"": """",
|
||||
""interactions"": """",
|
||||
""initialStateCheck"": false
|
||||
},
|
||||
{
|
||||
""name"": ""Key_Left"",
|
||||
""type"": ""Button"",
|
||||
""id"": ""5afd9129-22e8-4e22-9843-f936922fc2a9"",
|
||||
""expectedControlType"": """",
|
||||
""processors"": """",
|
||||
""interactions"": """",
|
||||
""initialStateCheck"": false
|
||||
},
|
||||
{
|
||||
""name"": ""Key_Right"",
|
||||
""type"": ""Button"",
|
||||
""id"": ""5d342104-f81c-46cf-af37-f9388136115b"",
|
||||
""expectedControlType"": """",
|
||||
""processors"": """",
|
||||
""interactions"": """",
|
||||
""initialStateCheck"": false
|
||||
}
|
||||
],
|
||||
""bindings"": [
|
||||
@@ -113,6 +131,28 @@ public @GameInput()
|
||||
""action"": ""Jump"",
|
||||
""isComposite"": false,
|
||||
""isPartOfComposite"": false
|
||||
},
|
||||
{
|
||||
""name"": """",
|
||||
""id"": ""8de0c981-8048-4b3e-baf8-d77cf3dbcc39"",
|
||||
""path"": ""<Keyboard>/leftArrow"",
|
||||
""interactions"": """",
|
||||
""processors"": """",
|
||||
""groups"": """",
|
||||
""action"": ""Key_Left"",
|
||||
""isComposite"": false,
|
||||
""isPartOfComposite"": false
|
||||
},
|
||||
{
|
||||
""name"": """",
|
||||
""id"": ""e2dedd13-89db-4921-8a87-303eaded5f2e"",
|
||||
""path"": ""<Keyboard>/rightArrow"",
|
||||
""interactions"": """",
|
||||
""processors"": """",
|
||||
""groups"": """",
|
||||
""action"": ""Key_Right"",
|
||||
""isComposite"": false,
|
||||
""isPartOfComposite"": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -122,6 +162,8 @@ public @GameInput()
|
||||
// Player
|
||||
m_Player = asset.FindActionMap("Player", throwIfNotFound: true);
|
||||
m_Player_Jump = m_Player.FindAction("Jump", throwIfNotFound: true);
|
||||
m_Player_Key_Left = m_Player.FindAction("Key_Left", throwIfNotFound: true);
|
||||
m_Player_Key_Right = m_Player.FindAction("Key_Right", throwIfNotFound: true);
|
||||
}
|
||||
|
||||
~@GameInput()
|
||||
@@ -203,6 +245,8 @@ public int FindBinding(InputBinding bindingMask, out InputAction action)
|
||||
private readonly InputActionMap m_Player;
|
||||
private List<IPlayerActions> m_PlayerActionsCallbackInterfaces = new List<IPlayerActions>();
|
||||
private readonly InputAction m_Player_Jump;
|
||||
private readonly InputAction m_Player_Key_Left;
|
||||
private readonly InputAction m_Player_Key_Right;
|
||||
/// <summary>
|
||||
/// Provides access to input actions defined in input action map "Player".
|
||||
/// </summary>
|
||||
@@ -219,6 +263,14 @@ public struct PlayerActions
|
||||
/// </summary>
|
||||
public InputAction @Jump => m_Wrapper.m_Player_Jump;
|
||||
/// <summary>
|
||||
/// Provides access to the underlying input action "Player/Key_Left".
|
||||
/// </summary>
|
||||
public InputAction @Key_Left => m_Wrapper.m_Player_Key_Left;
|
||||
/// <summary>
|
||||
/// Provides access to the underlying input action "Player/Key_Right".
|
||||
/// </summary>
|
||||
public InputAction @Key_Right => m_Wrapper.m_Player_Key_Right;
|
||||
/// <summary>
|
||||
/// Provides access to the underlying input action map instance.
|
||||
/// </summary>
|
||||
public InputActionMap Get() { return m_Wrapper.m_Player; }
|
||||
@@ -247,6 +299,12 @@ public void AddCallbacks(IPlayerActions instance)
|
||||
@Jump.started += instance.OnJump;
|
||||
@Jump.performed += instance.OnJump;
|
||||
@Jump.canceled += instance.OnJump;
|
||||
@Key_Left.started += instance.OnKey_Left;
|
||||
@Key_Left.performed += instance.OnKey_Left;
|
||||
@Key_Left.canceled += instance.OnKey_Left;
|
||||
@Key_Right.started += instance.OnKey_Right;
|
||||
@Key_Right.performed += instance.OnKey_Right;
|
||||
@Key_Right.canceled += instance.OnKey_Right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -261,6 +319,12 @@ private void UnregisterCallbacks(IPlayerActions instance)
|
||||
@Jump.started -= instance.OnJump;
|
||||
@Jump.performed -= instance.OnJump;
|
||||
@Jump.canceled -= instance.OnJump;
|
||||
@Key_Left.started -= instance.OnKey_Left;
|
||||
@Key_Left.performed -= instance.OnKey_Left;
|
||||
@Key_Left.canceled -= instance.OnKey_Left;
|
||||
@Key_Right.started -= instance.OnKey_Right;
|
||||
@Key_Right.performed -= instance.OnKey_Right;
|
||||
@Key_Right.canceled -= instance.OnKey_Right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -308,5 +372,19 @@ public interface IPlayerActions
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||
void OnJump(InputAction.CallbackContext context);
|
||||
/// <summary>
|
||||
/// Method invoked when associated input action "Key_Left" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||
void OnKey_Left(InputAction.CallbackContext context);
|
||||
/// <summary>
|
||||
/// Method invoked when associated input action "Key_Right" is either <see cref="UnityEngine.InputSystem.InputAction.started" />, <see cref="UnityEngine.InputSystem.InputAction.performed" /> or <see cref="UnityEngine.InputSystem.InputAction.canceled" />.
|
||||
/// </summary>
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.started" />
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||
void OnKey_Right(InputAction.CallbackContext context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,24 @@
|
||||
"processors": "",
|
||||
"interactions": "",
|
||||
"initialStateCheck": false
|
||||
},
|
||||
{
|
||||
"name": "Key_Left",
|
||||
"type": "Button",
|
||||
"id": "5afd9129-22e8-4e22-9843-f936922fc2a9",
|
||||
"expectedControlType": "",
|
||||
"processors": "",
|
||||
"interactions": "",
|
||||
"initialStateCheck": false
|
||||
},
|
||||
{
|
||||
"name": "Key_Right",
|
||||
"type": "Button",
|
||||
"id": "5d342104-f81c-46cf-af37-f9388136115b",
|
||||
"expectedControlType": "",
|
||||
"processors": "",
|
||||
"interactions": "",
|
||||
"initialStateCheck": false
|
||||
}
|
||||
],
|
||||
"bindings": [
|
||||
@@ -27,6 +45,28 @@
|
||||
"action": "Jump",
|
||||
"isComposite": false,
|
||||
"isPartOfComposite": false
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"id": "8de0c981-8048-4b3e-baf8-d77cf3dbcc39",
|
||||
"path": "<Keyboard>/leftArrow",
|
||||
"interactions": "",
|
||||
"processors": "",
|
||||
"groups": "",
|
||||
"action": "Key_Left",
|
||||
"isComposite": false,
|
||||
"isPartOfComposite": false
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"id": "e2dedd13-89db-4921-8a87-303eaded5f2e",
|
||||
"path": "<Keyboard>/rightArrow",
|
||||
"interactions": "",
|
||||
"processors": "",
|
||||
"groups": "",
|
||||
"action": "Key_Right",
|
||||
"isComposite": false,
|
||||
"isPartOfComposite": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user