Files
Shopping_UnityVR/README.md
2026-05-04 15:47:05 +00:00

192 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VR 장보기 시뮬레이션 (Shopping VR)
> Unity 6 · XR Interaction Toolkit 기반 1인칭 VR 장보기 게임. 주문서대로 매장에서 상품을 골라 셀프 계산대에서 결제하고, NPC와 대화하며 시식·허기 관리까지 수행한다.
<!-- 헤더용 GIF/이미지 자리 (TODO: 매장 입장 → 주문서 펼치기 → 스캔 → 결제 4컷 GIF) -->
<!-- ![preview](docs/preview.gif) -->
---
## 데모 영상
<!-- TODO: 유튜브/구글드라이브 링크 또는 GIF -->
- 메인 데모(2분): _업로드 예정_
- 하이라이트(30초): _업로드 예정_
- 빌드(.apk, Quest 사이드로드): _업로드 예정_
---
## 프로젝트 정보
| | |
|---|---|
| **개발 기간** | _YYYY.MM ~ YYYY.MM_ |
| **인원** | 1인 (기획·개발·일부 에셋 통합) |
| **플랫폼** | Meta Quest (Android XR / OpenXR), PC VR |
| **엔진** | Unity 6000.3.9f1 · URP 17.3 |
| **주요 패키지** | XR Interaction Toolkit 3.3, XR Hands 1.7, OpenXR 1.16, Oculus XR 4.5 |
---
## 게임 플로우
1. **GameStartScene** — 시작 화면에서 게임 진입
2. **GameScene (매장)** — 좌측 컨트롤러 Primary 버튼으로 주문서 펼치기
3. **상품 픽업** — XR Direct Interactor로 상품을 잡고 손에 든다 (Hover 하이라이트로 피드백)
4. **셀프 계산대** — 바코드 스캔 영역에 상품을 가져가면 항목이 행으로 추가됨, 합계 자동 갱신
5. **결제**`PlayerWallet`에서 차감 → 주문서 충족 검사 → Clear / Missing 패널 분기
6. **카트** — 카트 영역에 떨어뜨려 둔 상품은 일정 시간 후 자동으로 카트에 정착(부착)되어 카트와 함께 움직인다. 다시 손으로 집으면 분리.
7. **부가 시스템** — NPC 대화·시식으로 허기 회복, 시간이 지나면 허기가 깎이고 굶주리면 이동 속도 저하
---
## 핵심 기능
### 1. 쇼핑 주문 미션 시스템
- `ItemData`(ScriptableObject) — 상품 ID·브랜드·`ItemCategory`(과일/유제품/스낵 등)·`ProductGroup`(브랜드별 구체 상품) 분리. **카테고리는 매대 분류용, 그룹은 미션 매칭용**으로 의도적으로 두 단계로 나눔.
- `ShoppingOrderList` — 주문서를 SO로 데이터화. 미션마다 다른 주문서를 에셋만 교체해 재사용.
- `PlayerController.ShoppingOrderMissionClearCheck` — 결제 시 구매 항목을 `ProductGroup`으로 집계 후 부족분을 (group, shortage) 튜플 리스트로 반환. 부족분 0이면 클리어.
### 2. 셀프 계산대 (`CheckoutMachine`, `BarcodeScaner`)
- `Physics.OverlapBoxNonAlloc`로 스캔 영역 안의 콜라이더를 GC 부담 없이 수집한 뒤, 영역 중심에 가장 가까운 `ItemInstance` 한 개만 스캔.
- 결제 행은 `Dictionary<string, CheckoutProductionRow>`로 동일 상품 묶음·수량 카운트를 O(1)로 처리.
- UI 행 추가 시 **`OnEnable` 지연 트릭**: 행을 비활성 상태로 인스턴스화 → `LayoutRebuilder.ForceRebuildLayoutImmediate`로 레이아웃 확정 → 활성화. 활성 직후 첫 프레임에 RectTransform이 0인 채로 OnEnable이 도는 문제를 피한다.
### 3. NPC 대화·립싱크 시스템
- **데이터 그래프**: `DialogGroup``DialogNode`(대사·제스처·표정·보이스·선택지) → `DialogChoice` → 다음 노드. 모두 ScriptableObject로 노드 그래프를 자산화.
- `DialogPlayer` — Unity 6의 `Awaitable``await PlayNode → await WaitForChoice → 다음 노드` 흐름을 코루틴 없이 비동기 직렬화. 대화 종료 시 초기 제스처/표정 상태를 `CrossFade`로 복원.
- **플레이어 향해 회전** — 대화 중 NPC가 카메라 방향으로 부드럽게 회전, 종료 시 원래 회전으로 복귀.
- **블렌드셰이프 립싱크 (`LipSync`)** — `AudioSource.GetOutputData`로 RMS amplitude 계산 → 0~1로 매핑 → 입 관련 블렌드셰이프 7종에 가중치 분배. **`LateUpdate`에서 적용**해 같은 프레임에 Animator가 0으로 덮어쓴 값을 다시 씌우는 방식으로 표정 애니메이션과 충돌 없이 공존.
### 4. 플레이어 시스템
- `PlayerHunger` — 시간 경과로 허기 게이지 감소. 0이 되면 `ContinuousMoveProvider.moveSpeed``_starvedMoveSpeed`로 교체, 시식으로 회복 시 원본 속도 복구. UI는 `Action<float, float>` 이벤트로 게이지 바인딩.
- `PlayerWallet` — 결제 시 잔고 차감, 부족 시 `false` 반환해 결제 자체를 막음.
- `InputManager` — XR 컨트롤러 버튼 이벤트(`XRLeftControllerPrimaryButton_Event` 등)를 게임 로직 측에서 구독.
### 5. 카트 시스템
<img src="/readme/cart1.png" width="400"><br><br><br>
<img src="/readme/cart2.png" width="200"><br>
- 해당 영역에 아이템이 들어오면, 아이템의 움직임이 없어질 때(혹은 0.3초가 지났을 때) 물리영향을 끄고 카트의 움직임을 ParentConstraint 로 추적한다.
### 6. 씬 전환 라이프사이클
- `ITransScenePossible` 인터페이스 — 씬 로드 직후 `OnSceneLoaded()`를 일괄 호출해 **씬을 가로지르는 매니저(GameManager 등)가 새 씬의 매니저(GameSceneUIManager)와 다시 결선**되도록 함. 싱글톤이 죽지 않은 채 새 씬의 UI 참조만 갱신.
---
## 아키텍처
```
Managers (DontDestroyOnLoad, 싱글톤)
├─ GameManager 전역 상태·씬 매니저 결선
├─ InputManager XR 컨트롤러 입력 이벤트 허브
├─ SoundManager BGM / SE
└─ SceneLoadManager ITransScenePossible 알림 디스패치
Scene-local (GameScene)
├─ GameSceneUIManager HUD·패널 컨트롤
├─ PlayerController 주문서 토글, 결제 진입점
├─ PlayerWallet / PlayerHunger
├─ RideController + RideDetectionZone 카트 (ParentConstraint로 플레이어 종속)
└─ CheckoutMachine ← BarcodeScaner ← ItemInstance(ItemData SO)
Data (ScriptableObject)
├─ ItemData / ShoppingOrderList / ShoppingOrderEntry
├─ DialogGroup / DialogNode / DialogChoice / VoiceClip
└─ CharacterData / ExpressionData / GestureData / BGMClip
```
---
## 폴더 구조
```
Assets/
├─ 01_Scenes/MyProject/ GameStartScene, GameScene
├─ 02_Scripts/ 자체 구현 코드 (네임스페이스 VRShopping.*)
│ ├─ Managers/ GameManager, InputManager, SoundManager, SceneLoadManager...
│ ├─ Player/ PlayerController, PlayerHunger, PlayerWallet, RideController
│ ├─ Shopping/ BarcodeScaner, CheckoutMachine
│ ├─ Communication/ Dialog/, Voice/ (LipSync, DialogPlayer, CharacterVoiceObject)
│ ├─ Item/ ItemData, ItemInstance, TastingSample
│ ├─ Interact/ ItemHoverHighlight, ItemInfoOnGrab, DialogInteractable
│ ├─ Data/ ScriptableObject 정의
│ └─ UI/ HUD·패널 (HungerHud, GameTimerHud, ChoiceHud, ClearPanel...)
├─ 03_Models/ ~ 10_Audio/ 아트·오디오 에셋
└─ 99_Settings/ XR / URP / Input 설정
```
---
## 사용한 외부 에셋
| 분류 | 에셋 | 용도 |
|---|---|---|
| XR | XR Interaction Toolkit, XR Hands, OpenXR, Oculus XR | 컨트롤러·핸드 인터랙션 기반 |
| XR 확장 | MikeNspired XRI Starter Kit | Climbing, DistanceGrab, HandPoser |
| 캐릭터 | BoZo Modular Anime Characters | NPC 모델·블렌드셰이프 |
| 의상 시뮬 | Magica Cloth 2 | 옷·머리카락 물리 |
| 셰이딩 | RealToon, Umbra Soft Shadows, BadDog AreaLight | 툰 셰이딩·소프트 섀도우 |
| 인터랙션 피드백 | HighlightPlus, Outline Plus | Hover/Grab 외곽선 |
| VFX | CartoonVFX9X | 스캔·결제 이펙트 |
> 자체 구현은 `Assets/02_Scripts/` (네임스페이스 `VRShopping.*`).
---
## 빌드 / 실행
### 요구 사항
- Unity **6000.3.9f1**
- (Quest 빌드) Android Build Support, OpenXR Loader: Meta Quest
### 실행
1. 저장소 클론 후 Unity Hub에서 프로젝트 열기
2. `Assets/01_Scenes/MyProject/GameStartScene.unity` 열기
3. PC: Quest Link / Air Link 연결 후 Play
4. Android 빌드: Build Settings → Android → Build, Quest에 사이드로드
---
## 트러블슈팅 / 학습 포인트
### 1. 립싱크와 표정 애니메이션 충돌
- **문제**: `Animator`가 표정 레이어에서 매 프레임 블렌드셰이프 가중치를 0으로 덮어써 립싱크가 동작하지 않음.
- **해결**: 립싱크를 `LateUpdate`에서 적용. `Animator``Update` 이후 동작이지만 같은 프레임의 `LateUpdate`에서 다시 가중치를 씌우는 순서로 충돌을 회피.
### 2. UI 행 추가 직후 RectTransform 0 문제
- **문제**: `Instantiate` 후 활성 상태에서 바로 `OnEnable`이 돌면 부모 레이아웃이 아직 갱신 전이라 사이즈 0으로 잡힘.
- **해결**: 비활성 상태로 생성 → `LayoutRebuilder.ForceRebuildLayoutImmediate` → 활성. 첫 프레임 깜빡임 제거.
### 3. 씬 전환 시 매니저 참조 끊김
- **문제**: `DontDestroyOnLoad` 매니저가 새 씬의 UI 매니저를 못 찾음.
- **해결**: `ITransScenePossible` 인터페이스로 통일 → `SceneLoadManager`가 씬 로드 콜백에서 일괄 `OnSceneLoaded()` 호출 → 각자 필요한 씬-로컬 참조를 다시 잡음.
### 4. 스캔 영역 다중 상품 처리
- **문제**: 스캔 박스 안에 여러 상품이 들어오면 한 번에 다 결제됨.
- **해결**: `OverlapBoxNonAlloc` 결과 중 박스 중심에 **가장 가까운 한 개**만 스캔 대상으로 선택.
### 5. 카트에 담은 물건이 카트와 따로 노는 문제
- **문제**: VR 카트가 움직이면 안에 든 물건이 충돌·관성으로 튀어나옴. 그렇다고 진입 즉시 부착하면 던져 넣을 때 운동량으로 다시 빠져나감.
- **해결**: **속도 임계값(`_settleVelocity`) 이하 상태가 `_settleDuration` 동안 연속 유지**되면 카트 자식으로 reparent + `isKinematic = true`. 잡혀있는 동안엔 타이머 자체를 무효화. (자세한 흐름은 [카트 시스템 섹션](#5-카트-시스템-ridecontroller-ridedetectionzone) 참고)
### 6. XRGrabInteractable의 상태 스냅샷·복원이 카트 부착을 깨뜨림
- **문제**: `XRGrabInteractable`은 잡힐 때 부모/kinematic을 스냅샷하고 놓을 때 복원함. 카트에 kinematic 상태로 부착된 물건을 잡았다 놓으면, **놓는 순간 다시 카트 자식 + kinematic으로 복원되어 공중에 박제**됨.
- **해결**: 부착 시 `retainTransformParent = false`로 부모 복원 차단 + `selectExited`에서 `isKinematic = false`를 직접 덮어씀.
---
## 향후 개선
- [ ] 카트(`RideController`)에 담은 아이템 별도 인벤토리화
- [ ] 대화 노드 진행 입력을 VR 컨트롤러 버튼으로 (현재 임시 1초 대기)
- [ ] PlayerController의 임시 bool 플래그 → 상태 머신 전환
- [ ] 주문서 종류 다양화 (난이도별 SO 추가)
---
## 라이선스
자체 구현 코드(`Assets/02_Scripts/`)에 대해서만 권리 주장. 외부 에셋은 각 에셋 라이선스를 따른다.