Files
BeatSaber/HANDOFF.md
T
2026-05-26 17:18:02 +09:00

17 KiB

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 역할을 한다.

현재 구현된 주요 흐름

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가 연결되어 있다.
    • VRPointerSetupVR_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
git clone https://whdwo798.synology.me/whdwo798/BeatSaber.git

단, 이 저장소는 구버전 프로젝트다. 새 프로젝트는 VRBeatsKit을 기반으로 재구성하며, 위 저장소의 Assets/Script/ 등 핵심 스크립트를 참고/이식하는 용도로 사용한다.


Git 설정 (새 프로젝트)

새 Unity 프로젝트를 생성한 뒤 가장 먼저 git을 초기화하고 파일을 커밋해야 한다.
Claude Code는 대화 시작 시 git status / git log를 자동으로 읽어 컨텍스트를 파악한다.
커밋이 없으면 Claude가 변경 이력을 추적할 수 없다.

초기화 순서

# 새 프로젝트 루트에서
git init
git remote add origin <GitHub 저장소 URL>

.gitignore

기존 프로젝트의 .gitignore를 복사하면 된다. 핵심 규칙:

# Unity 표준
/Library/
/Temp/
/Obj/
/Build/
/Builds/
/Logs/
/UserSettings/

# NAS 비밀번호 — 절대 커밋 금지
/Assets/StreamingAssets/nas_config.json
/Assets/StreamingAssets/nas_config.json.meta

첫 커밋

파일 복사 완료 후:

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 핵심

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.csVRBeats.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에 추가 필수.

{
  "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 형식

{
  "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)

{
  "target": [
    { "time": 1.23, "position": 1, "lineLayer": 1, "colorType": 0, "cutDirection": 1 }
  ]
}

Beat Sage API

  • Base URL: https://beatsage.com
  • 흐름: POST /createGET /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 명세

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<int, int, int> OnScoreChanged;  // score, combo, multiplier
    public event Action<int> 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 구조

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에 없다.