paiams

Go Back

StarCraft vision overlay


스타크래프트의 방어 건물은 공격 범위가 화면에 표시되지 않는다. 게임에 익숙하지 않으면 포토 캐논이나 미사일 터렛에 얼마나 가까이 접근할 수 있는지 알기 어렵다. 팀에서는 게임 화면의 유닛과 건물을 객체 탐지 모델로 찾고 방어 건물의 사거리와 화면에 보이는 병력 정보를 게임 위에 표시하는 보조 프로그램을 만들었다.

2024년 5월부터 6월까지 인공지능과 게임 프로그래밍 수업에서 진행한 4인 팀 프로젝트다. 나는 팀원이 만든 탐지·판단 코드를 실제 스타크래프트 창에 연결하는 Win32 오버레이를 맡았다. 캡처 영역과 탐지 결과, 오버레이의 좌표를 맞추고 추론 루프와 Win32 메시지 루프가 함께 동작하도록 구성했다. 게임 창 위에는 마우스 입력을 통과시키는 투명한 창을 겹쳐 탐지된 건물의 사거리와 병력 정보를 표시했다.

게임 화면 위의 투명 오버레이

중간 결과물은 캡처한 화면과 탐지 상자를 별도의 Tkinter 창에서 보여 줬다. 모델이 무엇을 찾았는지 확인하기에는 충분했지만 실제 게임을 하면서 보조 정보를 보기에는 적합하지 않았다. 나는 탐지 결과를 스타크래프트 창 위에 바로 표시하려고 했다.

필러박스가 포함된 기존 데이터와 4대 3 비율로 정리한 데이터에서 스타크래프트 유닛 탐지 결과를 비교한 화면

화면 비율이 섞인 데이터에서 탐지가 흔들리는 원인을 찾고, 필러박스 제거와 4:3 비율 정리 뒤 결과를 다시 비교했다.

처음에는 게임 창 자체에 도형을 그리는 방법을 시도했지만 원하는 대로 동작하지 않았다. 대신 게임 창과 같은 위치와 크기의 투명한 팝업 창을 만들고 항상 위에 놓되 마우스 입력은 아래 게임으로 통과시키는 구조로 바꿨다. 이 창의 배경색은 투명하게 처리하고 탐지 결과에서 받은 원과 글자만 그렸다.

게임 창의 클라이언트 영역은 GetClientRect로 구하고 ClientToScreen으로 화면상의 위치를 찾았다. 팀의 캡처 코드도 같은 영역을 읽었기 때문에 모델이 반환한 좌표를 오버레이에 그대로 연결할 수 있었다. 다만 오버레이 창의 위치와 크기는 프로그램을 시작할 때 한 번만 정했다. 실행 중에 게임 창을 옮기거나 크기를 바꾸면 오버레이가 이를 따라가지는 못했다.

스타크래프트 게임 창에서 방어 건물을 중심으로 붉은 사거리 원과 병력 비율을 표시한 오버레이

별도의 디버그 창이 아니라 실제 게임 창 위에 사거리 원과 병력 비율을 표시했다.

추론과 메시지 루프의 병행

Win32 창은 운영체제가 보내는 그리기와 종료 메시지를 계속 받아 처리해야 한다. 처음에는 메시지 루프를 시작한 뒤 호출이 돌아오지 않아 화면 캡처와 추론 결과 갱신을 이어 갈 수 없었다.

오버레이의 메시지 루프를 별도 스레드로 옮겨 이 문제를 해결했다. 탐지 코드에서는 현재 화면에 그릴 도형을 목록에 넣고 오버레이 창에 다시 그리기를 요청한다. WM_PAINT를 받으면 이전 결과 대신 최신 목록에 있는 원과 문자를 그리도록 구성했다. 덕분에 모델 추론과 Windows 창 처리가 서로를 기다리지 않고 각자의 흐름을 유지할 수 있었다.

타일 사거리의 픽셀 환산

모델이 알려 주는 것은 방어 건물의 경계 상자와 중심 좌표다. 반면 스타크래프트의 사거리는 타일 단위이고 화면에 그리는 원의 반지름은 픽셀 단위다. 기준 해상도 640×480에서 타일 한 칸을 32픽셀로 두고 현재 게임 창의 너비에 비례해 한 칸의 픽셀 크기를 다시 계산했다.

이 값을 방어 건물의 사거리와 곱해 탐지 상자 중심에 원을 그렸다. 미사일 터렛과 포토 캐논에는 고정 사거리를 적용했다. 창 크기를 바꿔도 건물과 원의 상대적인 크기가 유지되는지는 실행 화면에서 확인했지만 창 크기별 좌표 오차를 따로 측정하지는 않았다. 너비만 기준으로 환산했기 때문에 게임 화면의 종횡비가 달라지거나 레터박스와 필러박스가 생기면 오차가 날 수 있었다.

벙커 내부 정보의 한계

벙커의 사거리는 안에 들어간 유닛에 따라 달라진다. 하지만 유닛이 벙커 안으로 들어가면 화면에서는 어떤 유닛인지 확인할 수 없다. 객체 탐지만으로 정확한 사거리를 정할 수 없는 정보였다.

팀에서는 실제 게임에서 흔히 쓰는 마린이 들어 있다고 가정했다. 업그레이드 전후의 두 사거리를 겹친 원으로 표시해 하나의 값처럼 단정하지 않도록 했다. 완전한 해결은 아니며 화면에 드러나지 않는 게임 상태를 영상만으로 추론할 때 생기는 한계가 남았다.

객체 탐지와 외부 API 연동

이 기능의 목적은 승률 계산보다 객체 탐지 결과를 외부 API의 입력으로 연결해 보는 데 있었다. 팀원이 만든 로직은 화면에서 탐지한 유닛을 테란과 프로토스로 나누고 종류별 개수를 문자열로 정리했다. 이 값을 GPT에 보내 두 종족의 비율을 받아 오버레이에 표시했다. 비율 자체는 코드로 계산할 수 있으므로 GPT가 필요한 문제는 아니었다.

당시에는 API 제한과 비용을 고려해 30프레임마다 한 번 요청하도록 제한했다. 프레임 속도에 따라 실제 요청 주기가 달라지는 임시 제어였다. 유닛의 체력과 업그레이드, 지형, 조작과 다른 화면의 병력을 알 수 없고 응답을 별도로 검증하지도 않았기 때문에 이 결과를 실제 승률로 해석할 수는 없다.

결과와 한계

최종 프로그램은 스타크래프트 창을 캡처해 유닛과 건물을 찾고 탐지한 방어 건물의 위치에 사거리 원을 표시했다. 별도의 디버그 화면을 보지 않고 게임 위에서 결과를 확인할 수 있었다.

다만 모델이 배경이나 이동 중인 화면을 건물로 잘못 인식하면 큰 사거리 원이 곧바로 나타났다. 작은 탐지 상자의 오탐이 오버레이에서는 더 눈에 띄는 오류로 커졌다. 당시에는 신뢰도 임계값과 화면을 보며 정성적으로 확인했을 뿐, 실제 플레이 영상에서 오탐률이나 처리 지연을 반복 측정하지 않았다.

다시 만든다면 여러 프레임에서 같은 위치에 건물이 이어서 탐지될 때만 원을 표시하고 잠깐 탐지가 끊겨도 바로 사라지지 않도록 시간 조건을 두겠다. 녹화한 게임 영상을 같은 입력으로 반복 재생하면서 탐지 정확도와 화면 표시까지 걸리는 시간도 함께 측정할 것이다.