From 182d2c90b94fade9c2ef8e28f6e6d787aee21aed Mon Sep 17 00:00:00 2001 From: jongjae0305 Date: Tue, 26 May 2026 18:21:58 +0900 Subject: [PATCH] fix: stabilize VR UI and song playback --- Assets/Scenes/Game.unity | 16 +++--- Assets/Script/VRPointerSetup.cs | 52 ++++++++++++++++++- .../VRBeatsKit/Scripts/Core/AudioManager.cs | 34 ++++++++++-- .../Scripts/Core/VR_InteractorController.cs | 47 +++++++++++++---- HANDOFF.md | 13 ++++- 5 files changed, 136 insertions(+), 26 deletions(-) diff --git a/Assets/Scenes/Game.unity b/Assets/Scenes/Game.unity index 4257265..27d0663 100644 --- a/Assets/Scenes/Game.unity +++ b/Assets/Scenes/Game.unity @@ -4551,9 +4551,11 @@ MonoBehaviour: m_GameObject: {fileID: 2094521061} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 3f8ab482667b2f44691ffe7131ffbdb7, type: 3} + m_Script: {fileID: 11500000, guid: 504eaffe3c185bf469313a589c4026d0, type: 3} m_Name: m_EditorClassIdentifier: + button: {fileID: 2094521063} + sceneName: Menu --- !u!1 &2138780048 GameObject: m_ObjectHideFlags: 0 @@ -4893,12 +4895,12 @@ PrefabInstance: - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, type: 3} propertyPath: m_LocalRotation.w - value: 1 + value: 0.9238795 objectReference: {fileID: 0} - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, type: 3} propertyPath: m_LocalRotation.x - value: -0 + value: 0.38268343 objectReference: {fileID: 0} - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, type: 3} @@ -4913,7 +4915,7 @@ PrefabInstance: - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, type: 3} propertyPath: m_LocalEulerAnglesHint.x - value: 0 + value: 45 objectReference: {fileID: 0} - target: {fileID: 3968956848465607219, guid: f555cbb0b089fb64ca7c7a5b07f11520, type: 3} @@ -5103,12 +5105,12 @@ PrefabInstance: - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, type: 3} propertyPath: m_LocalRotation.w - value: 1 + value: 0.9238795 objectReference: {fileID: 0} - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, type: 3} propertyPath: m_LocalRotation.x - value: -0 + value: 0.38268343 objectReference: {fileID: 0} - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, type: 3} @@ -5123,7 +5125,7 @@ PrefabInstance: - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, type: 3} propertyPath: m_LocalEulerAnglesHint.x - value: 0 + value: 45 objectReference: {fileID: 0} - target: {fileID: 1002067974212273307, guid: e7173b1ee3369204eb181b376ede2a3e, type: 3} diff --git a/Assets/Script/VRPointerSetup.cs b/Assets/Script/VRPointerSetup.cs index 99dd314..6885f04 100644 --- a/Assets/Script/VRPointerSetup.cs +++ b/Assets/Script/VRPointerSetup.cs @@ -8,16 +8,64 @@ namespace VRBeats // 나머지 씬: 바로 활성 상태로 추가. public class VRPointerSetup : MonoBehaviour { - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + private static VRPointerSetup instance; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStatics() + { + instance = null; + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void AutoInject() { + if (instance != null) + return; + var go = new GameObject("[VRPointerSetup]"); go.AddComponent(); } + private void Awake() + { + if (instance != null && instance != this) + { + Destroy(gameObject); + return; + } + + instance = this; + DontDestroyOnLoad(gameObject); + } + + private void OnEnable() + { + SceneManager.sceneLoaded += OnSceneLoaded; + } + + private void OnDisable() + { + SceneManager.sceneLoaded -= OnSceneLoaded; + } + private void Start() { - bool isGameScene = SceneManager.GetActiveScene().name == "Game"; + SetupActiveScene(); + } + + private void OnSceneLoaded(Scene scene, LoadSceneMode mode) + { + SetupScene(scene); + } + + private static void SetupActiveScene() + { + SetupScene(SceneManager.GetActiveScene()); + } + + private static void SetupScene(Scene scene) + { + bool isGameScene = scene.name == "Game"; SetupControllers(disabledByDefault: isGameScene); } diff --git a/Assets/VRBeatsKit/Scripts/Core/AudioManager.cs b/Assets/VRBeatsKit/Scripts/Core/AudioManager.cs index 505025f..aa261fa 100644 --- a/Assets/VRBeatsKit/Scripts/Core/AudioManager.cs +++ b/Assets/VRBeatsKit/Scripts/Core/AudioManager.cs @@ -11,6 +11,8 @@ namespace VRBeats [SerializeField] private float fadeOutTime = 4.0f; private AudioSource audioSource = null; + private double scheduledDspStartTime = -1.0; + private bool hasScheduledClip = false; private void Start() { @@ -43,13 +45,37 @@ namespace VRBeats public void PlayClip(AudioClip clip) { - audioSource.clip = clip; - audioSource.Play(); + PlayClipScheduled(clip); } - public float CurrentTime => audioSource != null ? audioSource.time : 0f; + public double PlayClipScheduled(AudioClip clip, double delaySeconds = 0.1) + { + ResetThisComponent(); + audioSource.Stop(); + audioSource.clip = clip; + audioSource.time = 0.0f; + + scheduledDspStartTime = AudioSettings.dspTime + delaySeconds; + hasScheduledClip = true; + audioSource.PlayScheduled(scheduledDspStartTime); + + return scheduledDspStartTime; + } + + public float CurrentTime + { + get + { + if (audioSource == null) + return 0.0f; + + if (hasScheduledClip) + return (float)(AudioSettings.dspTime - scheduledDspStartTime); + + return audioSource.time; + } + } } } - diff --git a/Assets/VRBeatsKit/Scripts/Core/VR_InteractorController.cs b/Assets/VRBeatsKit/Scripts/Core/VR_InteractorController.cs index 360ada3..d525fda 100644 --- a/Assets/VRBeatsKit/Scripts/Core/VR_InteractorController.cs +++ b/Assets/VRBeatsKit/Scripts/Core/VR_InteractorController.cs @@ -23,23 +23,48 @@ namespace VRBeats public void DisableXRRayInteractorComponents() { - rayInteractor.enabled = false; - interactorLineVisual.enabled = false; - lineRender.enabled = false; + if (rayInteractor != null) + rayInteractor.enabled = false; + if (interactorLineVisual != null) + interactorLineVisual.enabled = false; + if (lineRender != null) + lineRender.enabled = false; - // VRPointerController는 부모("LeftHand/RightHand Controller")에 추가되므로 InParent로 탐색 - var pointer = GetComponentInParent(); - if (pointer != null) pointer.enabled = false; + // VRPointerController 위치가 컨트롤러/레이 구조에 따라 달라질 수 있어서 계층 전체에서 찾는다. + var pointer = FindPointerController(); + if (pointer != null) + pointer.enabled = false; } public void EnableXRRayInteractorComponents() { - rayInteractor.enabled = true; - interactorLineVisual.enabled = true; - lineRender.enabled = true; + if (rayInteractor != null) + rayInteractor.enabled = true; + if (interactorLineVisual != null) + interactorLineVisual.enabled = true; + if (lineRender != null) + lineRender.enabled = true; - var pointer = GetComponentInParent(); - if (pointer != null) pointer.enabled = true; + var pointer = FindPointerController(); + if (pointer != null) + pointer.enabled = true; + } + + private VRPointerController FindPointerController() + { + var pointer = GetComponent(); + if (pointer != null) + return pointer; + + pointer = GetComponentInParent(); + if (pointer != null) + return pointer; + + pointer = GetComponentInChildren(true); + if (pointer != null) + return pointer; + + return transform.root.GetComponentInChildren(true); } } diff --git a/HANDOFF.md b/HANDOFF.md index a1106be..e8a648b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -14,8 +14,8 @@ Meta Quest용 VR Beat Saber 클론. Beat Sage API로 노래를 자동 채보하 - Unity 버전: `6000.3.12f1` - 현재 브랜치: `main` - 원격 저장소: `origin` = `https://whdwo798.synology.me/whdwo798/BeatSaber.git` -- 최근 푸시 커밋: `10e9eba feat: improve VR menu pointer and BeatSaber flow` -- `dotnet build VRBeatSaber.slnx` 결과: 오류 0개, 경고 60개 +- 최근 푸시 커밋: `5e5e918 docs: update project handoff status` +- `dotnet build VRBeatSaber.slnx` 결과: 오류 0개, 경고 0개 ### 실제 씬 구성 @@ -56,14 +56,23 @@ SongCreator.unity - `Assets/Script/VRPointerController.cs`, `VRPointerSetup.cs` 추가 - VR 컨트롤러 레이로 Unity UI 버튼을 직접 hover/click 처리한다. - `Game` 씬에서는 게임오버 전까지 비활성화하고, 메뉴 계열 씬에서는 활성화한다. +- `Assets/Script/VRPointerSetup.cs` + - `DontDestroyOnLoad` 싱글턴으로 변경되어 `Menu -> SongCreator -> Game` 같은 씬 전환 후에도 포인터를 다시 주입한다. + - `SceneManager.sceneLoaded`마다 현재 씬 컨트롤러를 검사한다. - `Assets/VRBeatsKit/Scripts/Core/VR_InteractorController.cs` - XR Ray Interactor enable/disable 시 `VRPointerController`도 함께 제어한다. + - 컨트롤러 구조 차이를 고려해 현재 오브젝트, 부모, 자식, 루트 하위에서 `VRPointerController`를 찾는다. +- `Assets/VRBeatsKit/Scripts/Core/AudioManager.cs` + - `AudioSource.Play()` 대신 `PlayScheduled()`를 사용하고, `AudioSettings.dspTime` 기준으로 `CurrentTime`을 계산한다. + - MP3 재생 시작 시점과 노트 스폰 기준 시간이 프레임 상태에 따라 흔들리는 문제를 줄이기 위한 변경이다. - `Assets/VRBeatsKit/Scripts/Core/VR_BeatCube.cs` - `IsCutIntentValid()`를 public으로 변경하고 `maxCutAngle`을 추가했다. - `Assets/VRBeatsKit/Scripts/Core/Cuttable.cs` - 색상/방향/속도가 틀린 큐브는 절단 시각 효과도 발생하지 않도록 막았다. - `Assets/Scenes/Game.unity` - `SongController`가 큐브 프리팹, `OnLevelComplete`, 카운트다운 텍스트와 연결되어 있다. + - `GameOverPopup`의 Back 버튼에 깨진 스크립트 참조가 있어 `LoadSceneButton`으로 복구했다. + - 현재 사용 중인 좌/우 세이버 루트 회전을 `X 45도`로 보정해 컨트롤러에서 너무 수직으로 서는 문제를 줄였다. - `Assets/VRBeatsKit/Scenes/Menu.unity` - `SongSelectManager`, `DownloadManager`, `SongDetailPanel`, `SongLibrary`가 연결되어 있다. - `VRPointerSetup`이 `VR_Manager`에 추가되어 있다.