# VR Beat Saber 프로젝트 인수인계 문서 ## 개요 Meta Quest용 VR Beat Saber 클론. Beat Sage API로 노래를 자동 채보하고, Synology NAS에 업로드/다운로드한다. 이 문서는 **기존 프로젝트(구버전)를 VRBeatsKit 기반 새 프로젝트로 이전**하기 위한 인수인계 자료다. --- ## 현재 상태 (2026-05-26) 현재 저장소는 VRBeatsKit 기반 프로젝트 위에 커스텀 곡 선택/생성/NAS 연동 흐름을 붙인 상태다. - 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개 ### 실제 씬 구성 현재 Build Settings는 아래 순서다. 1. `Assets/VRBeatsKit/Scenes/Menu.unity` 2. `Assets/VRBeatsKit/Scenes/BoxingStyle.unity` 3. `Assets/Scenes/SongCreator.unity` 4. `Assets/VRBeatsKit/Scenes/SaberStyle.unity` 5. `Assets/Scenes/Game.unity` 문서 아래쪽에 남아 있는 `Intro -> SongSelect -> Game -> SongCreator` 흐름은 목표 설계에 가깝다. 현재 실제 진입점은 VRBeatsKit `Menu.unity`이며, 그 안의 `SongSelect` 패널이 커스텀 곡 선택 UI 역할을 한다. ### 현재 구현된 주요 흐름 ```text Menu.unity / SongSelect -> DownloadManager가 NAS 정적 서버의 songs.json 로드 -> SongDetailPanel에서 곡/난이도 다운로드 -> GameSession.SelectedSong / SelectedDifficulty 설정 -> Game.unity 로드 Game.unity -> SongController가 temporaryCachePath의 mp3 + map json 로드 -> VRBeats.AudioManager로 음악 재생 -> 오디오 시간 기준으로 VR_BeatManager.Spawn() 호출 -> VR_BeatCube / Cuttable / DamageSaber가 색상, 방향, 속도 판정 SongCreator.unity -> SongCreatorManager가 로컬 mp3 또는 직접 mp3 URL 입력 -> BeatSageUploader가 Beat Sage 요청/폴링/ZIP 다운로드 -> BeatSageConverter가 .dat를 NoteData로 변환 -> NasPublisher가 mp3, map json, songs.json을 Synology NAS에 업로드 ``` ### 최근 반영된 변경 - `Assets/Script/VRPointerController.cs`, `VRPointerSetup.cs` 추가 - VR 컨트롤러 레이로 Unity UI 버튼을 직접 hover/click 처리한다. - `Game` 씬에서는 게임오버 전까지 비활성화하고, 메뉴 계열 씬에서는 활성화한다. - `Assets/VRBeatsKit/Scripts/Core/VR_InteractorController.cs` - XR Ray Interactor enable/disable 시 `VRPointerController`도 함께 제어한다. - `Assets/VRBeatsKit/Scripts/Core/VR_BeatCube.cs` - `IsCutIntentValid()`를 public으로 변경하고 `maxCutAngle`을 추가했다. - `Assets/VRBeatsKit/Scripts/Core/Cuttable.cs` - 색상/방향/속도가 틀린 큐브는 절단 시각 효과도 발생하지 않도록 막았다. - `Assets/Scenes/Game.unity` - `SongController`가 큐브 프리팹, `OnLevelComplete`, 카운트다운 텍스트와 연결되어 있다. - `Assets/VRBeatsKit/Scenes/Menu.unity` - `SongSelectManager`, `DownloadManager`, `SongDetailPanel`, `SongLibrary`가 연결되어 있다. - `VRPointerSetup`이 `VR_Manager`에 추가되어 있다. - `Assets/img/360.mp4`, `Assets/img/beatSaber.png` - 메뉴/비주얼용 에셋으로 추가됨. - `.gitignore` - `*.csproj.user` 제외 추가. - `.gitattributes` - `*.mp4 binary` 추가. ### 현재 주의사항 1. `Assets/StreamingAssets/nas_config.json`은 현재 저장소에 없다. NAS 업로드를 테스트하려면 로컬에 직접 만들어야 하며, 절대 커밋하지 않는다. 2. `SongCreator.unity`의 직렬화된 `nasBaseUrl` 값에 끝 공백이 들어가 있다: `http://whdwo798.synology.me:5000 `. 런타임에서 `nas_config.json`으로 덮어쓰지 않으면 로그인 URL 문제가 날 수 있다. 3. `SongCreatorManager`는 난이도 토글 필드를 갖고 있지만 현재 로직은 4개 난이도(`normal`, `hard`, `expert`, `expertplus`)를 항상 전부 생성한다. 4. `manualEditorButton`은 씬에서 미연결이고 코드에서도 사용하지 않는다. 5. `Assets/img/360.mp4`는 약 192MB다. 현재 일반 Git으로 푸시되어 있으므로, 원격 정책이 바뀌면 Git LFS 전환을 검토해야 한다. --- ## 기존 프로젝트 소스 코드 **기존 프로젝트 전체 파일은 아래 git 저장소에서 가져온다.** ``` https://whdwo798.synology.me/whdwo798/BeatSaber.git ``` ```bash git clone https://whdwo798.synology.me/whdwo798/BeatSaber.git ``` > 단, 이 저장소는 구버전 프로젝트다. 새 프로젝트는 VRBeatsKit을 기반으로 재구성하며, > 위 저장소의 `Assets/Script/` 등 핵심 스크립트를 참고/이식하는 용도로 사용한다. --- ## Git 설정 (새 프로젝트) 새 Unity 프로젝트를 생성한 뒤 **가장 먼저** git을 초기화하고 파일을 커밋해야 한다. Claude Code는 대화 시작 시 `git status` / `git log`를 자동으로 읽어 컨텍스트를 파악한다. 커밋이 없으면 Claude가 변경 이력을 추적할 수 없다. ### 초기화 순서 ```bash # 새 프로젝트 루트에서 git init git remote add origin ``` ### .gitignore 기존 프로젝트의 `.gitignore`를 복사하면 된다. 핵심 규칙: ```gitignore # Unity 표준 /Library/ /Temp/ /Obj/ /Build/ /Builds/ /Logs/ /UserSettings/ # NAS 비밀번호 — 절대 커밋 금지 /Assets/StreamingAssets/nas_config.json /Assets/StreamingAssets/nas_config.json.meta ``` ### 첫 커밋 파일 복사 완료 후: ```bash git add . git commit -m "init: VRBeatsKit 기반 프로젝트 초기 설정" ``` 이후 기능 단위로 커밋하면 Claude가 `git log`로 작업 이력을 파악한다. --- ## 새 프로젝트 구성 방법 ### 전제 조건 1. Unity Hub에서 **새 URP 3D 프로젝트** 생성 (기존 프로젝트와 동일 Unity 버전) 2. Asset Store에서 **VRBeatsKit** 임포트 3. Package Manager에서 아래 패키지 설치: - XR Interaction Toolkit (3.x) - XR Hands - OpenXR Plugin - TextMeshPro - Unity Input System ### 복사할 파일 (기존 프로젝트 → 새 프로젝트) 아래 폴더/파일을 `Assets/` 아래에 그대로 복사한다. ``` Assets/Script/ ← 아래 "복사 제외" 목록 참고 Assets/Editor/VRBeatSaberSceneBuilder.cs Assets/StreamingAssets/ ← nas_config.json 포함 (절대 git 커밋 금지) Assets/Fonts/NanumGothic SDF.asset 및 관련 파일 Assets/360Music/ Assets/Audio/ ← HitSound.wav, MissSound.wav Assets/Prefab/ ← RED.prefab, BLUE.prefab (추후 VRBeatsKit 큐브로 교체) ``` ### 복사 제외 (VRBeatsKit으로 대체) ``` Assets/Script/Saber.cs → VRBeatsKit VR_Saber.cs 사용 Assets/Script/Cube.cs → VRBeatsKit VR_BeatCube.cs 사용 ``` --- ## 전체 씬 구성 ``` Intro → SongSelect → Game SongSelect → SongCreator → (NAS 업로드) → SongSelect ``` | 씬 | 역할 | |---|---| | Intro | 로고 → SongSelect 자동 전환 | | SongSelect | NAS에서 songs.json 로드, 곡 목록 표시, 다운로드/플레이 | | Game | 음악 재생 + 큐브 스폰 + 점수/HP + 결과 화면 | | SongCreator | 음악 파일 선택 → Beat Sage API 채보 → NAS 업로드 | | MapEditorScene | 맵 에디터 (선택적) | --- ## 전체 데이터 흐름 ``` [SongCreator] 사용자: 음악 파일 선택 (로컬 파일 또는 URL) → BeatSageUploader: Beat Sage API 채보 요청 POST https://beatsage.com/create → GET /heartbeat/{id} 폴링 → GET /download/{id} → .zip (Normal.dat, Hard.dat, Expert.dat, ExpertPlus.dat) → BeatSageConverter: .dat → NoteData 변환 → NasPublisher: Synology NAS 업로드 songs.json 갱신: /web/beatsaber/songs.json 맵 JSON: /web/beatsaber/maps/Map_{id}_{diff}.json 오디오: /web/beatsaber/music/{id}.mp3 [SongSelect] → DownloadManager: NAS에서 songs.json 로드 → 사용자 곡 선택 → GameSession.SelectedSong, GameSession.SelectedDifficulty 설정 → 다운로드: {id}.mp3 + Map_{id}_{diff}.json → Application.temporaryCachePath/beatsaber/{id}/ [Game] → Spawner.InitGame(): 캐시에서 오디오/맵 로드 → VRBeatsKit AudioManager AudioSource에 클립 세팅 → 카운트다운 3→2→1→GO → 매 프레임: audioSource.time 기준으로 VR_BeatManager.Spawn() 호출 → ScoreManager: 히트/미스 집계 → HP → 결과 화면 ``` --- ## 주요 스크립트 역할 ### 복사하는 스크립트 (수정 없음) | 파일 | 역할 | |---|---| | `GameSession.cs` | static 컨테이너 — 씬 간 선택 곡/난이도 전달 | | `NoteData.cs` | DTO — NoteData, MapData, SongInfo, DifficultyMap 등 | | `BeatSageConverter.cs` | Beat Sage .dat 형식 → NoteData 변환 | | `BeatSageUploader.cs` | Beat Sage API 연동 (POST/GET). `LastMetadata` 프로퍼티에 info.dat 파싱 결과 저장. | | `NasPublisher.cs` | Synology DSM 7.2 API 업로드 | | `DownloadManager.cs` | NAS → 로컬 캐시 다운로드 | | `SongLibrary.cs` | 다운로드 상태 추적 (persistentDataPath) | | `SongSelectManager.cs` | 곡 목록 UI | | `SongDetailPanel.cs` | 곡 상세 / 다운로드 / 플레이 버튼 | | `SongCreatorManager.cs` | 크리에이터 UI, 파일 선택, URL 다운로드. title/BPM 수동 입력 불필요 — info.dat에서 자동 추출. 난이도는 현재 항상 4개 전부 생성. | | `SongController.cs` | Game 씬 실행부. 캐시된 mp3/map json을 로드하고 VRBeatsKit `VR_BeatManager.Spawn()`으로 노트를 스폰. | | `DesktopUIMode.cs` | 에디터에서 XR 없이 테스트할 때 UI Raycaster 교체 | | `VRPointerController.cs` | VR 컨트롤러 레이로 UI hover/click 처리. 디버그 로그 포함. | | `VRPointerSetup.cs` | 씬 로드 후 손/컨트롤러 오브젝트에 `VRPointerController` 자동 주입. | | `XRSimulatorLoader.cs` | 에디터/PC 테스트용 XR Interaction Simulator 프리팹 주입. | ### 현재 미이식/미확인 스크립트 | 파일 | 내용 | |---|---| | `IntroManager.cs` | 현재 저장소에 없음. 인트로 씬 흐름을 살릴 경우 작성/이식 필요. | | `ScoreManager.cs`, `ScoreHUD.cs`, `ResultsPanel.cs` | 전역 네임스페이스 커스텀 점수 UI는 현재 저장소에 없음. 현재는 VRBeatsKit `VRBeats.ScoreManager`와 이벤트 자산을 사용. | | `SaberGlow.cs`, `SaberSkinSelector.cs`, `CacheManager.cs` | 현재 저장소에 없음. 필요 시 기존 프로젝트에서 이식. | --- ## Game 실행부 현재 구현 기존 인수인계 문서에는 `Spawner.cs`를 새로 작성하라고 되어 있었지만, 현재 저장소에서는 별도 `Spawner.cs` 대신 `Assets/Script/SongController.cs`가 그 역할을 수행한다. ### `SongController.cs` 핵심 ```csharp private IEnumerator LoadAndPlay() { SongInfo song = GameSession.SelectedSong; string diff = GameSession.SelectedDifficulty; // mp3와 map json을 Application.temporaryCachePath/beatsaber/{songId}/ 에서 로드 // 카운트다운 후 VRBeats.AudioManager.PlayClip(clip) // SpawnRoutine(map.target) 실행 } private void SpawnNote(NoteData note) { var info = new SpawnEventInfo { position = new Vector3(x, y, 0f), colorSide = note.colorType == 0 ? ColorSide.Left : ColorSide.Right, hitDirection = MapCutDirection(note.cutDirection), useSpark = true, speed = 2f, travelTimeOverride = note.time - _audio.CurrentTime, }; VR_BeatManager.instance.Spawn(cubePrefab, info); } ``` `travelTimeOverride`는 동시 노트가 프레임 차이로 스폰되어도 같은 타이밍에 도착하도록 `VR_BeatManager`에 추가된 값이다. --- ## ScoreManager 충돌 없음 - 예전 설계: 전역 네임스페이스 커스텀 `ScoreManager.cs` - 현재 저장소: `Assets/VRBeatsKit/Scripts/UI/ScoreManager.cs`의 `VRBeats.ScoreManager` 사용 - 전역 커스텀 `ScoreManager.cs`를 다시 이식하면 네임스페이스가 달라 공존은 가능하지만, 이벤트 연결과 UI 패널을 별도로 구성해야 한다. --- ## NAS 설정 | 항목 | 값 | |---|---| | DSM API (내부) | `http://192.168.55.3:5000` | | DSM API (외부) | `http://whdwo798.synology.me` | | 정적 파일 서버 | `http://whdwo798.synology.me/beatsaber` | | NAS 루트 경로 | `/web/beatsaber` | | 비밀번호 저장 위치 | `Assets/StreamingAssets/nas_config.json` | **보안 규칙**: `nas_config.json`은 절대 git에 커밋하지 않는다. `.gitignore`에 추가 필수. ```json { "host": "http://192.168.55.3:5000", "publicHost": "http://whdwo798.synology.me", "account": "계정명", "password": "비밀번호" } ``` --- ## NAS 파일 구조 ``` /web/beatsaber/ ├── songs.json ← 전체 곡 목록 ├── maps/ │ └── Map_{id}_{difficulty}.json ← 난이도별 맵 (NoteData 배열) └── music/ └── {id}.mp3 ← 오디오 파일 ``` ### songs.json 형식 ```json { "version": "1.0", "songs": [ { "id": "uuid", "title": "곡 제목", "artist": "아티스트", "bpm": 120.0, "duration": 180, "audioFile": "music/uuid.mp3", "audioSize": 1234567, "coverImage": "", "noteJumpSpeed": 10.0, "difficulties": { "normal": { "mapFile": "maps/Map_uuid_normal.json", "mapSize": 0, "noteCount": 0 }, "hard": { "mapFile": "maps/Map_uuid_hard.json", "mapSize": 0, "noteCount": 0 }, "expert": { "mapFile": "maps/Map_uuid_expert.json", "mapSize": 0, "noteCount": 0 }, "expertplus": { "mapFile": "maps/Map_uuid_expertplus.json", "mapSize": 0, "noteCount": 0 } }, "addedAt": "2026-05-21T00:00:00Z" } ] } ``` ### 맵 JSON 형식 (Map_{id}_{diff}.json) ```json { "target": [ { "time": 1.23, "position": 1, "lineLayer": 1, "colorType": 0, "cutDirection": 1 } ] } ``` --- ## Beat Sage API - **Base URL**: `https://beatsage.com` - **흐름**: `POST /create` → `GET /heartbeat/{id}` 폴링 (status: "DONE") → `GET /download/{id}` (.zip) - **지원 난이도**: Normal, Hard, Expert, ExpertPlus - **zip 내 파일명**: `Normal.dat`, `Hard.dat`, `Expert.dat`, `ExpertPlus.dat`, `info.dat` - **인증 불필요** (퍼블릭 API) - **입력 방식 2가지**: `audio_file`(로컬 파일 업로드) 또는 `audio_url`(직접 URL 전달, Beat Sage 서버에서 다운로드) - **info.dat 활용**: `_beatsPerMinute`(자동 감지), `_songName`, `_songAuthorName` 추출 → `BeatSageUploader.LastMetadata`에 저장. SongCreatorManager에서 이 값을 우선 사용하고 UI 입력이 있으면 override. --- ## ScoreManager 명세 ```csharp public class ScoreManager : MonoBehaviour { public static ScoreManager Instance; public int Score; public int Combo; public int MaxCombo; public int Multiplier; // 1/2/4/8 — 4콤보마다 증가 public int HP; // 기본 100, 미스 시 -10, 0이면 게임오버 public const int MaxHP = 100; public float HitRate; // notesHit / noteCount (0~1) public event Action OnScoreChanged; // score, combo, multiplier public event Action OnHPChanged; public event Action OnGameOver; public void SetNoteCount(int count); public void RegisterHit(); public void RegisterMiss(); } ``` ### 랭크 기준 (ResultsPanel) | 랭크 | HitRate | |---|---| | S | 95% 이상 | | A | 80% 이상 | | B | 65% 이상 | | C | 50% 이상 | | D | 50% 미만 | --- ## VRBeatsKit 주요 클래스 요약 | 클래스 | 역할 | |---|---| | `VR_BeatManager` | 싱글턴 — 큐브 스폰, 색상 설정, GameOver | | `VR_BeatCube` | 큐브 이동 + 히트/미스 판정 | | `VR_BeatCubeSpawneable` | 큐브 스폰 설정 (화살표/점, ColorSide) | | `VR_Saber` | 세이버 슬라이싱 (EzySlice 기반) | | `SpawnEventInfo` | 스폰 파라미터 (hitDirection, colorSide, position, speed) | | `AudioManager` | AudioSource + AudioMixer 래퍼 | | `VR_BeatSettings` | ScriptableObject — 색상, 속도, 멀티플라이어 한도 등 | ### SpawnEventInfo 구조 ```csharp public class SpawnEventInfo { public Direction hitDirection; // UpperLeft=0,Up=1,UpperRight=2,Left=3,Center=4,Right=5,LowerLeft=6,Down=7,LowerRight=8 public ColorSide colorSide; // Left, Right public bool useSpark; public Vector3 position; // -0.5~0.5 정규화 (PlayZone 기준) public Vector3 rotation; public float speed; public int speedMultiplier; } ``` --- ## 로컬 캐시 경로 ``` Application.temporaryCachePath/beatsaber/{songId}/ ├── {songId}.mp3 ├── Map_{songId}_normal.json ├── Map_{songId}_hard.json ├── Map_{songId}_expert.json └── Map_{songId}_expertplus.json ``` --- ## 알려진 주의사항 1. **Game 씬 직접 Play 주의**: `GameSession.SelectedSong == null`이면 `SongController`가 오류를 로그로 남기고 진행하지 않는다. 곡 플레이는 `Menu.unity`의 SongSelect에서 선택/다운로드 후 진입해야 한다. 2. **NAS 업로드**: 수동 multipart body (UploadHandlerRaw) 사용. Unity 기본 multipart는 DSM에서 401 오류. 3. **AudioType.MPEG**: MP3 로딩 시 `UnityWebRequestMultimedia.GetAudioClip(uri, AudioType.MPEG)` 사용. 4. **Unity `??` 연산자**: Unity Object에 `??` 쓰면 fake-null을 못 잡음. 반드시 `if (x == null)` 또는 `TryGetComponent` 사용. 5. **Build Settings 현재 상태**: 현재 등록 순서는 `Menu`, `BoxingStyle`, `SongCreator`, `SaberStyle`, `Game`이다. 예전 목표 설계의 `Intro`, `SongSelect`, `MapEditorScene`은 현재 Build Settings에 없다.