From 67c2b2c179d9d84a0db0183dff8580b0c7d55666 Mon Sep 17 00:00:00 2001 From: nakjun Date: Thu, 18 Jun 2026 15:07:21 +0900 Subject: [PATCH] =?UTF-8?q?2026-06-18=20=EB=A7=8E=EC=9D=80=20=ED=94=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WhaleAdventure_VR/Rooms/BaseRoom.unity | 4 +- .../WhaleAdventure_VR/Rooms/CatsRoom.unity | 4 +- Assets/02_Scripts/Interaction.meta | 8 ++ .../Interaction/InteractionDetector.cs | 132 ++++++++++++++++++ .../Interaction/InteractionDetector.cs.meta | 2 + .../Interaction/InteractionObject.cs | 20 +++ .../Interaction/InteractionObject.cs.meta | 2 + Assets/02_Scripts/Interaction/SitObject.cs | 27 ++++ .../02_Scripts/Interaction/SitObject.cs.meta | 2 + .../Managers/CatsRoom/RhythmManager.cs | 60 +++++++- Assets/02_Scripts/Managers/InputManager.cs | 9 +- Assets/02_Scripts/Player/PlayerController.cs | 54 +++++++ Assets/02_Scripts/Rhythm/RhythmDrumPad.cs | 42 ++++++ .../02_Scripts/Rhythm/RhythmDrumPad.cs.meta | 2 + Assets/02_Scripts/Rhythm/RhythmStickTip.cs | 26 ++++ .../02_Scripts/Rhythm/RhythmStickTip.cs.meta | 2 + Assets/02_Scripts/UI/RhythmCountdownHud.cs | 56 ++++++++ .../02_Scripts/UI/RhythmCountdownHud.cs.meta | 2 + .../WoodStick/Prefabs/WoodStick_L.prefab | 4 +- .../WoodStick/Prefabs/WoodStick_R.prefab | 4 +- Assets/04_Models/Player.prefab | 3 - Assets/04_Models/VRPlayer.prefab | 3 + ...layer.prefab.meta => VRPlayer.prefab.meta} | 2 +- .../07_Data/Terrain/RoomOriginTerrain.asset | 2 +- .../Terrain/RoomOriginTerrain.asset.meta | 2 +- Assets/99_Settings/Input/GameInput.cs | 50 +++++++ .../99_Settings/Input/GameInput.inputactions | 31 ++++ ProjectSettings/EditorBuildSettings.asset | 2 +- ProjectSettings/TagManager.asset | 4 +- ProjectSettings/TimeManager.asset | 4 +- 30 files changed, 543 insertions(+), 22 deletions(-) create mode 100644 Assets/02_Scripts/Interaction.meta create mode 100644 Assets/02_Scripts/Interaction/InteractionDetector.cs create mode 100644 Assets/02_Scripts/Interaction/InteractionDetector.cs.meta create mode 100644 Assets/02_Scripts/Interaction/InteractionObject.cs create mode 100644 Assets/02_Scripts/Interaction/InteractionObject.cs.meta create mode 100644 Assets/02_Scripts/Interaction/SitObject.cs create mode 100644 Assets/02_Scripts/Interaction/SitObject.cs.meta create mode 100644 Assets/02_Scripts/Rhythm/RhythmDrumPad.cs create mode 100644 Assets/02_Scripts/Rhythm/RhythmDrumPad.cs.meta create mode 100644 Assets/02_Scripts/Rhythm/RhythmStickTip.cs create mode 100644 Assets/02_Scripts/Rhythm/RhythmStickTip.cs.meta create mode 100644 Assets/02_Scripts/UI/RhythmCountdownHud.cs create mode 100644 Assets/02_Scripts/UI/RhythmCountdownHud.cs.meta delete mode 100644 Assets/04_Models/Player.prefab create mode 100644 Assets/04_Models/VRPlayer.prefab rename Assets/04_Models/{Player.prefab.meta => VRPlayer.prefab.meta} (74%) diff --git a/Assets/01_Scenes/WhaleAdventure_VR/Rooms/BaseRoom.unity b/Assets/01_Scenes/WhaleAdventure_VR/Rooms/BaseRoom.unity index 14e5baa0..08fb8c5c 100644 --- a/Assets/01_Scenes/WhaleAdventure_VR/Rooms/BaseRoom.unity +++ b/Assets/01_Scenes/WhaleAdventure_VR/Rooms/BaseRoom.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37166ae25eee9456f6febea8fe87263eea4b1227752bed82a103167cb257a5bf -size 1954335 +oid sha256:131391a4bfe66c180687bd845586ca9b1eb4a5a455fdd3eb8ff19fd777bed422 +size 1954428 diff --git a/Assets/01_Scenes/WhaleAdventure_VR/Rooms/CatsRoom.unity b/Assets/01_Scenes/WhaleAdventure_VR/Rooms/CatsRoom.unity index ec533170..89052b30 100644 --- a/Assets/01_Scenes/WhaleAdventure_VR/Rooms/CatsRoom.unity +++ b/Assets/01_Scenes/WhaleAdventure_VR/Rooms/CatsRoom.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20a97e516f803d7a5d2a8bc62aaf5de5815b673407d0f2a848c1dccbeea08ab9 -size 2106715 +oid sha256:a5bc4f974575b7697b035e1f251b9498e3b66a6c8e0453a4f022b4a6e6f16bc2 +size 2058260 diff --git a/Assets/02_Scripts/Interaction.meta b/Assets/02_Scripts/Interaction.meta new file mode 100644 index 00000000..478ea25e --- /dev/null +++ b/Assets/02_Scripts/Interaction.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 76ca75090be3f364bbef8965c09e22e5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/02_Scripts/Interaction/InteractionDetector.cs b/Assets/02_Scripts/Interaction/InteractionDetector.cs new file mode 100644 index 00000000..b5ec2a42 --- /dev/null +++ b/Assets/02_Scripts/Interaction/InteractionDetector.cs @@ -0,0 +1,132 @@ +using System; +using UnityEngine; + +// 플레이어에 붙는다. 매 프레임 반경(_detectRadius) 안의 InteractionObject를 OverlapSphere로 찾아 +// 가장 가까운 대상을 Current로 노출한다. Interact 키를 누르면 그 대상과 상호작용한다. +// 지속형 상호작용(앉기 등) 중에는 대상을 잠가, 다음 키 입력이 종료로 가게 한다. +// +// 트리거 이벤트가 아니라 물리 쿼리(OverlapSphere)라 Rigidbody/트리거/충돌 매트릭스 설정이 필요 없다. +// 의자 등 대상에 Collider만 있으면 감지된다. +public class InteractionDetector : MonoBehaviour, ISceneInitializable +{ + [SerializeField] private PlayerController _player; + [SerializeField] private float _detectRadius = 1.5f; // 감지 반경(m) + [SerializeField] private LayerMask _detectLayers = ~0; // 감지할 레이어 (기본: 전체) + [SerializeField] private bool _debugLog = true; // 콘솔에 감지/입력 로그 (문제 진단용) + + private InteractionObject _current; // 가장 가까운 상호작용 대상 (없으면 null) + private InteractionObject _active; // 진행 중인 지속형 상호작용 (앉아있는 의자 등). 있으면 대상 잠금 + private readonly Collider[] _hits = new Collider[16]; // OverlapSphereNonAlloc 결과 버퍼 + + // UI 프롬프트용: 상호작용 가능한 대상이 바뀔 때 호출 (null이면 "대상 없음") + public event Action OnInteractableChanged; + + public InteractionObject Current => _current; // 현재 상호작용 가능한 대상 + public bool HasActiveInteraction => _active != null; + + private void Awake() + { + if (_player == null) _player = GetComponentInParent(); + } + + private void Start() => Subscribe(); // SceneLoadManager가 없는 씬에서도 동작하도록 폴백 구독 + + // 씬 로드 후 SceneLoadManager가 호출 (있을 때) + public void OnSceneLoaded() => Subscribe(); + + // 중복 없이 InputManager 구독 (Start/OnSceneLoaded 양쪽에서 호출돼도 안전) + private void Subscribe() + { + if (InputManager.Instance == null) + { + Debug.LogWarning("[Interaction] InputManager.Instance가 없어 Interact 입력을 구독하지 못했습니다.", this); + return; + } + InputManager.Instance.OnInteract_Event -= HandleInteract; + InputManager.Instance.OnInteract_Event += HandleInteract; + } + + private void OnDestroy() + { + if (InputManager.Instance != null) + InputManager.Instance.OnInteract_Event -= HandleInteract; + } + + private void Update() + { + // 진행 중인 상호작용이 있으면 대상 고정(잠금) — 가장 가까운 대상 갱신 안 함 + if (_active != null) return; + UpdateCurrent(); + } + + // 반경 안에서 가장 가까운 InteractionObject를 골라 Current 갱신 (바뀌면 이벤트 발생) + private void UpdateCurrent() + { + InteractionObject nearest = null; + float best = float.MaxValue; + Vector3 p = transform.position; + + // 마스크가 Nothing(0)이면 전체 레이어로 (인스펙터에서 미설정 시 안전장치) + int mask = _detectLayers.value == 0 ? ~0 : _detectLayers.value; + + // 트리거 콜라이더도 잡도록 QueryTriggerInteraction.Collide + int count = Physics.OverlapSphereNonAlloc(p, _detectRadius, _hits, mask, QueryTriggerInteraction.Collide); + for (int i = 0; i < count; i++) + { + Collider c = _hits[i]; + if (c == null) continue; + + // 콜라이더가 자식이어도 부모 쪽까지 탐색 + InteractionObject obj = c.GetComponentInParent(); + if (obj == null) continue; + + float d = (obj.transform.position - p).sqrMagnitude; + if (d < best) { best = d; nearest = obj; } + } + + if (nearest != _current) + { + _current = nearest; + if (_debugLog) Debug.Log($"[Interaction] 대상 변경: {(nearest ? nearest.name : "none")}", this); + OnInteractableChanged?.Invoke(_current); // UI 프롬프트 갱신용 + } + } + + // Interact 키 입력 처리 + private void HandleInteract() + { + if (_debugLog) + Debug.Log($"[Interaction] 키 입력. active={(_active ? _active.name : "none")}, current={(_current ? _current.name : "none")}"); + + if (_player == null) + { + Debug.LogWarning("[Interaction] Player 참조가 없습니다. (Awake 자동탐색 실패)", this); + return; + } + + // 1) 진행 중인 상호작용이 있으면 그 대상에게 (보통 종료/일어서기) + if (_active != null) + { + _active.Interact(_player); + if (!_active.IsInteracting) _active = null; // 종료됨 → 다음 Update에서 _current 재계산 + return; + } + + // 2) 아니면 가장 가까운 대상과 새로 상호작용 시작 + if (_current == null) return; + _current.Interact(_player); + if (_current.IsInteracting) + { + _active = _current; // 지속형이면 대상 잠금 + _current = null; + OnInteractableChanged?.Invoke(null); // 잠금 중엔 프롬프트 숨김 + } + } + + // 씬 뷰에서 감지 반경 시각화 (선택 시) + private void OnDrawGizmosSelected() + { + Gizmos.color = Color.cyan; + Gizmos.DrawWireSphere(transform.position, _detectRadius); + } +} diff --git a/Assets/02_Scripts/Interaction/InteractionDetector.cs.meta b/Assets/02_Scripts/Interaction/InteractionDetector.cs.meta new file mode 100644 index 00000000..21a16912 --- /dev/null +++ b/Assets/02_Scripts/Interaction/InteractionDetector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1c172720ccb5a4c41a34476feb7ddaba \ No newline at end of file diff --git a/Assets/02_Scripts/Interaction/InteractionObject.cs b/Assets/02_Scripts/Interaction/InteractionObject.cs new file mode 100644 index 00000000..6bc40a1f --- /dev/null +++ b/Assets/02_Scripts/Interaction/InteractionObject.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +// 상호작용 가능한 오브젝트의 베이스. InteractionDetector가 이 타입을 감지하고 Interact()를 호출한다. +// 감지를 위해 Collider가 반드시 필요 (RequireComponent로 강제). +[RequireComponent(typeof(Collider))] +public abstract class InteractionObject : MonoBehaviour +{ + [SerializeField] protected string _promptText = "상호작용"; // UI 프롬프트 문구 (예: "앉으려면 E") + + [SerializeField] protected Transform _interactionPos; + + // 지속형 상호작용이 진행 중인지 (예: 앉아있는 중). + // true인 동안 디텍터는 대상을 이 오브젝트로 잠가, 다음 키 입력이 종료(해제)로 가게 한다. + public bool IsInteracting { get; protected set; } + + public virtual string PromptText => _promptText; + + // 상호작용 키를 눌렀을 때 호출. 시작/종료(토글) 여부는 각 구현이 결정한다. + public abstract void Interact(PlayerController player); +} diff --git a/Assets/02_Scripts/Interaction/InteractionObject.cs.meta b/Assets/02_Scripts/Interaction/InteractionObject.cs.meta new file mode 100644 index 00000000..5aa913ac --- /dev/null +++ b/Assets/02_Scripts/Interaction/InteractionObject.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6f43a516396901747867c5f1dc2b7041 \ No newline at end of file diff --git a/Assets/02_Scripts/Interaction/SitObject.cs b/Assets/02_Scripts/Interaction/SitObject.cs new file mode 100644 index 00000000..c76c99ec --- /dev/null +++ b/Assets/02_Scripts/Interaction/SitObject.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +// 앉기 상호작용. 한 번 누르면 앉고(이동 잠금), 다시 누르면 일어선다. +public class SitObject : InteractionObject +{ + // 컴포넌트 추가 시 기본 프롬프트 문구를 앉기용으로 설정 + private void Reset() + { + _promptText = "앉으려면 E"; + } + + public override void Interact(PlayerController player) + { + if (player == null) return; + + if (!IsInteracting) + { + player.Sit(_interactionPos); // 좌석 위치로 이동 후 앉기 (_interactionPos 비우면 제자리) + IsInteracting = true; // 앉음 → 디텍터가 대상 잠금 + } + else + { + player.StandUp(); + IsInteracting = false; // 일어섬 → 잠금 해제 + } + } +} diff --git a/Assets/02_Scripts/Interaction/SitObject.cs.meta b/Assets/02_Scripts/Interaction/SitObject.cs.meta new file mode 100644 index 00000000..986080b9 --- /dev/null +++ b/Assets/02_Scripts/Interaction/SitObject.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4ea79a7e991b1004da87e7518d7c3860 \ No newline at end of file diff --git a/Assets/02_Scripts/Managers/CatsRoom/RhythmManager.cs b/Assets/02_Scripts/Managers/CatsRoom/RhythmManager.cs index 5a103cee..cab742c9 100644 --- a/Assets/02_Scripts/Managers/CatsRoom/RhythmManager.cs +++ b/Assets/02_Scripts/Managers/CatsRoom/RhythmManager.cs @@ -25,12 +25,18 @@ public class RhythmManager : MonoBehaviour [SerializeField] private GameObject StartButtonObj; + [Header("시작 카운트다운 (VR 준비 시간)")] + [SerializeField] private float _countdownTime = 5f; // 시작 버튼 후 노래가 실제로 시작되기까지 대기 시간(초) + [SerializeField] private AudioClip _countdownBeep; // 매초 카운트 효과음 (5,4,3,2,1) + [SerializeField] private AudioClip _countdownGoSfx; // 카운트 끝(GO) 효과음 (선택) + // 모든 타이밍의 기준. 오디오 클럭(dspTime) 기반이라 리드인 동안 음수(-leadTime→0)로 흐른다 public float SongTime => (float)(AudioSettings.dspTime - _dspSongStart); private List SongNoteList; private int _nextNoteIndex; // 다음에 소환할 노트 인덱스 private bool _isPlaying; //곡이 재생 중인지 (종료 감지용) + private bool _isCountingDown; // 시작 카운트다운 진행 중 여부 (중복 클릭 방지용) private double _dspSongStart; // 오디오가 실제 시작되는 dsp 시각 (= SongTime 0 지점) private float _clipLength; // 곡 길이 캐시 (종료 감지용) private Func _songTimeProvider; // 노트에 넘길 시간 제공자 (매 스폰마다 람다 재생성 방지용 캐시) @@ -44,6 +50,10 @@ public class RhythmManager : MonoBehaviour public event Action OnScoreChanged; // 점수/콤보가 바뀔 때 (실시간 HUD) public event Action OnSongFinished; // 곡이 끝났을 때 (결과창) + public event Action OnCountdownStarted; // 시작 버튼 직후 카운트다운 시작 (CountdownHud 표시) + public event Action OnCountdownTick; // 남은 초 (5,4,3,2,1) — 매초 호출 + public event Action OnCountdownFinished; // 카운트 종료(GO!) — 곡 시작 직전 + private RhythmCat[] _rhythmCats; private void Awake() @@ -151,8 +161,55 @@ public void ChangeSong() // 오토플레이 아이템 사용: 곡 시작 전(또는 도중)에 호출하면 남은 노트가 전부 자동 Perfect public void EnableAutoPlay() => _autoPlay = true; - // 곡 재생 + 채보 로드 + // 시작 버튼 핸들러: 곧장 시작하지 않고 VR 준비 시간(카운트다운) 후 BeginSong 호출 public void StartSong() + { + if (_isPlaying) return; // 이미 재생 중이면 무시 + if (_isCountingDown) return; // 카운트다운 진행 중 중복 클릭 차단 + + if (StartButtonObj != null) StartButtonObj.SetActive(false); // 버튼 즉시 숨김 + _ = CountdownThenBeginAsync(); // fire-and-forget (await 결과 불필요) + } + + // 매초 비프음 + 남은 초 이벤트를 보내며 _countdownTime 만큼 대기한 뒤 곡 시작 + private async Awaitable CountdownThenBeginAsync() + { + _isCountingDown = true; + try + { + OnCountdownStarted?.Invoke(); // CountdownHud 표시 + + int remaining = Mathf.Max(1, Mathf.CeilToInt(_countdownTime)); + while (remaining > 0) + { + OnCountdownTick?.Invoke(remaining); // 화면에 숫자 표시 + if (_countdownBeep != null && SoundManager.Instance != null) + SoundManager.Instance.PlaySFX(_countdownBeep); // 매초 비프음 + + // destroyCancellationToken: 카운트다운 도중 오브젝트가 파괴되면 await가 취소돼 + // 파괴된 객체에서 BeginSong이 호출되는 걸 막는다 + await Awaitable.WaitForSecondsAsync(1f, destroyCancellationToken); + remaining--; + } + } + catch (OperationCanceledException) + { + return; // 파괴/취소 시 곡 시작하지 않고 종료 + } + finally + { + _isCountingDown = false; + } + + OnCountdownFinished?.Invoke(); // GO! 표시 + if (_countdownGoSfx != null && SoundManager.Instance != null) + SoundManager.Instance.PlaySFX(_countdownGoSfx); + + BeginSong(); + } + + // 곡 재생 + 채보 로드 (카운트다운 종료 후 실제 시작) + private void BeginSong() { SongNoteList = _currentChart.GenerateNotes(); _nextNoteIndex = 0; @@ -161,7 +218,6 @@ public void StartSong() Score.Reset(); OnSongStarted?.Invoke(); // ScoreHud 표시 / ResultHud 숨김 OnScoreChanged?.Invoke(Score); // HUD 초기화(0점) - StartButtonObj.SetActive(false); _clipLength = _audioSource.clip != null ? _audioSource.clip.length : 0f; diff --git a/Assets/02_Scripts/Managers/InputManager.cs b/Assets/02_Scripts/Managers/InputManager.cs index 7b754a30..a54032d2 100644 --- a/Assets/02_Scripts/Managers/InputManager.cs +++ b/Assets/02_Scripts/Managers/InputManager.cs @@ -11,7 +11,8 @@ public class InputManager : MonoBehaviour, GameInput.IPlayerActions // ─── 입력 이벤트들 (PlayerController 등이 구독) ────────────────────── public event Action OnJump_Event; // 한 번씩 (눌렀을 때) - + public event Action OnInteract_Event; // 상호작용 키 (앉기 등) — 눌렀을 때 한 번씩 + //키보드로 테스트용 public event Action OnKey_Left_Event; public event Action OnKey_Right_Event; @@ -44,6 +45,12 @@ public void OnJump(InputAction.CallbackContext ctx) OnJump_Event?.Invoke(); } + public void OnInteract(InputAction.CallbackContext ctx) + { + if (ctx.phase == InputActionPhase.Started) + OnInteract_Event?.Invoke(); + } + public void OnKey_Left(InputAction.CallbackContext ctx) { if (ctx.phase == InputActionPhase.Started) diff --git a/Assets/02_Scripts/Player/PlayerController.cs b/Assets/02_Scripts/Player/PlayerController.cs index 6e050d62..92c011e6 100644 --- a/Assets/02_Scripts/Player/PlayerController.cs +++ b/Assets/02_Scripts/Player/PlayerController.cs @@ -1,3 +1,4 @@ +using Unity.XR.CoreUtils; using UnityEngine; using UnityEngine.XR.Interaction.Toolkit.Locomotion.Movement; @@ -9,12 +10,17 @@ public class PlayerController : MonoBehaviour, ISceneInitializable private Vector3 _playerVelocity; private CharacterController _controller; private ContinuousMoveProvider _moveProvider; + private XROrigin _xrOrigin; private float gravityValue = Physics.gravity.y; + public bool IsSitting { get; private set; } // 앉아있는 동안 true (이동/점프 잠금) + private float _standingHeight; // 앉기 전 카메라 높이(일어설 때 복원용) + private void Awake() { _controller = GetComponentInChildren(true); _moveProvider = GetComponentInChildren(true); + _xrOrigin = GetComponentInChildren(true); } public void OnSceneLoaded() @@ -24,14 +30,62 @@ public void OnSceneLoaded() InputManager.Instance.OnJump_Event += this.OnJump; } + public void SetHeight(float height) + { + _xrOrigin.CameraYOffset = height; + } + public void OnJump() { if (_controller == null) return; + if (IsSitting) return; // 앉아있는 동안은 점프 금지 if (!_controller.isGrounded) return; _playerVelocity.y = Mathf.Sqrt(_jumpHeight * -2f * gravityValue); } + // 앉기 시작: (좌석 지정 시) 그 위치로 이동 후 카메라 높이를 낮추고 이동을 잠근다. + public void Sit(Transform sitPoint = null) + { + if (IsSitting) return; + IsSitting = true; + + if (sitPoint != null) TeleportRig(sitPoint); // 좌석 위치/방향으로 순간이동 + + _standingHeight = _xrOrigin.CameraYOffset; // 현재 높이 기억 + SetHeight(1.2f); + + if (_moveProvider != null) _moveProvider.enabled = false; // 이동 잠금 + } + + // XR 리그(=플레이어 전체)를 target의 위치/방향으로 순간이동. + // CharacterController는 켜진 채로 transform을 직접 바꾸면 되돌려지므로 잠시 끈다. + private void TeleportRig(Transform target) + { + Transform rig = _xrOrigin != null ? _xrOrigin.transform : transform; + + bool ccWasEnabled = _controller != null && _controller.enabled; + if (_controller != null) _controller.enabled = false; + + // 회전은 수평(yaw)만 적용해 기울어짐 방지 + rig.SetPositionAndRotation(target.position, Quaternion.Euler(0f, target.eulerAngles.y, 0f)); + + if (_controller != null) _controller.enabled = ccWasEnabled; + + _playerVelocity = Vector3.zero; // 텔레포트 직후 누적 낙하속도 튐 방지 + } + + // 일어서기: 높이 복원 + 이동 잠금 해제. + public void StandUp() + { + if (!IsSitting) return; + IsSitting = false; + + SetHeight(_standingHeight); + + if (_moveProvider != null) _moveProvider.enabled = true; // 이동 잠금 해제 + } + private void Update() { // 바닥 체크 및 Y축 속도 초기화 diff --git a/Assets/02_Scripts/Rhythm/RhythmDrumPad.cs b/Assets/02_Scripts/Rhythm/RhythmDrumPad.cs new file mode 100644 index 00000000..57e62fd1 --- /dev/null +++ b/Assets/02_Scripts/Rhythm/RhythmDrumPad.cs @@ -0,0 +1,42 @@ +using UnityEngine; + +// 스네어 위치의 "트리거 콜라이더"에 붙인다. +// VR 스틱 끝(RhythmStickTip)이 이 트리거에 들어오면 RhythmManager.OnPlayerInput()을 호출한다. +// → 키보드 입력과 완전히 동일한 진입점이라 판정/효과음/노트 소비는 RhythmManager가 그대로 처리한다. +[RequireComponent(typeof(Collider))] +public class RhythmDrumPad : MonoBehaviour +{ + [SerializeField] private RhythmManager _manager; + + [Tooltip("이 속력(m/s) 미만으로 닿으면 무시. 스틱이 패드에 얹히거나 미세하게 떨릴 때 오발 방지. 0이면 비활성")] + [SerializeField] private float _minHitSpeed = 0.5f; + + [Tooltip("한 번 타격 후 이 시간(초) 동안 재타격 무시. 트리거 체류·경계 떨림으로 인한 중복 입력 방지")] + [SerializeField] private float _hitCooldown = 0.1f; + + private float _lastHitTime = float.NegativeInfinity; + + // 인스펙터에서 콜라이더를 트리거로 바꾸는 걸 깜빡하기 쉬워, 컴포넌트 추가 시 자동으로 켜 준다. + private void Reset() + { + GetComponent().isTrigger = true; + } + + private void OnTriggerEnter(Collider other) + { + if (_manager == null) return; + + // 닿은 게 스틱 끝인지 식별 (콜라이더가 자식이어도 부모 쪽까지 탐색) + RhythmStickTip tip = other.GetComponentInParent(); + if (tip == null) return; + + // 너무 약한 접촉(얹힘/떨림)은 무시 + if (_minHitSpeed > 0f && tip.Speed < _minHitSpeed) return; + + // 짧은 시간 내 중복 타격 무시 + if (Time.time - _lastHitTime < _hitCooldown) return; + _lastHitTime = Time.time; + + _manager.OnPlayerInput(); // 키 입력과 동일 경로 → 가장 가까운 노트 판정 + } +} diff --git a/Assets/02_Scripts/Rhythm/RhythmDrumPad.cs.meta b/Assets/02_Scripts/Rhythm/RhythmDrumPad.cs.meta new file mode 100644 index 00000000..61f16938 --- /dev/null +++ b/Assets/02_Scripts/Rhythm/RhythmDrumPad.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f6b6ae15947930d41b86403ced8cea59 \ No newline at end of file diff --git a/Assets/02_Scripts/Rhythm/RhythmStickTip.cs b/Assets/02_Scripts/Rhythm/RhythmStickTip.cs new file mode 100644 index 00000000..33251051 --- /dev/null +++ b/Assets/02_Scripts/Rhythm/RhythmStickTip.cs @@ -0,0 +1,26 @@ +using UnityEngine; + +// VR 스틱 끝에 붙이는 마커 + 속도 추적기. +// 드럼 패드(RhythmDrumPad)가 "닿은 게 스틱인지" 이 컴포넌트로 식별하고, +// 타격 속력을 읽어 패드에 살짝 얹히거나 떨리는 약한 접촉을 무시할 수 있게 한다. +[DisallowMultipleComponent] +public class RhythmStickTip : MonoBehaviour +{ + public float Speed { get; private set; } // 월드 기준 이동 속력(m/s) + + private Vector3 _lastPos; + + private void OnEnable() + { + _lastPos = transform.position; // 활성화 직후 첫 프레임 속력 튐 방지 + Speed = 0f; + } + + private void Update() + { + Vector3 pos = transform.position; + if (Time.deltaTime > 0f) + Speed = (pos - _lastPos).magnitude / Time.deltaTime; + _lastPos = pos; + } +} diff --git a/Assets/02_Scripts/Rhythm/RhythmStickTip.cs.meta b/Assets/02_Scripts/Rhythm/RhythmStickTip.cs.meta new file mode 100644 index 00000000..e70516b0 --- /dev/null +++ b/Assets/02_Scripts/Rhythm/RhythmStickTip.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 73ace7cbbacfff1498caf66704871f48 \ No newline at end of file diff --git a/Assets/02_Scripts/UI/RhythmCountdownHud.cs b/Assets/02_Scripts/UI/RhythmCountdownHud.cs new file mode 100644 index 00000000..1aa71e18 --- /dev/null +++ b/Assets/02_Scripts/UI/RhythmCountdownHud.cs @@ -0,0 +1,56 @@ +using TMPro; +using UnityEngine; + +// 시작 버튼 직후 VR 준비용 카운트다운(5..1, GO!) 표시. RhythmManager 이벤트만 구독하고 표시만 한다. +public class RhythmCountdownHud : MonoBehaviour +{ + [SerializeField] private RhythmManager _manager; + [SerializeField] private GameObject _root; // 카운트다운 표시 루트 (카운트 동안만 켬) + [SerializeField] private TMP_Text _countText; // 큰 숫자 / GO! 텍스트 + [SerializeField] private string _goText = "GO!"; + [SerializeField] private float _goHideDelay = 0.6f; // GO! 표시 후 자동으로 숨기는 시간(초) + + private void Awake() + { + if (_root != null) _root.SetActive(false); // 시작 전엔 숨김 + } + + private void OnEnable() + { + if (_manager == null) return; + _manager.OnCountdownStarted += HandleStarted; + _manager.OnCountdownTick += HandleTick; + _manager.OnCountdownFinished += HandleFinished; + } + + private void OnDisable() + { + if (_manager == null) return; + _manager.OnCountdownStarted -= HandleStarted; + _manager.OnCountdownTick -= HandleTick; + _manager.OnCountdownFinished -= HandleFinished; + } + + private void HandleStarted() + { + CancelInvoke(nameof(Hide)); + if (_root != null) _root.SetActive(true); // 카운트다운 시작 시 표시 + } + + private void HandleTick(int secondsLeft) + { + if (_countText != null) _countText.text = secondsLeft.ToString(); + } + + private void HandleFinished() + { + if (_countText != null) _countText.text = _goText; // GO! 잠깐 표시 후 숨김 + CancelInvoke(nameof(Hide)); + Invoke(nameof(Hide), _goHideDelay); + } + + private void Hide() + { + if (_root != null) _root.SetActive(false); + } +} diff --git a/Assets/02_Scripts/UI/RhythmCountdownHud.cs.meta b/Assets/02_Scripts/UI/RhythmCountdownHud.cs.meta new file mode 100644 index 00000000..251bab68 --- /dev/null +++ b/Assets/02_Scripts/UI/RhythmCountdownHud.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2188bb7711648514a804e0581ef52b50 \ No newline at end of file diff --git a/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_L.prefab b/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_L.prefab index e3201a57..18b22351 100644 --- a/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_L.prefab +++ b/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_L.prefab @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a99eb1e25a4920048ee2497fb6b198e1ac0633123f6fbce29e60a12936592f6 -size 22471 +oid sha256:b6e9fe63e00c6356a7d48990856837585eac460bcdc702a6fc049ad6224acf5a +size 24425 diff --git a/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_R.prefab b/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_R.prefab index 871636fc..2e341633 100644 --- a/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_R.prefab +++ b/Assets/04_Models/Objects/WoodStick/Prefabs/WoodStick_R.prefab @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c867eb9b2d3767b9de52563821b2b7ebe53aa0d60408f40c7941a977e512c32 -size 21851 +oid sha256:ce8209b6c3a166cede1d08e85499aa5923f6f2216ded2810bdca4f6bcef3db72 +size 23802 diff --git a/Assets/04_Models/Player.prefab b/Assets/04_Models/Player.prefab deleted file mode 100644 index 8ee481ac..00000000 --- a/Assets/04_Models/Player.prefab +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:195b97aba29b8c3f744aaad2fb9e2f771df59b510d1515872c7c38b05636de6f -size 61898 diff --git a/Assets/04_Models/VRPlayer.prefab b/Assets/04_Models/VRPlayer.prefab new file mode 100644 index 00000000..47cf1153 --- /dev/null +++ b/Assets/04_Models/VRPlayer.prefab @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f0b88fbbc9b25c6d1b2337d8dad11a3261964b5eabf15c471f53554bf413400 +size 106166 diff --git a/Assets/04_Models/Player.prefab.meta b/Assets/04_Models/VRPlayer.prefab.meta similarity index 74% rename from Assets/04_Models/Player.prefab.meta rename to Assets/04_Models/VRPlayer.prefab.meta index ceb613d9..0fc08cf5 100644 --- a/Assets/04_Models/Player.prefab.meta +++ b/Assets/04_Models/VRPlayer.prefab.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 401911b5467339048803f487515483df +guid: d161c4c82c3dd6e40b2ec1f88cc189c9 PrefabImporter: externalObjects: {} userData: diff --git a/Assets/07_Data/Terrain/RoomOriginTerrain.asset b/Assets/07_Data/Terrain/RoomOriginTerrain.asset index cf391b6e..34896d06 100644 --- a/Assets/07_Data/Terrain/RoomOriginTerrain.asset +++ b/Assets/07_Data/Terrain/RoomOriginTerrain.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a77d77ed5edaf40cbd13163103b82ba26ced96ab86c5b130a96410d0de2413b +oid sha256:ca4eb2dd1ffa75a5c7f362c27bcfbc12a792c462a8eabe542ba439c0f14976b4 size 5581228 diff --git a/Assets/07_Data/Terrain/RoomOriginTerrain.asset.meta b/Assets/07_Data/Terrain/RoomOriginTerrain.asset.meta index af010af8..e49eb5ba 100644 --- a/Assets/07_Data/Terrain/RoomOriginTerrain.asset.meta +++ b/Assets/07_Data/Terrain/RoomOriginTerrain.asset.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e794037241b2ed14e81cb389e83cc79e +guid: b021fdbbd2a9cc849863e0a3e92f0125 NativeFormatImporter: externalObjects: {} mainObjectFileID: 15600000 diff --git a/Assets/99_Settings/Input/GameInput.cs b/Assets/99_Settings/Input/GameInput.cs index 317ea553..8522142a 100644 --- a/Assets/99_Settings/Input/GameInput.cs +++ b/Assets/99_Settings/Input/GameInput.cs @@ -118,6 +118,15 @@ public @GameInput() ""processors"": """", ""interactions"": """", ""initialStateCheck"": false + }, + { + ""name"": ""Interact"", + ""type"": ""Button"", + ""id"": ""c7e3a1f0-1111-4abc-8def-000000000001"", + ""expectedControlType"": """", + ""processors"": """", + ""interactions"": """", + ""initialStateCheck"": false } ], ""bindings"": [ @@ -153,6 +162,28 @@ public @GameInput() ""action"": ""Key_Right"", ""isComposite"": false, ""isPartOfComposite"": false + }, + { + ""name"": """", + ""id"": ""c7e3a1f0-2222-4abc-8def-000000000002"", + ""path"": ""/e"", + ""interactions"": """", + ""processors"": """", + ""groups"": """", + ""action"": ""Interact"", + ""isComposite"": false, + ""isPartOfComposite"": false + }, + { + ""name"": """", + ""id"": ""c7e3a1f0-3333-4abc-8def-000000000003"", + ""path"": ""{LeftHand}/{PrimaryButton}"", + ""interactions"": """", + ""processors"": """", + ""groups"": """", + ""action"": ""Interact"", + ""isComposite"": false, + ""isPartOfComposite"": false } ] } @@ -164,6 +195,7 @@ public @GameInput() 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); + m_Player_Interact = m_Player.FindAction("Interact", throwIfNotFound: true); } ~@GameInput() @@ -247,6 +279,7 @@ public int FindBinding(InputBinding bindingMask, out InputAction action) private readonly InputAction m_Player_Jump; private readonly InputAction m_Player_Key_Left; private readonly InputAction m_Player_Key_Right; + private readonly InputAction m_Player_Interact; /// /// Provides access to input actions defined in input action map "Player". /// @@ -271,6 +304,10 @@ public struct PlayerActions /// public InputAction @Key_Right => m_Wrapper.m_Player_Key_Right; /// + /// Provides access to the underlying input action "Player/Interact". + /// + public InputAction @Interact => m_Wrapper.m_Player_Interact; + /// /// Provides access to the underlying input action map instance. /// public InputActionMap Get() { return m_Wrapper.m_Player; } @@ -305,6 +342,9 @@ public void AddCallbacks(IPlayerActions instance) @Key_Right.started += instance.OnKey_Right; @Key_Right.performed += instance.OnKey_Right; @Key_Right.canceled += instance.OnKey_Right; + @Interact.started += instance.OnInteract; + @Interact.performed += instance.OnInteract; + @Interact.canceled += instance.OnInteract; } /// @@ -325,6 +365,9 @@ private void UnregisterCallbacks(IPlayerActions instance) @Key_Right.started -= instance.OnKey_Right; @Key_Right.performed -= instance.OnKey_Right; @Key_Right.canceled -= instance.OnKey_Right; + @Interact.started -= instance.OnInteract; + @Interact.performed -= instance.OnInteract; + @Interact.canceled -= instance.OnInteract; } /// @@ -386,5 +429,12 @@ public interface IPlayerActions /// /// void OnKey_Right(InputAction.CallbackContext context); + /// + /// Method invoked when associated input action "Interact" is either , or . + /// + /// + /// + /// + void OnInteract(InputAction.CallbackContext context); } } diff --git a/Assets/99_Settings/Input/GameInput.inputactions b/Assets/99_Settings/Input/GameInput.inputactions index 855e0454..3ea9adef 100644 --- a/Assets/99_Settings/Input/GameInput.inputactions +++ b/Assets/99_Settings/Input/GameInput.inputactions @@ -32,6 +32,15 @@ "processors": "", "interactions": "", "initialStateCheck": false + }, + { + "name": "Interact", + "type": "Button", + "id": "c7e3a1f0-1111-4abc-8def-000000000001", + "expectedControlType": "", + "processors": "", + "interactions": "", + "initialStateCheck": false } ], "bindings": [ @@ -67,6 +76,28 @@ "action": "Key_Right", "isComposite": false, "isPartOfComposite": false + }, + { + "name": "", + "id": "c7e3a1f0-2222-4abc-8def-000000000002", + "path": "/e", + "interactions": "", + "processors": "", + "groups": "", + "action": "Interact", + "isComposite": false, + "isPartOfComposite": false + }, + { + "name": "", + "id": "c7e3a1f0-3333-4abc-8def-000000000003", + "path": "{LeftHand}/{PrimaryButton}", + "interactions": "", + "processors": "", + "groups": "", + "action": "Interact", + "isComposite": false, + "isPartOfComposite": false } ] } diff --git a/ProjectSettings/EditorBuildSettings.asset b/ProjectSettings/EditorBuildSettings.asset index 877358fb..88c35099 100644 --- a/ProjectSettings/EditorBuildSettings.asset +++ b/ProjectSettings/EditorBuildSettings.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79980c566d6789829ec652b5271dc43bbcbfafb2b4d0bd5fbbbfe0935fcc2efd +oid sha256:47a3c7fbb626ac30ca03fb77b586d372e8f10a277eb8db5b8d15b2a68b951db3 size 1546 diff --git a/ProjectSettings/TagManager.asset b/ProjectSettings/TagManager.asset index 170fdef7..843fac75 100644 --- a/ProjectSettings/TagManager.asset +++ b/ProjectSettings/TagManager.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe19e8834fcf7de3473ee504bc95f496265d39cb0d4777f99769fb421349314a -size 545 +oid sha256:fb3180412845388ffd9217932e4390a32d0328df6148a25ffd2b64da75e5dbf7 +size 562 diff --git a/ProjectSettings/TimeManager.asset b/ProjectSettings/TimeManager.asset index 38794bfd..54e03970 100644 --- a/ProjectSettings/TimeManager.asset +++ b/ProjectSettings/TimeManager.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a83e54adbbda7c9f4851103a0c6ab7f6448a3343d4ea5b7620452fa08416ecd -size 202 +oid sha256:e2bd7ac82776ce12c9f4381bc9fbe1d707b3b7bdcb205b0970c871c6f6762eb8 +size 305