Unity
"내 몸의 몇 %까지 기계로 바꿀 수 있을까?" - Unity + Arduino로 만든 인천 인터랙티브 전시 개발기
김*겸··수정됨 2026.07.22
프로젝트 개요
"신체-기계 변환 경계 (Body Material To Boundary)" 는 인천에서 진행된 미디어아트 전시 프로젝트입니다. 관람객이 물리적 컨트롤러(레버, 회전 다이얼, 버튼)를 조작해 화면 속 사람 이미지를 실시간으로 기계 이미지로 변환(Dissolve)시키고, 최종적으로 "이건 사람인가, 로봇인가?"를 선택하는 체험형 콘텐츠입니다.
작업 구조
[물리 입력] Arduino → Serial 통신 → Unity
↓
┌──────────────────┐
│ ControlManager │ ← 시리얼 패킷 파싱
│ (레버/다이얼/버튼)│
└──────┬───────────┘
↓
┌────────────────────────────────┐
│ HumanToRobotTransform │
│ transformValue: 0(사람)~100(로봇)│
└────────────────┬───────────────┘
↓
┌──────────────────────┼──────────────────────┐
↓ ↓ ↓
Custom Dissolve Shader GaugeWithText(UI) SelectButtonHandler
(사람←→로봇 시각 전환) (게이지 + %) (최종 선택 + 결과)
핵심 스크립트 14개, 커스텀 셰이더 4개 구성이고, 크게 4가지 레이어로 나뉩니다.
1. 하드웨어 통신 레이어 (SerialManager + ControlManager)
SerialManager<T>: 제네릭 싱글톤 기반 시리얼 통신 프레임워크. 아두이노 COM 포트를 자동 탐색하고, 백그라운드 스레드에서 바이트 배열을 수신합니다.ControlManager: 수신된 패킷(0xFA헤더 + 커맨드 바이트)을 파싱해 게임 로직으로 변환합니다.command 1→ 레버 상하 (사람/로봇 토글)command 2→ 버튼 클릭 (확정)command 4→ 다이얼 오른쪽 (기계 비율 증가)command 8→ 다이얼 왼쪽 (기계 비율 감소)
2. 비주얼 레이어 (커스텀 Dissolve 셰이더)
HorizontalDissolve.shader: 왼쪽→오른쪽으로 이미지가 나타나는 셰이더 (로봇 이미지용). Edge Glow 효과 포함.HorizontalDissolveRight.shader: 왼쪽→오른쪽으로 사라지는 셰이더 (사람 이미지용).UV 리매핑으로 컨텐츠 영역만 정확히 Dissolve되도록 처리.
_DissolveAmount파라미터 하나로 사람↔로봇 전환을 실시간 제어.
3. 게임 로직 레이어
HumanToRobotTransform: 0100 값을 셰이더_DissolveAmount(01)로 정규화하여 양쪽 스프라이트에 동시 적용.SelectButtonHandler: 최종 선택 시 4단계 판정:
범위단계설명0~29%1단계 기능보조인공치아, 심박동기 등 의료 보조 장치30~59%2단계 부분대체기계 의수, 인공 장기 (사이보그)60~99%3단계 전신대체뇌와 심장만 남은 상태100%4단계 기억이식뇌까지 반도체로 교체 (마인드 업로딩)
4. 전시 운영 레이어
AllGameInit: 30초 자동 리셋 타이머 (다음 관람객을 위해)CountdownTimer: "10초 후 원래 화면으로.." 카운트다운 UICursorHider: 전시 모드용 마우스 커서 숨김DebugSpeedController: 개발/테스트 시 1/3 속도로 슬로우 모션
힘들었던 점
1. 시리얼 통신의 스레딩 지옥
Unity는 메인 스레드에서만 API를 호출할 수 있는데, 시리얼 포트 수신은 백그라운드 스레드에서 돌아갑니다. Queue<byte[]>로 스레드 세이프하게 데이터를 넘겨야 했고, lock과 TryDequeue 패턴으로 해결했지만 — 초반에 데이터가 씹히거나 중복 처리되는 버그를 잡는 데 상당히 고생했습니다.
2. Dissolve 셰이더의 UV 여백 문제
스프라이트 이미지에 여백(padding)이 있으면 Dissolve가 빈 공간부터 시작되어 "아무것도 안 변하는 것처럼" 보이는 현상이 발생합니다. _ContentStart/_ContentEnd 파라미터를 추가해 실제 컨텐츠 영역만 리매핑하는 방식으로 해결했는데, 이 문제를 인식하기까지가 오래 걸렸습니다. "셰이더는 되는데 왜 안 보이지?" 의 원인이 UV 여백이라니...
3. Reflection 해킹 — 급한 불을 끈 대가
ControlManager에서 SelectButtonHandler의 private 필드에 접근하기 위해 System.Reflection을 사용한 부분이 있습니다. _endBox, _endText, _gameInit을 리플렉션으로 꺼내 쓴 건 사실상 "public으로 바꿀 시간이 없어서" 였는데, 이런 코드는 나중에 필드명 하나만 바뀌어도 런타임에서 조용히 죽습니다. 전시 D-1에 이런 코드가 들어간다는 건... 전시 개발의 현실입니다.
4. 키보드 입력과 하드웨어 입력의 이중 경로
개발 중에는 키보드(1~5번 키)로 테스트하고, 실제 전시에서는 아두이노 하드웨어로 동작해야 합니다. 두 입력 경로가 동시에 존재하다 보니 "키보드로는 되는데 하드웨어로는 안 돼요" 같은 상황이 빈번했습니다. Update()에서 키보드 입력을 처리하는 코드와 ControlManager의 시리얼 입력이 동일한 함수를 호출하도록 통일하는 게 핵심이었습니다.
함정들
COM 포트 자동 탐색의 함정:
SerialPort.GetPortNames()가 블루투스 가상 포트까지 잡아버려서, 아두이노가 아닌 엉뚱한 포트에 연결되는 사고가 발생.UnusedPorts.txt로 제외 목록을 관리하는 우회법을 만들었습니다.30초 타이머 vs 관람객: 자동 리셋 타이머가 30초인데, 관람객이 결과를 충분히 읽기도 전에 화면이 초기화되는 문제. 결과 화면에는 별도로 10초 카운트다운 UI를 붙여 "곧 초기화됩니다"를 시각적으로 안내해야 했습니다.
DebugSpeedController 끄기 깜빡:
Time.timeScale = 1/3인 채로 빌드해서 전시장에서 모든 게 슬로모션으로 동작한 적이 있습니다.debugModebool 하나가 만든 참사.시리얼 깨진 한글 주석:
SerialManager.cs의 주석이 인코딩 문제로 깨져 있습니다. 다른 환경에서 작업한 코드를 가져올 때 UTF-8 인코딩 통일을 안 하면 이렇게 됩니다.
마무리
전시 개발은 일반 소프트웨어 개발과 다릅니다. "동작하면 됐다" 가 최우선이고, 리팩토링할 시간은 항상 부족합니다. Reflection 해킹이나 깨진 주석 같은 기술 부채가 남지만, 관람객이 다이얼을 돌리며 "나는 60%까지는 괜찮을 것 같은데..."라고 말하는 순간 — 그 모든 밤샘이 보상받습니다.
기술 스택: Unity 2D, C#, Arduino Serial Communication, Custom HLSL/CG Shader, New Input System, Singleton Pattern, TextMeshPro
읽기 도구
약 7분 읽기
이 글이 도움이 되었나요?