2026-06-13 리듬게임 프로토타입

This commit is contained in:
skrwns304@gmail.com
2026-06-13 03:24:18 +09:00
parent e8086180a0
commit fab54e162e
8 changed files with 202 additions and 4 deletions

View File

@@ -17,6 +17,8 @@ public class RhythmManager : MonoBehaviour
[SerializeField] private float _goodWindow = 0.10f; [SerializeField] private float _goodWindow = 0.10f;
[SerializeField] private float _badWindow = 0.15f; // 이 밖이면 입력 무시(헛침) [SerializeField] private float _badWindow = 0.15f; // 이 밖이면 입력 무시(헛침)
[SerializeField] private GameObject StartButtonObj;
public float SongTime => _audioSource.time; // 모든 타이밍의 기준 public float SongTime => _audioSource.time; // 모든 타이밍의 기준
private List<Note> SongNoteList; private List<Note> SongNoteList;
@@ -25,6 +27,14 @@ public class RhythmManager : MonoBehaviour
private Func<float> _songTimeProvider; // 노트에 넘길 시간 제공자 (매 스폰마다 람다 재생성 방지용 캐시) private Func<float> _songTimeProvider; // 노트에 넘길 시간 제공자 (매 스폰마다 람다 재생성 방지용 캐시)
private readonly List<RhythmNoteInstance> _activeNotes = new(); // 화면에 떠 있는(아직 처리 안 된) 노트들 private readonly List<RhythmNoteInstance> _activeNotes = new(); // 화면에 떠 있는(아직 처리 안 된) 노트들
// 점수 상태 (HUD가 읽음). 누적 로직은 RhythmScore가 전담
public RhythmScore Score { get; } = new();
public event Action OnSongStarted; // 곡 시작 (ScoreHud 표시 / ResultHud 숨김)
public event Action<Result> OnJudged; // 노트 하나가 판정될 때마다 (HUD 연출용)
public event Action<RhythmScore> OnScoreChanged; // 점수/콤보가 바뀔 때 (실시간 HUD)
public event Action<RhythmScore> OnSongFinished; // 곡이 끝났을 때 (결과창)
private void Awake() private void Awake()
{ {
_songTimeProvider = () => SongTime; // 한 번만 만들어 모든 노트가 공유 _songTimeProvider = () => SongTime; // 한 번만 만들어 모든 노트가 공유
@@ -80,6 +90,11 @@ public void StartSong()
_nextNoteIndex = 0; _nextNoteIndex = 0;
_activeNotes.Clear(); _activeNotes.Clear();
Score.Reset();
OnSongStarted?.Invoke(); // ScoreHud 표시 / ResultHud 숨김
OnScoreChanged?.Invoke(Score); // HUD 초기화(0점)
StartButtonObj.SetActive(false);
_audioSource.Play(); _audioSource.Play();
_isPlaying = true; _isPlaying = true;
@@ -90,17 +105,21 @@ public void StartSong()
// 곡 정지 + 주변음 복구 // 곡 정지 + 주변음 복구
public void StopSong() public void StopSong()
{ {
if (!_isPlaying) return; // 중복 호출 방지(결과창 두 번 뜨는 것 차단)
_audioSource.Stop(); _audioSource.Stop();
_isPlaying = false; _isPlaying = false;
if (SoundManager.Instance != null) SoundManager.Instance.ExitMinigameMode(); if (SoundManager.Instance != null) SoundManager.Instance.ExitMinigameMode();
OnSongFinished?.Invoke(Score); // 결과창 표시
} }
// 노트가 판정선을 지나쳐 스스로 Miss 처리될 때 호출 (노트는 이미 자기 파괴됨) // 노트가 판정선을 지나쳐 스스로 Miss 처리될 때 호출 (노트는 이미 자기 파괴됨)
private void OnNoteMissed(RhythmNoteInstance note) private void OnNoteMissed(RhythmNoteInstance note)
{ {
_activeNotes.Remove(note); _activeNotes.Remove(note);
// TODO: 점수/콤보 시스템 연결 시 Miss 반영 ApplyJudge(Result.Miss);
Debug.Log("[Rhythm] Miss (지나침)"); Debug.Log("[Rhythm] Miss (지나침)");
} }
@@ -134,10 +153,18 @@ public void OnPlayerInput()
_activeNotes.Remove(target); _activeNotes.Remove(target);
Destroy(target.gameObject); Destroy(target.gameObject);
// TODO: 점수/콤보 시스템 연결 시 result 반영 ApplyJudge(result);
Debug.Log($"[Rhythm] {result} (diff {bestDiff:F3}s)"); Debug.Log($"[Rhythm] {result} (diff {bestDiff:F3}s)");
} }
// 판정 결과를 점수에 반영하고 이벤트로 알림
private void ApplyJudge(Result result)
{
Score.Apply(result);
OnJudged?.Invoke(result);
OnScoreChanged?.Invoke(Score);
}
// diff = 절대 시간차(초). 윈도우 안이면 Perfect/Good/Bad, 밖이면 Miss(입력 무시) // diff = 절대 시간차(초). 윈도우 안이면 Perfect/Good/Bad, 밖이면 Miss(입력 무시)
public Result Judge(float diff) public Result Judge(float diff)
{ {

View File

@@ -0,0 +1,59 @@
// 리듬게임 점수/콤보/판정수 누적 (Unity 비의존 순수 로직 - 테스트하기 쉬움)
public class RhythmScore
{
// 판정별 기본 점수
private const int PerfectPoint = 100;
private const int GoodPoint = 50;
private const int BadPoint = 10;
public int Score { get; private set; }
public int Combo { get; private set; }
public int MaxCombo { get; private set; }
public int PerfectCount { get; private set; }
public int GoodCount { get; private set; }
public int BadCount { get; private set; }
public int MissCount { get; private set; }
public int TotalJudged => PerfectCount + GoodCount + BadCount + MissCount;
// 판정 하나를 점수에 반영
public void Apply(Result result)
{
switch (result)
{
case Result.Perfect:
PerfectCount++;
Combo++;
Score += PerfectPoint + Combo; // 콤보가 쌓일수록 보너스 (연속 보상)
break;
case Result.Good:
GoodCount++;
Combo++;
Score += GoodPoint + Combo;
break;
case Result.Bad:
BadCount++;
Combo = 0; // 콤보 끊김
Score += BadPoint;
break;
case Result.Miss:
MissCount++;
Combo = 0; // 콤보 끊김
break;
}
if (Combo > MaxCombo) MaxCombo = Combo;
}
public void Reset()
{
Score = 0;
Combo = 0;
MaxCombo = 0;
PerfectCount = 0;
GoodCount = 0;
BadCount = 0;
MissCount = 0;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: adfd55d5980f58940ad4cb735db42f89

View File

@@ -0,0 +1,50 @@
using TMPro;
using UnityEngine;
// 곡 종료 후 결과창. 총점/판정수/정확도/등급/최대콤보를 표시한다.
public class RhythmResultHud : MonoBehaviour
{
[SerializeField] private RhythmManager _manager;
[SerializeField] private GameObject _root; // 결과창 루트
[SerializeField] private TMP_Text _scoreText;
[SerializeField] private TMP_Text _maxComboText;
[SerializeField] private TMP_Text _countText; // Perfect/Good/Bad/Miss 모아서 표시
private void Awake()
{
if (_root != null) _root.SetActive(false);
}
private void OnEnable()
{
if (_manager == null) return;
_manager.OnSongStarted += Hide;
_manager.OnSongFinished += Show;
}
private void OnDisable()
{
if (_manager == null) return;
_manager.OnSongStarted -= Hide;
_manager.OnSongFinished -= Show;
}
private void Hide()
{
if (_root != null) _root.SetActive(false); // 곡 시작/재시작 시 이전 결과창 숨김
}
private void Show(RhythmScore score)
{
if (_scoreText != null) _scoreText.text = $"{score.Score:N0}";
if (_maxComboText != null) _maxComboText.text = $"{score.MaxCombo}";
if (_countText != null)
_countText.text =
$"Perfect {score.PerfectCount}\n" +
$"Good {score.GoodCount}\n" +
$"Bad {score.BadCount}\n" +
$"Miss {score.MissCount}";
if (_root != null) _root.SetActive(true);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b187e420986956a4ba9407759ed4050f

View File

@@ -0,0 +1,56 @@
using TMPro;
using UnityEngine;
// 플레이 중 실시간 점수/콤보 HUD. RhythmManager 이벤트만 구독하고 표시만 한다.
public class RhythmScoreHud : MonoBehaviour
{
[SerializeField] private RhythmManager _manager;
[SerializeField] private GameObject _root; // HUD 루트 (곡 시작/종료에 켜고 끔)
[SerializeField] private TMP_Text _scoreText;
[SerializeField] private TMP_Text _comboText;
[SerializeField] private TMP_Text _judgeText; // Perfect/Good 등 최근 판정 표시(선택)
private void Awake()
{
if (_root != null) _root.SetActive(false); // 곡 시작 전엔 숨김
}
private void OnEnable()
{
if (_manager == null) return;
_manager.OnSongStarted += HandleSongStarted;
_manager.OnScoreChanged += HandleScoreChanged;
_manager.OnJudged += HandleJudged;
_manager.OnSongFinished += HandleSongFinished;
}
private void OnDisable()
{
if (_manager == null) return;
_manager.OnSongStarted -= HandleSongStarted;
_manager.OnScoreChanged -= HandleScoreChanged;
_manager.OnJudged -= HandleJudged;
_manager.OnSongFinished -= HandleSongFinished;
}
private void HandleSongStarted()
{
if (_root != null) _root.SetActive(true); // 곡 시작 시 실시간 HUD 표시
}
private void HandleScoreChanged(RhythmScore score)
{
if (_scoreText != null) _scoreText.text = $"SCORE {score.Score:N0}";
if (_comboText != null) _comboText.text = score.Combo > 0 ? $"{score.Combo}" : "";
}
private void HandleJudged(Result result)
{
if (_judgeText != null) _judgeText.text = result.ToString();
}
private void HandleSongFinished(RhythmScore score)
{
if (_root != null) _root.SetActive(false); // 곡 끝나면 실시간 HUD는 숨김(결과창이 대신 뜸)
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2d2c41822c633294a986c6803818303d