Unity
한 편의 롤러코스터 영상을 보는 동안 의자가 영상과 **1프레임도 어긋나지 않게** 기울고 진동한다면 어떨까요?
전*원··수정됨 2026.07.21

운영자가 다른 PC에서 버튼 하나를 누르면 관객 PC의 영상이 페이드아웃 없이 시작되고, 벨트가 풀리면 색깔이 바뀌고, 장면이 끝나면 의자가 스스로 홈 포지션으로 돌아온다면요. 이번 포스팅에서는 두 대의 PC와 의자 제어 보드를 UDP + 시리얼 + JSON 타임라인으로 묶어 하나의 극장 경험으로 완성한 인천 학생과학관 '4D 체험관'의 기술적인 구현 과정을 공유합니다.

1. 시스템 아키텍처 개요
본 프로젝트는 크게 세 가지 단계로 구성됩니다.
듀얼 PC 협업: 운영자 PC와 관객 PC가 LAN 상에서 UDP 포트 5555/5556으로 양방향 통신하여 영상 선택·재생·정지·볼륨 등의 명령과 좌석 상태·영상 종료 등의 상태를 교환
타임라인 기반 모션 이벤트: 영상 파일 옆에 놓인 .json 타임라인을 런타임에 파싱하여 진동·기울기·방향 이벤트를 시작 시각 기준으로 정렬하고, 영상 재생 시점부터 매 프레임 트리거
의자 보드 시리얼 제어: 파싱된 이벤트를 SF1000, SVF310, SH0020 같은 텍스트 명령으로 변환해 9600bps 시리얼 포트로 의자 보드에 전송
이 모든 과정은 Unity 2022 LTS에서 운영자 씬과 관객 씬이 별도 빌드로 생성되어 각각 전용 PC에 배포되는 이원화 아키텍처로 구성됩니다.
2. 주요 기술적 구현
2.1 양방향 UDP 명령/상태 프로토콜 (UDPManager_4DMovies_Controller·Player.cs)
두 PC 사이의 통신은 1바이트 헤더 기반 바이너리 프로토콜로 설계했습니다. 운영자 → 관객 방향은 [0xFA, 영상번호] 2바이트로 영상 재생을, [0xFA, 105, 볼륨] 3바이트로 볼륨 설정을, 관객 → 운영자 방향은 [0xFB]로 영상 종료, [0xFA, 그룹번호, 상태ASCII] 가변 길이로 좌석 상태를 실시간 보고합니다. 양쪽 UDPManager 모두 백그라운드 스레드에서 수신 루프를 돌리고 큐를 통해 메인 스레드에 전달하는 구조로 Unity의 단일 스레드 제약을 우회했습니다.
2.2 Stopwatch 기반 정밀 타임라인 엔진 (InGamePanelManager_4DMovies_Player.cs)
모션 이벤트의 타이밍 기준을 VideoPlayer.time 대신 System.Diagnostics.Stopwatch로 전환한 것이 이 콘텐츠의 핵심 설계입니다. 프레임 드롭이 발생해도 Stopwatch.Elapsed.TotalSeconds는 실제 경과 시간을 정확히 반환하므로, 매 프레임 while (events[nextIndex].startTime <= t) 루프가 밀린 이벤트들을 한 번에 소화해 영상과 의자의 싱크를 유지합니다. 타임라인 JSON은 영상과 같은 폴더Desktop\Movies\*.json)에 두어 영상 교체만으로 이벤트를 바꿀 수 있도록 했고, VibrationLevel만 채워져 있으면 VibrationAmplitude를 자동 보정하는 하위 호환 레이어까지 포함했습니다.
```json
{
"Video": "롤러코스터.mp4",
"Fps": 29.97,
"Timeline": [
{ "StartTimeLine": 2.0, "Direction": "F", "TiltLevel1": 6,
"Vibration": "Y", "VibrationAmplitude": 3, "VibrationTime": 0.2 }
]
}
```
2.3 의자 보드 시리얼 명령 매핑 (SerialManager_4DMovies_Player.cs)
타임라인의 추상적 이벤트를 의자 보드가 이해하는 줄바꿈 없는 텍스트 명령으로 변환하는 계층입니다. 기울기 이벤트는 SendMoveTo('F', 1, 0, 0) → SF1000으로, 진동 이벤트는 SendVibration(Front, 3, 1, 1) → SVF310으로, 영상 종료 시에는 SendMoveHome(2.0) → SH0020으로 직렬화합니다. 모든 송신은 50ms 간격의 큐를 통해 순차 전송되어 보드의 입력 버퍼 오버플로우를 방지하며, 보드가 주기적으로 응답하는 G 헤더 좌석 상태는 다시 UDP로 포워딩되어 운영자 PC의 시각화 UI에 반영됩니다.
3. 사용자 경험(UX) 최적화
부드러운 화면 전환 연출 : 영상 시작·종료 시점에 fadeCanvasGroup 으로 0.5초 페이드와 1~2초 블랙 유지를 삽입해, 영상 교체·의자 이동 같은 기계적 순간이 관객 시야에 드러나지 않도록 가렸습니다.
벨트 글리치 필터링 : 센서가 순간적으로 "미착용"을 튀게 보고할 수 있어, seatbeltGlitchThreshold = 3으로 연속 3회 이상 감지될 때만 진짜 이탈로 처리해 오탐으로 인한 갑작스런 정지를 방지했습니다.
안전한 재개 판정 : IsSafeToResume()이 영상 직전에 캡처한 초기 착용 상태initialGroupSeatMasks)를 기준으로 재개 시점에 동일 좌석의 착용 여부를 검증하여, 누군가 벨트를 풀고 일어난 상태에서 재개 버튼이 눌리는 사고를 원천 차단했습니다.
외부 폴더 런타임 로드 : 영상과 JSON을 Unity 빌드에 포함하지 않고 운영 PC의 지정 폴더에서 런타임에 직접 읽어, 현장에서 새 영상을 추가하거나 교체할 때 재빌드 없이 파일 복사만으로 콘텐츠를 갱신할 수 있도록 했습니다.
4. 마치며
이 프로젝트는 두 대의 PC와 의자 보드라는 세 주체를 단일한 시간축 위에서 협력시키는 작업이었습니다. UDP 프로토콜 설계, Stopwatch 기반 타임라인 엔진, 시리얼 명령 큐까지 기술적으로도 흥미로운 요소가 많았지만, 관객에게는 "영상이 시작되면 의자가 정확히 그 순간 움직이기 시작한다"는 당연해 보이는 몰입감을 돌려주었다는 점에서 큰 의미가 있었습니다.
향후에는 현재 수기로 편집하는 타임라인 JSON을 시각적 편집 툴로 대체하여 기획자가 영상을 보며 드래그만으로 이벤트를 배치할 수 있도록 하고, Stopwatch와 VideoPlayer.time을 연동한 일시정지·재개 정밀 동기화 레이어를 추가하여 중단 후 재개 시에도 프레임 단위로 완벽하게 싱크가 맞도록 개선할 예정입니다.
읽기 도구
약 6분 읽기
이 글이 도움이 되었나요?