2026-06-19 UI, UI로직
This commit is contained in:
210
Assets/My project/RhythmGame/Scripts/CatNoteSpawner.cs
Normal file
210
Assets/My project/RhythmGame/Scripts/CatNoteSpawner.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class CatNoteSpawner : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private RectTransform noteRoot;
|
||||
[SerializeField] private RectTransform noteSpawnPoint;
|
||||
[SerializeField] private RectTransform hitZone;
|
||||
|
||||
[Header("Note Prefabs")]
|
||||
[SerializeField] private RhythmNote pawNotePrefab;
|
||||
[SerializeField] private RhythmNote fishNotePrefab;
|
||||
[SerializeField] private RhythmNote musicNotePrefab;
|
||||
|
||||
[Header("Spawn Settings")]
|
||||
[SerializeField] private float normalSpawnInterval = 0.9f;
|
||||
[SerializeField] private float easySpawnInterval = 1.2f;
|
||||
[SerializeField] private float normalNoteSpeed = 360f;
|
||||
[SerializeField] private float easyNoteSpeed = 300f;
|
||||
[SerializeField] private float firstSpawnDelay = 0.5f;
|
||||
[SerializeField] private float autoMissDistance = 90f;
|
||||
[SerializeField] private int maxActiveNotes = 12;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugLog = false;
|
||||
|
||||
private readonly List<RhythmNote> activeNotes = new List<RhythmNote>();
|
||||
private Coroutine spawnRoutine;
|
||||
private bool spawning;
|
||||
private bool easyMode;
|
||||
|
||||
public event Action<RhythmNote> NoteAutoMissed;
|
||||
|
||||
public bool IsSpawning => spawning;
|
||||
public IReadOnlyList<RhythmNote> ActiveNotes => activeNotes;
|
||||
|
||||
public void StartSpawn(bool useEasyMode)
|
||||
{
|
||||
StopSpawn(false);
|
||||
|
||||
easyMode = useEasyMode;
|
||||
spawning = true;
|
||||
spawnRoutine = StartCoroutine(SpawnRoutine());
|
||||
}
|
||||
|
||||
public void StopSpawn(bool clearNotes = true)
|
||||
{
|
||||
spawning = false;
|
||||
|
||||
if (spawnRoutine != null)
|
||||
{
|
||||
StopCoroutine(spawnRoutine);
|
||||
spawnRoutine = null;
|
||||
}
|
||||
|
||||
if (clearNotes)
|
||||
ClearNotes();
|
||||
}
|
||||
|
||||
private IEnumerator SpawnRoutine()
|
||||
{
|
||||
yield return new WaitForSeconds(firstSpawnDelay);
|
||||
|
||||
while (spawning)
|
||||
{
|
||||
if (activeNotes.Count < maxActiveNotes)
|
||||
SpawnNote();
|
||||
|
||||
yield return new WaitForSeconds(GetSpawnInterval());
|
||||
}
|
||||
}
|
||||
|
||||
public RhythmNote SpawnNote()
|
||||
{
|
||||
RhythmNote prefab = PickNotePrefab();
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning("[CatNoteSpawner] 사용할 노트 프리팹이 없습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (noteRoot == null || noteSpawnPoint == null || hitZone == null)
|
||||
{
|
||||
Debug.LogWarning("[CatNoteSpawner] NoteRoot, NoteSpawnPoint, HitZone 중 연결되지 않은 값이 있습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
RhythmNote note = Instantiate(prefab, noteRoot);
|
||||
RectTransform noteRect = note.RectTransform;
|
||||
|
||||
noteRect.localScale = Vector3.one;
|
||||
noteRect.localRotation = Quaternion.identity;
|
||||
noteRect.localPosition = noteRoot.InverseTransformPoint(noteSpawnPoint.position);
|
||||
|
||||
float hitX = noteRoot.InverseTransformPoint(hitZone.position).x;
|
||||
note.Initialize(GetNoteSpeed(), hitX, autoMissDistance);
|
||||
note.PassedHitZone += OnNotePassedHitZone;
|
||||
|
||||
activeNotes.Add(note);
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.Log("[CatNoteSpawner] Note Spawned");
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
private RhythmNote PickNotePrefab()
|
||||
{
|
||||
List<RhythmNote> availablePrefabs = new List<RhythmNote>();
|
||||
|
||||
if (pawNotePrefab != null)
|
||||
availablePrefabs.Add(pawNotePrefab);
|
||||
|
||||
if (fishNotePrefab != null)
|
||||
availablePrefabs.Add(fishNotePrefab);
|
||||
|
||||
if (musicNotePrefab != null)
|
||||
availablePrefabs.Add(musicNotePrefab);
|
||||
|
||||
if (availablePrefabs.Count == 0)
|
||||
return null;
|
||||
|
||||
return availablePrefabs[UnityEngine.Random.Range(0, availablePrefabs.Count)];
|
||||
}
|
||||
|
||||
private float GetSpawnInterval()
|
||||
{
|
||||
return easyMode ? easySpawnInterval : normalSpawnInterval;
|
||||
}
|
||||
|
||||
private float GetNoteSpeed()
|
||||
{
|
||||
return easyMode ? easyNoteSpeed : normalNoteSpeed;
|
||||
}
|
||||
|
||||
private void OnNotePassedHitZone(RhythmNote note)
|
||||
{
|
||||
NoteAutoMissed?.Invoke(note);
|
||||
RemoveNote(note, true);
|
||||
}
|
||||
|
||||
public RhythmNote GetClosestNote()
|
||||
{
|
||||
CleanupNullNotes();
|
||||
|
||||
RhythmNote closest = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < activeNotes.Count; i++)
|
||||
{
|
||||
RhythmNote note = activeNotes[i];
|
||||
|
||||
if (note == null || note.IsJudged)
|
||||
continue;
|
||||
|
||||
float distance = note.GetDistanceToHitZone();
|
||||
|
||||
if (distance < closestDistance)
|
||||
{
|
||||
closest = note;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
public void RemoveNote(RhythmNote note, bool destroyObject)
|
||||
{
|
||||
if (note == null)
|
||||
return;
|
||||
|
||||
note.PassedHitZone -= OnNotePassedHitZone;
|
||||
activeNotes.Remove(note);
|
||||
|
||||
if (destroyObject)
|
||||
note.Remove();
|
||||
else
|
||||
note.MarkJudged();
|
||||
}
|
||||
|
||||
public void ClearNotes()
|
||||
{
|
||||
for (int i = activeNotes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
RhythmNote note = activeNotes[i];
|
||||
|
||||
if (note == null)
|
||||
continue;
|
||||
|
||||
note.PassedHitZone -= OnNotePassedHitZone;
|
||||
note.Remove();
|
||||
}
|
||||
|
||||
activeNotes.Clear();
|
||||
}
|
||||
|
||||
private void CleanupNullNotes()
|
||||
{
|
||||
for (int i = activeNotes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (activeNotes[i] == null)
|
||||
activeNotes.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07ecee837dba4c046b3036198781e371
|
||||
323
Assets/My project/RhythmGame/Scripts/RhythmGameManager.cs
Normal file
323
Assets/My project/RhythmGame/Scripts/RhythmGameManager.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
public class RhythmGameManager : MonoBehaviour
|
||||
{
|
||||
public enum ResultType
|
||||
{
|
||||
Perfect,
|
||||
Good,
|
||||
Miss
|
||||
}
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private RhythmGameUI ui;
|
||||
[SerializeField] private CatNoteSpawner noteSpawner;
|
||||
[SerializeField] private RhythmHapticManager haptic;
|
||||
|
||||
[Header("Game Settings")]
|
||||
[SerializeField] private string songTitle = "고양이 합창단의 노래";
|
||||
[SerializeField] private bool startOnAwake = false;
|
||||
[SerializeField] private bool useFishBonusOnStart = false;
|
||||
|
||||
[Header("Rules")]
|
||||
[SerializeField] private int requiredScore = 1000;
|
||||
[SerializeField] private int allowedMisses = 5;
|
||||
[SerializeField] private int perfectScore = 150;
|
||||
[SerializeField] private int goodScore = 80;
|
||||
|
||||
[Header("Judgment Distance")]
|
||||
[Tooltip("HitZone 중심과 노트 중심의 거리입니다. 값이 작을수록 판정이 어려워집니다.")]
|
||||
[SerializeField] private float perfectDistance = 35f;
|
||||
[SerializeField] private float goodDistance = 75f;
|
||||
[SerializeField] private float missRemoveDistance = 120f;
|
||||
|
||||
[Header("Events")]
|
||||
public UnityEvent onGameStarted;
|
||||
public UnityEvent onGameSuccess;
|
||||
public UnityEvent onGameFailed;
|
||||
public UnityEvent onPerfect;
|
||||
public UnityEvent onGood;
|
||||
public UnityEvent onMiss;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool showDebugLog = true;
|
||||
|
||||
private int score;
|
||||
private int combo;
|
||||
private int missCount;
|
||||
private bool activeGame;
|
||||
private bool fishBonusActive;
|
||||
|
||||
public int Score => score;
|
||||
public int Combo => combo;
|
||||
public int MissCount => missCount;
|
||||
public bool IsPlaying => activeGame;
|
||||
public bool FishBonusActive => fishBonusActive;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.NoteAutoMissed += HandleAutoMiss;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.NoteAutoMissed -= HandleAutoMiss;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (startOnAwake)
|
||||
StartGame(useFishBonusOnStart);
|
||||
else
|
||||
InitializeIdleState();
|
||||
}
|
||||
|
||||
private void InitializeIdleState()
|
||||
{
|
||||
score = 0;
|
||||
combo = 0;
|
||||
missCount = 0;
|
||||
activeGame = false;
|
||||
|
||||
if (ui != null)
|
||||
{
|
||||
ui.InitializeUI(songTitle);
|
||||
ui.ShowFishBonus(false);
|
||||
}
|
||||
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.StopSpawn(true);
|
||||
}
|
||||
|
||||
public void StartGame()
|
||||
{
|
||||
StartGame(false);
|
||||
}
|
||||
|
||||
public void StartGameWithFishBonus()
|
||||
{
|
||||
StartGame(true);
|
||||
}
|
||||
|
||||
public void StartGame(bool useFishBonus)
|
||||
{
|
||||
score = 0;
|
||||
combo = 0;
|
||||
missCount = 0;
|
||||
fishBonusActive = useFishBonus;
|
||||
activeGame = true;
|
||||
|
||||
if (ui != null)
|
||||
{
|
||||
ui.InitializeUI(songTitle);
|
||||
ui.ShowFishBonus(fishBonusActive);
|
||||
ui.UpdateScore(score);
|
||||
ui.UpdateCombo(combo);
|
||||
}
|
||||
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.StartSpawn(fishBonusActive);
|
||||
|
||||
onGameStarted?.Invoke();
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.Log("[RhythmGameManager] 리듬게임 시작");
|
||||
}
|
||||
|
||||
public void SubmitHit()
|
||||
{
|
||||
if (!activeGame)
|
||||
return;
|
||||
|
||||
if (noteSpawner == null)
|
||||
{
|
||||
RegisterMiss(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
RhythmNote closestNote = noteSpawner.GetClosestNote();
|
||||
|
||||
if (closestNote == null)
|
||||
{
|
||||
RegisterMiss(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
float distance = closestNote.GetDistanceToHitZone();
|
||||
ResultType result = EvaluateResult(distance);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case ResultType.Perfect:
|
||||
RegisterPerfect(closestNote);
|
||||
break;
|
||||
|
||||
case ResultType.Good:
|
||||
RegisterGood(closestNote);
|
||||
break;
|
||||
|
||||
case ResultType.Miss:
|
||||
bool removeNote = distance <= missRemoveDistance;
|
||||
RegisterMiss(closestNote, removeNote);
|
||||
break;
|
||||
}
|
||||
|
||||
CheckGameEnd();
|
||||
}
|
||||
|
||||
private ResultType EvaluateResult(float distance)
|
||||
{
|
||||
if (distance <= perfectDistance)
|
||||
return ResultType.Perfect;
|
||||
|
||||
if (distance <= goodDistance)
|
||||
return ResultType.Good;
|
||||
|
||||
return ResultType.Miss;
|
||||
}
|
||||
|
||||
private void RegisterPerfect(RhythmNote note)
|
||||
{
|
||||
score += perfectScore;
|
||||
combo++;
|
||||
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.RemoveNote(note, true);
|
||||
|
||||
if (ui != null)
|
||||
{
|
||||
ui.UpdateScore(score);
|
||||
ui.UpdateCombo(combo);
|
||||
ui.ShowJudgment("완벽!");
|
||||
}
|
||||
|
||||
if (haptic != null)
|
||||
haptic.Perfect();
|
||||
|
||||
onPerfect?.Invoke();
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.Log("[RhythmGameManager] Perfect");
|
||||
}
|
||||
|
||||
private void RegisterGood(RhythmNote note)
|
||||
{
|
||||
score += goodScore;
|
||||
combo++;
|
||||
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.RemoveNote(note, true);
|
||||
|
||||
if (ui != null)
|
||||
{
|
||||
ui.UpdateScore(score);
|
||||
ui.UpdateCombo(combo);
|
||||
ui.ShowJudgment("좋아!");
|
||||
}
|
||||
|
||||
if (haptic != null)
|
||||
haptic.Good();
|
||||
|
||||
onGood?.Invoke();
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.Log("[RhythmGameManager] Good");
|
||||
}
|
||||
|
||||
private void RegisterMiss(RhythmNote note, bool removeNote)
|
||||
{
|
||||
missCount++;
|
||||
combo = 0;
|
||||
|
||||
if (removeNote && noteSpawner != null && note != null)
|
||||
noteSpawner.RemoveNote(note, true);
|
||||
|
||||
if (ui != null)
|
||||
{
|
||||
ui.UpdateCombo(combo);
|
||||
ui.ShowJudgment("실패!");
|
||||
}
|
||||
|
||||
if (haptic != null)
|
||||
haptic.Miss();
|
||||
|
||||
onMiss?.Invoke();
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.Log($"[RhythmGameManager] Miss {missCount}/{allowedMisses}");
|
||||
}
|
||||
|
||||
private void HandleAutoMiss(RhythmNote note)
|
||||
{
|
||||
if (!activeGame)
|
||||
return;
|
||||
|
||||
RegisterMiss(note, false);
|
||||
CheckGameEnd();
|
||||
}
|
||||
|
||||
private void CheckGameEnd()
|
||||
{
|
||||
if (!activeGame)
|
||||
return;
|
||||
|
||||
if (score >= requiredScore)
|
||||
{
|
||||
GameSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
if (missCount >= allowedMisses)
|
||||
{
|
||||
GameFail();
|
||||
}
|
||||
}
|
||||
|
||||
private void GameSuccess()
|
||||
{
|
||||
activeGame = false;
|
||||
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.StopSpawn(true);
|
||||
|
||||
if (ui != null)
|
||||
ui.ShowFinalResult("리듬 성공!");
|
||||
|
||||
onGameSuccess?.Invoke();
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.Log("[RhythmGameManager] 리듬게임 성공");
|
||||
}
|
||||
|
||||
private void GameFail()
|
||||
{
|
||||
activeGame = false;
|
||||
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.StopSpawn(true);
|
||||
|
||||
if (ui != null)
|
||||
ui.ShowFinalResult("리듬 실패!");
|
||||
|
||||
onGameFailed?.Invoke();
|
||||
|
||||
if (showDebugLog)
|
||||
Debug.Log("[RhythmGameManager] 리듬게임 실패");
|
||||
}
|
||||
|
||||
public void StopGame()
|
||||
{
|
||||
activeGame = false;
|
||||
|
||||
if (noteSpawner != null)
|
||||
noteSpawner.StopSpawn(true);
|
||||
}
|
||||
|
||||
public void ResetGame()
|
||||
{
|
||||
StartGame(fishBonusActive);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 547419d18208b11499a3e4597da9cfe0
|
||||
155
Assets/My project/RhythmGame/Scripts/RhythmGameUI.cs
Normal file
155
Assets/My project/RhythmGame/Scripts/RhythmGameUI.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using System.Collections;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class RhythmGameUI : MonoBehaviour
|
||||
{
|
||||
[Header("Panel")]
|
||||
[SerializeField] private GameObject rhythmPanel;
|
||||
[SerializeField] private GameObject fishBonusPanel;
|
||||
[SerializeField] private GameObject finalResultPanel;
|
||||
|
||||
[Header("Text")]
|
||||
[SerializeField] private TMP_Text songTitleText;
|
||||
[SerializeField] private TMP_Text scoreText;
|
||||
[SerializeField] private TMP_Text comboText;
|
||||
[SerializeField] private TMP_Text judgmentText;
|
||||
[SerializeField] private TMP_Text fishBonusText;
|
||||
[SerializeField] private TMP_Text guideText;
|
||||
[SerializeField] private TMP_Text finalResultText;
|
||||
|
||||
[Header("Optional Images")]
|
||||
[SerializeField] private Image fishIcon;
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private string defaultSongTitle = "고양이 합창단의 노래";
|
||||
[SerializeField] private string defaultGuideText = "박자에 맞춰 양동이를 두드리세요";
|
||||
[SerializeField] private float judgmentShowTime = 0.45f;
|
||||
[SerializeField] private bool hidePanelOnFinalResult = false;
|
||||
|
||||
private Coroutine judgmentRoutine;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
InitializeUI(defaultSongTitle);
|
||||
}
|
||||
|
||||
public void InitializeUI(string songTitle = null)
|
||||
{
|
||||
SetPanelVisible(true);
|
||||
HideFinalResult();
|
||||
HideJudgment();
|
||||
ShowFishBonus(false);
|
||||
|
||||
SetSongTitle(string.IsNullOrWhiteSpace(songTitle) ? defaultSongTitle : songTitle);
|
||||
UpdateScore(0);
|
||||
UpdateCombo(0);
|
||||
SetGuideText(defaultGuideText);
|
||||
}
|
||||
|
||||
public void SetPanelVisible(bool visible)
|
||||
{
|
||||
if (rhythmPanel != null)
|
||||
rhythmPanel.SetActive(visible);
|
||||
}
|
||||
|
||||
public void SetSongTitle(string title)
|
||||
{
|
||||
if (songTitleText != null)
|
||||
songTitleText.text = title;
|
||||
}
|
||||
|
||||
public void UpdateScore(int score)
|
||||
{
|
||||
if (scoreText != null)
|
||||
scoreText.text = $"점수 {score}";
|
||||
}
|
||||
|
||||
public void UpdateCombo(int combo)
|
||||
{
|
||||
if (comboText != null)
|
||||
comboText.text = $"{combo} COMBO";
|
||||
}
|
||||
|
||||
public void SetGuideText(string text)
|
||||
{
|
||||
if (guideText != null)
|
||||
guideText.text = text;
|
||||
}
|
||||
|
||||
public void ShowFishBonus(bool active)
|
||||
{
|
||||
if (fishBonusPanel != null)
|
||||
fishBonusPanel.SetActive(active);
|
||||
|
||||
if (fishIcon != null)
|
||||
fishIcon.gameObject.SetActive(active);
|
||||
|
||||
if (fishBonusText != null)
|
||||
{
|
||||
fishBonusText.gameObject.SetActive(active);
|
||||
fishBonusText.text = active ? "생선 사용됨! 난이도 감소" : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowJudgment(string text)
|
||||
{
|
||||
if (judgmentText == null)
|
||||
return;
|
||||
|
||||
if (judgmentRoutine != null)
|
||||
StopCoroutine(judgmentRoutine);
|
||||
|
||||
judgmentRoutine = StartCoroutine(JudgmentRoutine(text));
|
||||
}
|
||||
|
||||
private IEnumerator JudgmentRoutine(string text)
|
||||
{
|
||||
judgmentText.gameObject.SetActive(true);
|
||||
judgmentText.text = text;
|
||||
|
||||
yield return new WaitForSeconds(judgmentShowTime);
|
||||
|
||||
judgmentText.gameObject.SetActive(false);
|
||||
judgmentRoutine = null;
|
||||
}
|
||||
|
||||
public void HideJudgment()
|
||||
{
|
||||
if (judgmentRoutine != null)
|
||||
{
|
||||
StopCoroutine(judgmentRoutine);
|
||||
judgmentRoutine = null;
|
||||
}
|
||||
|
||||
if (judgmentText != null)
|
||||
judgmentText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void ShowFinalResult(string text)
|
||||
{
|
||||
HideJudgment();
|
||||
|
||||
if (hidePanelOnFinalResult)
|
||||
SetPanelVisible(false);
|
||||
|
||||
if (finalResultPanel != null)
|
||||
finalResultPanel.SetActive(true);
|
||||
|
||||
if (finalResultText != null)
|
||||
{
|
||||
finalResultText.gameObject.SetActive(true);
|
||||
finalResultText.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
public void HideFinalResult()
|
||||
{
|
||||
if (finalResultPanel != null)
|
||||
finalResultPanel.SetActive(false);
|
||||
|
||||
if (finalResultText != null)
|
||||
finalResultText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c479efa8fd85c62459abc520125dbd67
|
||||
54
Assets/My project/RhythmGame/Scripts/RhythmHapticManager.cs
Normal file
54
Assets/My project/RhythmGame/Scripts/RhythmHapticManager.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.XR;
|
||||
|
||||
public class RhythmHapticManager : MonoBehaviour
|
||||
{
|
||||
[Header("Haptic Target")]
|
||||
[SerializeField] private XRNode targetHand = XRNode.RightHand;
|
||||
|
||||
[Header("Haptic Settings")]
|
||||
[SerializeField] private bool useHaptic = true;
|
||||
|
||||
[SerializeField] private float perfectAmplitude = 0.85f;
|
||||
[SerializeField] private float perfectDuration = 0.12f;
|
||||
|
||||
[SerializeField] private float goodAmplitude = 0.5f;
|
||||
[SerializeField] private float goodDuration = 0.08f;
|
||||
|
||||
[SerializeField] private float missAmplitude = 0.2f;
|
||||
[SerializeField] private float missDuration = 0.05f;
|
||||
|
||||
private void SendHaptic(float amplitude, float duration)
|
||||
{
|
||||
if (!useHaptic)
|
||||
return;
|
||||
|
||||
InputDevice device = InputDevices.GetDeviceAtXRNode(targetHand);
|
||||
|
||||
if (!device.isValid)
|
||||
return;
|
||||
|
||||
if (device.TryGetHapticCapabilities(out HapticCapabilities capabilities))
|
||||
{
|
||||
if (!capabilities.supportsImpulse)
|
||||
return;
|
||||
}
|
||||
|
||||
device.SendHapticImpulse(0u, Mathf.Clamp01(amplitude), duration);
|
||||
}
|
||||
|
||||
public void Perfect()
|
||||
{
|
||||
SendHaptic(perfectAmplitude, perfectDuration);
|
||||
}
|
||||
|
||||
public void Good()
|
||||
{
|
||||
SendHaptic(goodAmplitude, goodDuration);
|
||||
}
|
||||
|
||||
public void Miss()
|
||||
{
|
||||
SendHaptic(missAmplitude, missDuration);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bacf1526cdbe3b41951db8edd4b82d9
|
||||
52
Assets/My project/RhythmGame/Scripts/RhythmInputHandler.cs
Normal file
52
Assets/My project/RhythmGame/Scripts/RhythmInputHandler.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class RhythmInputHandler : MonoBehaviour
|
||||
{
|
||||
[Header("Reference")]
|
||||
[SerializeField] private RhythmGameManager rhythmGameManager;
|
||||
|
||||
[Header("Keyboard Test")]
|
||||
[SerializeField] private bool allowKeyboardTest = true;
|
||||
[SerializeField] private Key testKey = Key.Space;
|
||||
|
||||
[Header("Auto Find")]
|
||||
[SerializeField] private bool autoFindManager = true;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (rhythmGameManager == null && autoFindManager)
|
||||
rhythmGameManager = FindFirstObjectByType<RhythmGameManager>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!allowKeyboardTest)
|
||||
return;
|
||||
|
||||
if (Keyboard.current == null)
|
||||
return;
|
||||
|
||||
if (Keyboard.current[testKey].wasPressedThisFrame)
|
||||
SubmitHit();
|
||||
}
|
||||
|
||||
public void OnHit(InputValue value)
|
||||
{
|
||||
if (value.isPressed)
|
||||
SubmitHit();
|
||||
}
|
||||
|
||||
public void SubmitHit()
|
||||
{
|
||||
if (rhythmGameManager == null)
|
||||
return;
|
||||
|
||||
rhythmGameManager.SubmitHit();
|
||||
}
|
||||
|
||||
public void SetManager(RhythmGameManager manager)
|
||||
{
|
||||
rhythmGameManager = manager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ebe14fa52a6afab49a0ccf9420f9c36a
|
||||
96
Assets/My project/RhythmGame/Scripts/RhythmNote.cs
Normal file
96
Assets/My project/RhythmGame/Scripts/RhythmNote.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public enum RhythmNoteType
|
||||
{
|
||||
Paw,
|
||||
Fish,
|
||||
Music
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
public class RhythmNote : MonoBehaviour
|
||||
{
|
||||
[Header("Note")]
|
||||
[SerializeField] private RhythmNoteType noteType = RhythmNoteType.Paw;
|
||||
|
||||
[Header("Runtime")]
|
||||
[SerializeField] private float moveSpeed = 360f;
|
||||
[SerializeField] private float hitZoneLocalX;
|
||||
[SerializeField] private float autoMissDistance = 90f;
|
||||
|
||||
private RectTransform rectTransform;
|
||||
private bool initialized;
|
||||
private bool judged;
|
||||
|
||||
public RhythmNoteType NoteType => noteType;
|
||||
public bool IsJudged => judged;
|
||||
public float HitZoneLocalX => hitZoneLocalX;
|
||||
public RectTransform RectTransform => rectTransform;
|
||||
|
||||
public event Action<RhythmNote> PassedHitZone;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!initialized || judged)
|
||||
return;
|
||||
|
||||
MoveNote();
|
||||
CheckAutoMiss();
|
||||
}
|
||||
|
||||
public void Initialize(float speed, float targetHitZoneLocalX, float missDistance)
|
||||
{
|
||||
if (rectTransform == null)
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
moveSpeed = speed;
|
||||
hitZoneLocalX = targetHitZoneLocalX;
|
||||
autoMissDistance = Mathf.Max(0f, missDistance);
|
||||
|
||||
judged = false;
|
||||
initialized = true;
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void MoveNote()
|
||||
{
|
||||
Vector2 position = rectTransform.anchoredPosition;
|
||||
position.x += moveSpeed * Time.deltaTime;
|
||||
rectTransform.anchoredPosition = position;
|
||||
}
|
||||
|
||||
private void CheckAutoMiss()
|
||||
{
|
||||
if (rectTransform.anchoredPosition.x <= hitZoneLocalX + autoMissDistance)
|
||||
return;
|
||||
|
||||
MarkJudged();
|
||||
PassedHitZone?.Invoke(this);
|
||||
}
|
||||
|
||||
public float GetDistanceToHitZone()
|
||||
{
|
||||
if (rectTransform == null)
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
return Mathf.Abs(rectTransform.anchoredPosition.x - hitZoneLocalX);
|
||||
}
|
||||
|
||||
public void MarkJudged()
|
||||
{
|
||||
judged = true;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
MarkJudged();
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
2
Assets/My project/RhythmGame/Scripts/RhythmNote.cs.meta
Normal file
2
Assets/My project/RhythmGame/Scripts/RhythmNote.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ebe9358a05af2f74188b1d31d50f813f
|
||||
Reference in New Issue
Block a user