2026-06-12 리듬게임 (진행중)
This commit is contained in:
@@ -1,20 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Hovl;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public enum Result { Perfect, Good, Miss }
|
||||
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()
|
||||
{
|
||||
@@ -35,7 +44,13 @@ private void Update()
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isPlaying) SpawnDueNotes();
|
||||
if (!_isPlaying) return;
|
||||
|
||||
SpawnDueNotes();
|
||||
|
||||
// 테스트용: 스페이스로 판정 (실제 VR에서는 컨트롤러/콜라이더 입력으로 교체)
|
||||
if (Keyboard.current != null && Keyboard.current.spaceKey.wasPressedThisFrame)
|
||||
OnPlayerInput();
|
||||
}
|
||||
|
||||
// SongTime 기준으로 소환할 때가 된 노트들을 순서대로 생성
|
||||
@@ -47,7 +62,8 @@ private void SpawnDueNotes()
|
||||
{
|
||||
Note note = SongNoteList[_nextNoteIndex];
|
||||
// spawnTime은 실제 프레임 시각이 아니라 이론값(note.Time - _leadTime)으로 줘야 보간이 정확
|
||||
_spawner.SpawnNote(note, note.Time - _leadTime, _songTimeProvider, OnNoteMissed);
|
||||
RhythmNoteInstance instance = _spawner.SpawnNote(note, note.Time - _leadTime, _songTimeProvider, OnNoteMissed);
|
||||
_activeNotes.Add(instance);
|
||||
_nextNoteIndex++;
|
||||
}
|
||||
}
|
||||
@@ -62,6 +78,7 @@ public void StartSong()
|
||||
{
|
||||
SongNoteList = _currentChart.GenerateNotes();
|
||||
_nextNoteIndex = 0;
|
||||
_activeNotes.Clear();
|
||||
|
||||
_audioSource.Play();
|
||||
_isPlaying = true;
|
||||
@@ -79,19 +96,54 @@ public void StopSong()
|
||||
if (SoundManager.Instance != null) SoundManager.Instance.ExitMinigameMode();
|
||||
}
|
||||
|
||||
// 노트가 판정선을 지나쳐 스스로 Miss 처리될 때 호출
|
||||
// 노트가 판정선을 지나쳐 스스로 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
public class RhythmNoteInstance : MonoBehaviour
|
||||
{
|
||||
public float HitTime; // 이 노트의 도달 시각
|
||||
[HideInInspector] public float HitTime; // 이 노트의 도달 시각
|
||||
|
||||
[SerializeField] private float _missWindow = 0.15f; // 판정선을 이만큼 지나면 자동 Miss
|
||||
|
||||
|
||||
37
Assets/02_Scripts/Rhythm/RhythmProjectile.cs
Normal file
37
Assets/02_Scripts/Rhythm/RhythmProjectile.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Hovl;
|
||||
using UnityEngine;
|
||||
|
||||
// Hovl의 HS_ProjectileMover(에셋)를 "수정하지 않고" 히트 이펙트를 코드로 터뜨리기 위한 서브클래스.
|
||||
// 핵심: protected 멤버(hit/hitPS/projectilePS/collided)는 '파생 클래스'에서는 접근 가능하다.
|
||||
// 그래서 에셋 원본은 그대로 두고, 충돌(Collision) 없이 판정 성공 시 Detonate()로 이펙트만 재생한다.
|
||||
public class RhythmProjectile : HS_ProjectileMover
|
||||
{
|
||||
// 판정 성공 시 호출. 노트가 곧 Destroy되어도 이펙트가 보이도록 노트에서 분리 후 재생.
|
||||
public void Detonate()
|
||||
{
|
||||
if (collided) return;
|
||||
collided = true;
|
||||
|
||||
// 날아가는 동안의 트레일 파티클은 정지
|
||||
if (projectilePS != null)
|
||||
projectilePS.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||||
|
||||
// 히트 이펙트 컨테이너(없으면 파티클 본체)를 노트에서 떼어내 현재 위치에서 재생
|
||||
GameObject fx = hit != null ? hit : (hitPS != null ? hitPS.gameObject : null);
|
||||
if (fx == null) return;
|
||||
|
||||
fx.transform.SetParent(null, true);
|
||||
fx.transform.position = transform.position;
|
||||
fx.SetActive(true);
|
||||
|
||||
if (hitPS != null)
|
||||
{
|
||||
hitPS.Clear(true);
|
||||
hitPS.Play(true);
|
||||
}
|
||||
|
||||
// 재생이 끝나면 분리한 이펙트도 정리
|
||||
float life = hitPS != null ? hitPS.main.duration + hitPS.main.startLifetime.constantMax : 2f;
|
||||
Destroy(fx, life);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Rhythm/RhythmProjectile.cs.meta
Normal file
2
Assets/02_Scripts/Rhythm/RhythmProjectile.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9840334459414e240b44e1b0cfe4ebb2
|
||||
Reference in New Issue
Block a user