2026-05-04 15:49:20 +00:00
2026-05-05 00:33:37 +09:00
2026-05-05 00:33:37 +09:00
2026-04-28 15:12:18 +09:00
2026-05-05 00:33:37 +09:00
2026-04-14 18:02:07 +09:00
2026-04-14 18:02:07 +09:00
2026-04-16 04:58:10 +09:00
2026-05-04 15:49:20 +00:00

VR 장보기 시뮬레이션 (Shopping VR)

Unity 6 · XR Interaction Toolkit 기반 1인칭 VR 장보기 게임. 주문서대로 매장에서 상품을 골라 셀프 계산대에서 결제하고, NPC와 대화하며 시식·허기 관리까지 수행한다.


데모 영상

  • 메인 데모(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 대화·립싱크 시스템

  • 데이터 그래프: DialogGroupDialogNode(대사·제스처·표정·보이스·선택지) → DialogChoice → 다음 노드. 모두 ScriptableObject로 노드 그래프를 자산화.
  • DialogPlayer — Unity 6의 Awaitableawait 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. 카트 시스템


  • 해당 영역에 아이템이 들어오면, 아이템의 움직임이 없어질 때(혹은 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에서 적용. AnimatorUpdate 이후 동작이지만 같은 프레임의 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. 잡혀있는 동안엔 타이머 자체를 무효화. (자세한 흐름은 카트 시스템 섹션 참고)

6. XRGrabInteractable의 상태 스냅샷·복원이 카트 부착을 깨뜨림

  • 문제: XRGrabInteractable은 잡힐 때 부모/kinematic을 스냅샷하고 놓을 때 복원함. 카트에 kinematic 상태로 부착된 물건을 잡았다 놓으면, 놓는 순간 다시 카트 자식 + kinematic으로 복원되어 공중에 박제됨.
  • 해결: 부착 시 retainTransformParent = false로 부모 복원 차단 + selectExited에서 isKinematic = false를 직접 덮어씀.

향후 개선

  • 카트(RideController)에 담은 아이템 별도 인벤토리화
  • 대화 노드 진행 입력을 VR 컨트롤러 버튼으로 (현재 임시 1초 대기)
  • PlayerController의 임시 bool 플래그 → 상태 머신 전환
  • 주문서 종류 다양화 (난이도별 SO 추가)

라이선스

자체 구현 코드(Assets/02_Scripts/)에 대해서만 권리 주장. 외부 에셋은 각 에셋 라이선스를 따른다.

Description
장보기 시뮬레이션
Readme 4.3 GiB
Languages
C# 90.5%
ShaderLab 3.1%
HLSL 2.7%
GLSL 1.6%
C++ 1.5%
Other 0.6%