feat: SongCreator 씬 완성 — Beat Sage URL 지원, info.dat 메타데이터 자동 추출
- BeatSageUploader: audio_url 지원(UploadFromUrl), PollAndDownload 공통화, ZIP 500 오류 3회 재시도 - BeatSageConverter: info.dat 파싱(SongMetadata), BPM 자동 감지 → 노트 타이밍 변환에 적용 - SongCreatorManager: title/BPM 필수 입력 제거, 난이도 4개 자동 선택, GenerateFlowFromUrl 버그 수정 - NasPublisher: audioPath null 허용(URL 흐름에서 로컬 파일 없는 경우 스킵) - .gitignore/.gitattributes 초기 설정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+434
@@ -0,0 +1,434 @@
|
||||
# VR Beat Saber 프로젝트 인수인계 문서
|
||||
|
||||
## 개요
|
||||
|
||||
Meta Quest용 VR Beat Saber 클론. Beat Sage API로 노래를 자동 채보하고, Synology NAS에 업로드/다운로드한다.
|
||||
이 문서는 **기존 프로젝트(구버전)를 VRBeatsKit 기반 새 프로젝트로 이전**하기 위한 인수인계 자료다.
|
||||
|
||||
---
|
||||
|
||||
## 기존 프로젝트 소스 코드
|
||||
|
||||
**기존 프로젝트 전체 파일은 아래 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 <GitHub 저장소 URL>
|
||||
```
|
||||
|
||||
### .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개 전부 생성. |
|
||||
| `IntroManager.cs` | 인트로 → SongSelect 자동 전환 |
|
||||
| `DesktopUIMode.cs` | 에디터에서 XR 없이 테스트할 때 UI Raycaster 교체 |
|
||||
| `ScoreManager.cs` | 싱글턴 — Score/Combo/MaxCombo/Multiplier/HP |
|
||||
| `ScoreHUD.cs` | 점수/콤보 TMP + HP 슬라이더 |
|
||||
| `ResultsPanel.cs` | 결과 화면 (CLEAR/FAILED, 랭크 S~D) |
|
||||
| `SaberGlow.cs` | 검 끝 포인트 라이트 제어 |
|
||||
| `SaberSkinSelector.cs` | 검 외형 선택 (PlayerPrefs 저장) |
|
||||
| `CacheManager.cs` | 캐시 정리 유틸 |
|
||||
|
||||
### 새로 작성이 필요한 스크립트
|
||||
|
||||
| 파일 | 내용 |
|
||||
|---|---|
|
||||
| `Spawner.cs` | **VRBeatsKit 통합 버전** — 아래 상세 참고 |
|
||||
|
||||
---
|
||||
|
||||
## Spawner.cs 새 버전 작성 방법 (핵심)
|
||||
|
||||
기존 Spawner는 `cubePrefabs[]`(GameObject)를 Instantiate했다.
|
||||
새 버전은 `VR_BeatManager.instance.Spawn(Spawneable, SpawnEventInfo)`를 호출한다.
|
||||
|
||||
### 변경 포인트
|
||||
|
||||
**1. AudioSource 획득 방식 변경**
|
||||
```csharp
|
||||
// 기존: [SerializeField] AudioSource audioSource;
|
||||
// 신규: VRBeatsKit AudioManager에서 가져옴
|
||||
void Awake()
|
||||
{
|
||||
var am = FindObjectOfType<VRBeats.AudioManager>();
|
||||
if (am != null) audioSource = am.GetComponent<AudioSource>();
|
||||
}
|
||||
```
|
||||
|
||||
**2. 큐브 프리팹 타입 변경**
|
||||
```csharp
|
||||
// 기존: public GameObject[] cubePrefabs; (RED=0, BLUE=1)
|
||||
// 신규:
|
||||
public VRBeats.Spawneable redCubePrefab; // colorType == 0
|
||||
public VRBeats.Spawneable blueCubePrefab; // colorType == 1
|
||||
```
|
||||
|
||||
**3. SpawnNote() 전면 교체**
|
||||
```csharp
|
||||
// Beat Saber cutDirection(0~8) → VRBeats Direction 매핑
|
||||
private static readonly VRBeats.Direction[] DirMap =
|
||||
{
|
||||
VRBeats.Direction.Up, // 0
|
||||
VRBeats.Direction.Down, // 1
|
||||
VRBeats.Direction.Left, // 2
|
||||
VRBeats.Direction.Right, // 3
|
||||
VRBeats.Direction.UpperLeft, // 4
|
||||
VRBeats.Direction.UpperRight, // 5
|
||||
VRBeats.Direction.LowerLeft, // 6
|
||||
VRBeats.Direction.LowerRight, // 7
|
||||
VRBeats.Direction.Center // 8 (dot)
|
||||
};
|
||||
|
||||
private void SpawnNote(NoteData data)
|
||||
{
|
||||
if (VRBeats.VR_BeatManager.instance == null) return;
|
||||
|
||||
VRBeats.Spawneable prefab = data.colorType == 0 ? redCubePrefab : blueCubePrefab;
|
||||
if (prefab == null) return;
|
||||
|
||||
var info = new VRBeats.SpawnEventInfo
|
||||
{
|
||||
colorSide = data.colorType == 0 ? VRBeats.ColorSide.Right : VRBeats.ColorSide.Left,
|
||||
hitDirection = DirMap[Mathf.Clamp(data.cutDirection, 0, 8)],
|
||||
// position: 0~3열 → -0.5~0.5, 0~2행 → -0.5~0.5
|
||||
position = new Vector3(
|
||||
(data.position / 3.0f) - 0.5f,
|
||||
(data.lineLayer / 2.0f) - 0.5f,
|
||||
0f),
|
||||
speed = noteSpeed,
|
||||
useSpark = true
|
||||
};
|
||||
|
||||
VRBeats.VR_BeatManager.instance.Spawn(prefab, info);
|
||||
}
|
||||
```
|
||||
|
||||
**4. spawnPoints[] 제거** — VR_BeatManager가 PlayZone 기반으로 위치 계산하므로 불필요.
|
||||
|
||||
**5. 나머지 로직은 기존과 동일** — InitGame(), CountDown(), LoadAudioClip(), ShowResults() 등
|
||||
|
||||
---
|
||||
|
||||
## ScoreManager 충돌 없음
|
||||
|
||||
- 우리 `ScoreManager.cs` → 전역 네임스페이스
|
||||
- VRBeatsKit `Scripts/UI/ScoreManager.cs` → `namespace VRBeats`
|
||||
- 두 파일이 공존 가능. 우리 ScoreManager를 그대로 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## 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<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 구조
|
||||
```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` → SongSelect로 튕김. 반드시 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 등록 필수**: Intro(0), SongSelect(1), Game(2), SongCreator(3), MapEditorScene(4)
|
||||
Reference in New Issue
Block a user