--- id: wiki-2026-0508-beat-saber title: Beat Saber category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Beat Saber, VR Rhythm Game] duplicate_of: none source_trust_level: A confidence_score: 0.85 verification_status: applied tags: [vr, game-architecture, rhythm-game, unity, ecs] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: csharp framework: unity,unity-dots,openxr --- # Beat Saber ## 매 한 줄 > **"매 VR rhythm game 의 architecture 의 reference — 90Hz minimum framerate · sub-20ms motion-to-photon · ECS-style note pool"**. 2018 Beat Games (Meta acq 2019) release, 2026 의 Quest 3/Vision Pro 의 cross-platform mainstream, "framerate 의 holy 의 above 의 nothing" 의 architecture 의 lesson. ## 매 핵심 ### 매 architecture 제약 - **Framerate floor**: 90 fps (Quest 2) · 120 fps (Quest 3 · Vision Pro). - **Motion-to-photon**: < 20 ms. - **GC 의 hostile**: 매 frame 의 spike 의 nausea 의 cause → object pool · Burst · Job System. - **Determinism**: scoring 의 reproducible — 매 input · note 의 fixed seed. ### 매 component - **Beatmap loader**: `.dat` JSON parse → preallocated note buffer. - **Note spawner**: 매 future 6 sec 의 lookahead, pool 에서 의 pull. - **Saber controller**: hand pose tracking + velocity smoothing. - **Cut detector**: plane intersection · direction match · score. - **Audio sync**: NJS (Note Jump Speed) + offset 의 calibrate. ### 매 응용 1. 매 fitness app (Supernatural, FitXR) 의 rhythm pattern 의 inherit. 2. Trainer/simulator (medical · drill) 의 timing-critical UX. 3. Education app (language drill · typing tutor) 의 VR variant. ## 💻 패턴 ### Unity DOTS: note spawn 의 zero-alloc ```csharp [BurstCompile] public partial struct NoteSpawnSystem : ISystem { public void OnUpdate(ref SystemState state) { var beatmap = SystemAPI.GetSingleton(); var time = SystemAPI.Time.ElapsedTime; var ecb = new EntityCommandBuffer(Allocator.Temp); for (int i = beatmap.NextIndex; i < beatmap.Notes.Length; i++) { var n = beatmap.Notes[i]; if (n.Time - time > LookaheadSeconds) break; var e = ecb.Instantiate(beatmap.NotePrefab); ecb.SetComponent(e, new Translation { Value = n.SpawnPosition }); ecb.SetComponent(e, new NoteData { CutDirection = n.Direction, HitTime = n.Time }); beatmap.NextIndex = i + 1; } ecb.Playback(state.EntityManager); } } ``` ### Saber-cut detector (plane intersection) ```csharp public bool TryCut(Vector3 saberTip, Vector3 saberBase, Vector3 saberVelocity, NoteData note, out CutResult result) { var plane = new Plane(saberVelocity.normalized, saberBase); if (!plane.Raycast(new Ray(note.Position, note.Forward), out float enter)) { result = default; return false; } float speed = saberVelocity.magnitude; float angle = Vector3.Angle(saberVelocity, note.RequiredCutDirection); float accuracy = 1f - Mathf.Clamp01(angle / 60f); result = new CutResult { Score = Mathf.RoundToInt(speed * accuracy * 115f), IsValid = speed > 2f && angle < 60f, }; return result.IsValid; } ``` ### Audio-visual sync (NJS-based) ```csharp // Note 의 spawn position 의 calc float jumpDist = njs * (60f / bpm) * halfJumpDuration * 2f; Vector3 spawnPos = playerPosition + Vector3.forward * jumpDist; // Note 의 frame 마다 의 lerp float t = (Time.timeAsDouble - spawnTime) / (hitTime - spawnTime); note.transform.position = Vector3.Lerp(spawnPos, hitPos, (float)t); ``` ### Object pool (zero-GC frame) ```csharp public class NotePool { readonly Stack pool = new(256); readonly Note prefab; public Note Get() => pool.Count > 0 ? pool.Pop() : Object.Instantiate(prefab); public void Return(Note n) { n.gameObject.SetActive(false); pool.Push(n); } } ``` ### OpenXR foveated rendering hint (Quest 3) ```csharp var feature = OpenXRSettings.Instance.GetFeature(); feature.foveatedRenderingLevel = FoveatedRenderingLevel.High; feature.useDynamicFoveatedRendering = true; // eye-tracked on Quest 3 ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | < 100 active note · prototype | MonoBehaviour + pool | | > 200 active note · production | DOTS/ECS + Burst | | Cross-platform (PCVR + Quest) | Unity URP + multi-quality preset | | Modding support | Open beatmap format (`.dat`) + PluginLoader | | 매 90fps 미달 의 perf budget | Foveation · LOD · GPU instancing | **기본값**: Unity DOTS + URP + OpenXR — 매 Quest 3 의 baseline 의 fit. ## 🔗 Graph - 응용: [[bitECS와_SharedArrayBuffer의_실제_코드_통합]] - Adjacent: [[Object Pool]] ## 🤖 LLM 활용 **언제**: beatmap 의 procedural generation, cut-direction pattern 의 difficulty curve 의 tune, NJS · offset 의 starter value 의 suggest. **언제 X**: 매 hand-crafted choreography (top mapper 의 art form) — LLM 의 generate 의 bland. ## ❌ 안티패턴 - **GC 의 frame 의 allocate**: 매 90fps 의 break → motion sickness. - **Animation curve 의 audio sync**: dt 의 drift → pop. NJS-based linear lerp 의 use. - **Saber 의 trigger collider**: physics step 의 sub-frame miss — manual raycast/plane 의 use. - **Per-note GameObject Instantiate**: pool 의 mandatory. ## 🧪 검증 / 중복 - Verified (Beat Games postmortem GDC 2019, Unity DOTS 1.3 docs, OpenXR 1.1 spec, BSMG modding wiki). - 신뢰도 A-. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — VR architecture constraints + DOTS spawner + cut detector |