Unity
관람객이 시작 버튼을 누르면 로봇팔이 준비를 마치고, 2분 동안 사람과 로봇이 **진짜 에어하키 경기**를 벌인다면 어떨까요?
전*원··수정됨 2026.07.21

점수가 날 때마다 사용자 쪽은 30초, 로봇 쪽은 10초의 쿨다운이 걸려 센서 노이즈가 중복 득점을 만들지 않고, 마지막 골이 언제 터졌는지에 따라 종료 후 정리 시간까지 **동적으로 계산**된다면요. 이번 포스팅에서는 Unity를 UI 허브로, 별도 PC의 로봇 제어 프로그램을 UDP로, 시리얼 보드를 통한 센서·에어 제어를 단일 상태 머신에 통합한 인천 학생과학관 '에어하키' 콘텐츠의 기술적인 구현 과정을 공유합니다.

1. 시스템 아키텍처 개요
본 프로젝트는 크게 세 가지 단계로 구성됩니다.
Stopwatch 기반 상태 머신: Idle → Waiting(25초) → Playing(120초) → Ended(동적 대기) 네 단계를 System.Diagnostics.Stopwatch로 관리하여 Time.deltaTime 누적 오차를 제거
이중 출력 제어: 로봇팔 제어는 UDP(127.0.0.1:5555)로 별도 프로그램에 ASCII 명령('1'/'2'/'3')을 전달, 에어 테이블 제어는 시리얼[0xFA, 0x31/0x30])로 동일 보드에 송신
비트 다중화 수신: 시리얼 보드로부터 한 바이트 안에 **시작 버튼·유저 골·로봇 골 3개 신호를 비트 단위로 다중화**한 패킷을 받아 쿨다운 기반으로 처리
이 모든 과정은 Unity 2022 LTS의 단일 씬에서 GameManagerRobotManagerSerialManager 세 싱글톤이 이벤트와 코루틴으로 협업하며 동작합니다.
2. 주요 기술적 구현
2.1 Stopwatch 기반 정밀 상태 머신 (GameManager_AirHockey.cs)
게임 타이머는 전통적으로 Time.deltaTime을 누적하지만, 프레임 드롭이 누적되면 2분 게임이 2분 1초 또는 1분 59초로 어긋날 수 있습니다. GameManager는 대기·게임·유저 쿨다운·로봇 쿨다운·시작 쿨다운까지 총 다섯 개의 Stopwatch를 독립적으로 운영하여 운영체제 기준 실제 경과 시간을 측정하고, 매 프레임 UpdateWaitTimer() / UpdateGameTimer()가 해당 Stopwatch의 Elapsed.TotalSeconds를 읽어 UI를 갱신합니다. 특히 종료 후 복귀 시간은 waitTime = max(0, ROBOT_PUCK_RETRIEVAL_TIME - lastGoalRemaining) + ROBOT_SHUTDOWN_TIME 공식으로 **마지막 골 시점에 따라 동적 계산**되어, 경기 종료 직전 골이 터져도 로봇팔이 퍽을 회수하고 정리할 시간을 확보합니다.
2.2 한 바이트 3비트 다중화 센서 프로토콜 (SerialManager_AirHockey.cs)
시리얼 대역폭과 레이턴시를 최소화하기 위해 *[0xFA, 상태바이트] 단 2바이트**로 세 개의 독립 센서 신호를 전달하도록 프로토콜을 설계했습니다. 상태바이트는 bit0 = 시작 버튼, bit1 = 유저 골, bit2 = 로봇 골로 할당되어, ProcessReceivedBuffer()가 비트 마스크로 한 번에 분해하여 각각의 userScore / robotScore / startButton 디버그 필드에 반영합니다. 이를 GameManager.ProcessSerialInput()이 매 프레임 폴링하여 prevBit 와 비교한 **0→1 에지 감지**로 골 이벤트를 잡아내므로, 센서 비트가 1로 고정되어도 중복 발화 없이 단 한 번만 처리됩니다.
2.3 자동 재연결 이벤트 + 쿨다운 센서 리셋 (SerialManager + RobotManager 협업)
시리얼 USB가 순간적으로 빠졌다가 다시 꽂히는 상황에 대비해 SerialManager.SerialConnectionTask가 2초 간격 재시도 루프로 포트를 재확보하고, 성공 시 OnReconnected 이벤트를 발화합니다. RobotManager는 이 이벤트를 구독하여 **게임 진행 중Playing 상태)이면 즉시 [0xFA, 0x31] 에어 ON 명령을 재전송**해 게임이 끊김 없이 복구되도록 처리했습니다. 추가로 GameManager가 골 처리 직후 SerialManager.ResetButtonState()를 호출해 해당 비트를 0으로 강제 리셋하는 안전장치를 두어, 보드 펌웨어가 비트를 계속 1로 유지하는 경우에도 다음 골을 놓치지 않도록 보완했습니다.
3. 사용자 경험(UX) 최적화
비대칭 쿨다운 설계 : 사용자 골 30초 / 로봇 골 10초의 **비대칭 쿨다운**은 사용자 골 후 로봇팔이 퍽을 회수하는 긴 준비 시간을 자연스럽게 게임 플로우에 녹여낸 선택으로, 게임이 끊기는 느낌 없이 매끄럽게 이어지도록 했습니다.
외부 파일 기반 현장 튜닝 : 에어 OFF 유지 시간stopTime)을 **운영 PC 바탕화면의 Info.txt** 에서 StopTime=24 형식으로 읽어 와, 사이트별 로봇팔 속도 차이에 맞춰 재빌드 없이 텍스트 파일만 수정하면 되도록 설계했습니다.
110초 시점 에어 재송신 : 보드 측 타이머 만료로 경기 막판에 에어가 꺼지는 사고를 방지하기 위해 GameProcessCoroutine이 110초 시점에 [0xFA, 0x31]을 한 번 더 보내 마지막 10초 구간의 퍽 흐름을 보장했습니다.
상태별 차별 팝업 : EPOPUPTYPE enum(Wait / Win / Lose / None)으로 경기 단계마다 시각적 피드백을 명확히 분리해, 관람객이 대기 시간·승패 결과·로봇 정리 대기를 헷갈리지 않고 인지할 수 있도록 했습니다.
4. 마치며
이 프로젝트는 Unity 앱·로봇 제어 프로그램·시리얼 보드라는 **세 개의 비동기 시스템**을 단일한 경기 흐름 아래 맞물리게 하는 작업이었습니다. Stopwatch 기반 정밀 타이머, 비트 다중화 프로토콜, 재연결 이벤트 체인 같은 요소는 기술적으로도 흥미로웠지만, 관람객에게는 "버튼 한 번만 누르면 25초 뒤 로봇이 살아나 나와 진짜 경기를 한다"는 몰입감을 흔들림 없이 제공했다는 점에서 큰 의미가 있었습니다.
향후에는 경기 중 **골 리플레이 영상 저장·재생** 기능을 추가하고, 승률·최다 골·최단 경기 시간 같은 **세션 통계 대시보드**를 보조 모니터에 띄워 체험 이후의 여운까지 디자인하며, 현재 외부 파일로 운영되는 타이밍 상수들을 **인게임 운영자 UI**로 승격시켜 관리 편의성을 한층 끌어올릴 예정입니다.
읽기 도구
약 7분 읽기
이 글이 도움이 되었나요?