2026-06-18 많은 플레이어 수정사항
This commit is contained in:
Binary file not shown.
Binary file not shown.
8
Assets/02_Scripts/Interaction.meta
Normal file
8
Assets/02_Scripts/Interaction.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76ca75090be3f364bbef8965c09e22e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
132
Assets/02_Scripts/Interaction/InteractionDetector.cs
Normal file
132
Assets/02_Scripts/Interaction/InteractionDetector.cs
Normal file
@@ -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<InteractionObject> OnInteractableChanged;
|
||||
|
||||
public InteractionObject Current => _current; // 현재 상호작용 가능한 대상
|
||||
public bool HasActiveInteraction => _active != null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_player == null) _player = GetComponentInParent<PlayerController>();
|
||||
}
|
||||
|
||||
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<InteractionObject>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c172720ccb5a4c41a34476feb7ddaba
|
||||
20
Assets/02_Scripts/Interaction/InteractionObject.cs
Normal file
20
Assets/02_Scripts/Interaction/InteractionObject.cs
Normal file
@@ -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);
|
||||
}
|
||||
2
Assets/02_Scripts/Interaction/InteractionObject.cs.meta
Normal file
2
Assets/02_Scripts/Interaction/InteractionObject.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f43a516396901747867c5f1dc2b7041
|
||||
27
Assets/02_Scripts/Interaction/SitObject.cs
Normal file
27
Assets/02_Scripts/Interaction/SitObject.cs
Normal file
@@ -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; // 일어섬 → 잠금 해제
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Interaction/SitObject.cs.meta
Normal file
2
Assets/02_Scripts/Interaction/SitObject.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ea79a7e991b1004da87e7518d7c3860
|
||||
@@ -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<Note> SongNoteList;
|
||||
private int _nextNoteIndex; // 다음에 소환할 노트 인덱스
|
||||
private bool _isPlaying; //곡이 재생 중인지 (종료 감지용)
|
||||
private bool _isCountingDown; // 시작 카운트다운 진행 중 여부 (중복 클릭 방지용)
|
||||
private double _dspSongStart; // 오디오가 실제 시작되는 dsp 시각 (= SongTime 0 지점)
|
||||
private float _clipLength; // 곡 길이 캐시 (종료 감지용)
|
||||
private Func<float> _songTimeProvider; // 노트에 넘길 시간 제공자 (매 스폰마다 람다 재생성 방지용 캐시)
|
||||
@@ -44,6 +50,10 @@ public class RhythmManager : MonoBehaviour
|
||||
public event Action<RhythmScore> OnScoreChanged; // 점수/콤보가 바뀔 때 (실시간 HUD)
|
||||
public event Action<RhythmScore> OnSongFinished; // 곡이 끝났을 때 (결과창)
|
||||
|
||||
public event Action OnCountdownStarted; // 시작 버튼 직후 카운트다운 시작 (CountdownHud 표시)
|
||||
public event Action<int> 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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<CharacterController>(true);
|
||||
_moveProvider = GetComponentInChildren<ContinuousMoveProvider>(true);
|
||||
_xrOrigin = GetComponentInChildren<XROrigin>(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축 속도 초기화
|
||||
|
||||
42
Assets/02_Scripts/Rhythm/RhythmDrumPad.cs
Normal file
42
Assets/02_Scripts/Rhythm/RhythmDrumPad.cs
Normal file
@@ -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<Collider>().isTrigger = true;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (_manager == null) return;
|
||||
|
||||
// 닿은 게 스틱 끝인지 식별 (콜라이더가 자식이어도 부모 쪽까지 탐색)
|
||||
RhythmStickTip tip = other.GetComponentInParent<RhythmStickTip>();
|
||||
if (tip == null) return;
|
||||
|
||||
// 너무 약한 접촉(얹힘/떨림)은 무시
|
||||
if (_minHitSpeed > 0f && tip.Speed < _minHitSpeed) return;
|
||||
|
||||
// 짧은 시간 내 중복 타격 무시
|
||||
if (Time.time - _lastHitTime < _hitCooldown) return;
|
||||
_lastHitTime = Time.time;
|
||||
|
||||
_manager.OnPlayerInput(); // 키 입력과 동일 경로 → 가장 가까운 노트 판정
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Rhythm/RhythmDrumPad.cs.meta
Normal file
2
Assets/02_Scripts/Rhythm/RhythmDrumPad.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6b6ae15947930d41b86403ced8cea59
|
||||
26
Assets/02_Scripts/Rhythm/RhythmStickTip.cs
Normal file
26
Assets/02_Scripts/Rhythm/RhythmStickTip.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Rhythm/RhythmStickTip.cs.meta
Normal file
2
Assets/02_Scripts/Rhythm/RhythmStickTip.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73ace7cbbacfff1498caf66704871f48
|
||||
56
Assets/02_Scripts/UI/RhythmCountdownHud.cs
Normal file
56
Assets/02_Scripts/UI/RhythmCountdownHud.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/UI/RhythmCountdownHud.cs.meta
Normal file
2
Assets/02_Scripts/UI/RhythmCountdownHud.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2188bb7711648514a804e0581ef52b50
|
||||
Binary file not shown.
Binary file not shown.
BIN
Assets/04_Models/Player.prefab
LFS
BIN
Assets/04_Models/Player.prefab
LFS
Binary file not shown.
BIN
Assets/04_Models/VRPlayer.prefab
LFS
Normal file
BIN
Assets/04_Models/VRPlayer.prefab
LFS
Normal file
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 401911b5467339048803f487515483df
|
||||
guid: d161c4c82c3dd6e40b2ec1f88cc189c9
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e794037241b2ed14e81cb389e83cc79e
|
||||
guid: b021fdbbd2a9cc849863e0a3e92f0125
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 15600000
|
||||
|
||||
@@ -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"": ""<Keyboard>/e"",
|
||||
""interactions"": """",
|
||||
""processors"": """",
|
||||
""groups"": """",
|
||||
""action"": ""Interact"",
|
||||
""isComposite"": false,
|
||||
""isPartOfComposite"": false
|
||||
},
|
||||
{
|
||||
""name"": """",
|
||||
""id"": ""c7e3a1f0-3333-4abc-8def-000000000003"",
|
||||
""path"": ""<XRController>{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;
|
||||
/// <summary>
|
||||
/// Provides access to input actions defined in input action map "Player".
|
||||
/// </summary>
|
||||
@@ -271,6 +304,10 @@ public struct PlayerActions
|
||||
/// </summary>
|
||||
public InputAction @Key_Right => m_Wrapper.m_Player_Key_Right;
|
||||
/// <summary>
|
||||
/// Provides access to the underlying input action "Player/Interact".
|
||||
/// </summary>
|
||||
public InputAction @Interact => m_Wrapper.m_Player_Interact;
|
||||
/// <summary>
|
||||
/// Provides access to the underlying input action map instance.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -386,5 +429,12 @@ public interface IPlayerActions
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.performed" />
|
||||
/// <seealso cref="UnityEngine.InputSystem.InputAction.canceled" />
|
||||
void OnKey_Right(InputAction.CallbackContext context);
|
||||
/// <summary>
|
||||
/// Method invoked when associated input action "Interact" 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 OnInteract(InputAction.CallbackContext context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "<Keyboard>/e",
|
||||
"interactions": "",
|
||||
"processors": "",
|
||||
"groups": "",
|
||||
"action": "Interact",
|
||||
"isComposite": false,
|
||||
"isPartOfComposite": false
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"id": "c7e3a1f0-3333-4abc-8def-000000000003",
|
||||
"path": "<XRController>{LeftHand}/{PrimaryButton}",
|
||||
"interactions": "",
|
||||
"processors": "",
|
||||
"groups": "",
|
||||
"action": "Interact",
|
||||
"isComposite": false,
|
||||
"isPartOfComposite": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user