하나의 런타임, 하나의 프로토콜로 게임을 프로그래밍 가능한 환경으로
W3 Runtime은 원본 게임에 주입되어 게임 스레드에서 월드 전체를 수집하고, 명령을 실행하고, 결과를 보고합니다. 여러분의 AI는 “무엇을 할지”만 말하면 되며, 버전이 붙은 공유 메모리 프로토콜로 런타임과 대화합니다.
하위 계층은 상위 계층의 존재를 모릅니다
인터페이스 계층에는 “싸울지 말지”를 판단하는 로직이 한 줄도 없습니다. 브레인끼리는 서로 import하지 않고 SDK에만 의존합니다. 이것이 아레나가 성립하는 전제입니다. 플랫폼에는 “인터페이스 계층 + 심판”만 있으면 되고, 누구의 브레인이든 꽂을 수 있습니다.
맵 전체를 50 ms마다 한 번씩
정보는 런타임이 게임 스레드에서 한 번에 수집해 공유 메모리에 푸시하고, 클라이언트는 바로 읽기만 하므로 더 이상 게임 스레드에 줄을 서지 않습니다. 수집 1회는 중앙값 0.5 ~ 0.9 ms(유닛 100 ~ 120개)이며, 구간별 소요 시간은 항상 월드 블록 헤더에 기록됩니다.
플레이어 ×16
금, 목재, 식량과 상한, 누적 채집량, 종족
유닛 ×1024
유형, 소유자, 좌표, 체력/마나, 현재 오더와 대상, 실제 공격 대상, 레벨·경험치·스킬 포인트, 플레이어별 가시성
유닛 세부 정보 ×256
스킬 12개(레벨, 남은 쿨다운), 버프 8개, 인벤토리 6칸. 영웅 우선
생산 테이블 ×128
훈련 / 연구 / 건설 / 업그레이드. 대기열, 총 소요 시간, 경과 시간, 막힘 여부
바닥 아이템 ×256
유형, 위치, 내구도. 줍거나 사용하면 이벤트 발생
나무 ×4096
파괴 가능한 오브젝트의 위치와 체력, 2초마다 갱신
맵
128 단위 한 칸의 이동 가능 / 건설 가능 그리드, 플레이 영역, 시작 위치. 게임 시작 후 몇 초 안에 계산 완료
시간
엔진 게임 시계, 게임 내 시각(낮과 밤), 배속, 게임 중 여부
어떤 경로로 갈지는 런타임이 정합니다
채집, 후퇴, 시전…… 똑같이 “유닛에게 일을 시키는” 것이라도 엔진 안에서 거치는 경로는 제각각입니다. 이 노하우는 모두 런타임에 담겨 있습니다. 여러분은 무엇을 할지만 말하면, 런타임이 검증된 레시피로 실행하고 같은 프레임에 명령 전후의 오더를 읽어 회신으로 돌려줍니다.
- 한 번에 한 배치를 제출해 같은 프레임에 실행
- 모든 명령에 회신: 상태 코드 + 엔진 사유 코드 + 실행 시간
- Shift 대기열, 경유점, 연속 건설
- 조회: 기술 카운트, 실행 가능성, 가시성, 금광 잔량, 컴퓨터 상대의 공격 목표
느린 원인은 프로세스 간 통신이 아니었습니다
공유 메모리 읽기/쓰기는 나노초 단위이며, 스냅샷 하나를 읽는 데 약 0.05 ms입니다. 기존 채널이 느렸던 것은 요청마다 시스템 전체가 공유하는 락 하나를 잡고, 게임이 다음에 메시지를 꺼낼 때까지(프레임당 한 번, 16 ~ 33 ms) 기다려야 했기 때문입니다. 레퍼런스 브레인의 실제 소요 시간 중 93%가 이 대기에 쓰였습니다.
- 시스템 전체가 공유하는 뮤텍스 획득
- 게임 스레드에 메시지 하나 전달
- 게임이 다음에 메시지를 꺼낼 때까지 대기(프레임당 한 번)
- 완료 이벤트를 기다린 뒤 락 해제
6개 프로세스가 함께 쓰면 처리량이 약 88회/초에서 막힙니다.
- 클라이언트마다 레인 하나를 독점: 쓰는 쪽 하나, 읽는 쪽 하나, 락 없음
- 실행 지점은 게임 스레드의 이벤트 디스패치 안(초당 수백 회)이며, 제출이 없으면 오버헤드는 정수 비교 몇 번뿐
- 한 번에 16개를 제출하면 같은 디스패치에서 모두 실행. 한 번 비울 때 4 ms 시간 예산
- 명령마다 마감 시간: 일시 정지 후 재개해도 만료된 명령은 다시 실행되지 않음
모든 명령의 회신에 실행 시간이 담기고, 레인 헤더에는 가장 느렸던 명령과 최근 비우기에 걸린 시간이 기록됩니다. 어디가 느린지 한눈에 보입니다.
| 등급 | 채널 | 지연 | 사용 주체 | 상태 |
|---|---|---|---|---|
| 0 | 푸시 스냅샷 + 이벤트 스트림 | 한 번 읽는 데 약 0.4 ms. 50 ms마다 새로 발행(16 ms까지 조정 가능) | 모든 Bot | 실게임 검증 |
| 1 | 고속 레인 | 약 1프레임. 6개 프로세스 동시 실행 시 중앙값 0.06 ms, 처리량 약 3000회/초 | SDK 기본값 | 실게임 검증 |
| 2 | 제어 채널 | 20 ~ 40 ms | 폴백, 일부 UI 관련 조작 | 실게임 검증 |
| 3 | 게이트웨이(WebSocket / JSON) | 1등급 + 약 1 ms | 모든 언어, 브라우저 페이지, LLM(MCP), 다른 컴퓨터 | 실게임 검증 |
실측(1.27 테스트 인스턴스, 2026-09-23 / 24)
엔지니어링 노트: 게임 초반 3분간 명령이 1000배 느렸던 이유
명령마다 실행 시간을 기록하기 시작하자, 게임 초반 몇 분 동안 같은 배치의 두 번째 명령부터 건당 4 ~ 10 ms가 걸리다가 게임 시간 약 180초에 갑자기 몇 마이크로초로 떨어지는 현상이 보였습니다. 런타임에 내장된 게임 스레드 샘플러로 샘플 1700개를 수집해 보니 93%가 런타임 자체의 로그 함수에 있었습니다. 한 줄을 쓸 때마다 로그 파일을 동기적으로 열고 닫았는데, 게임 초반에는 엔진 오더마다 한 줄씩 기록했던 것입니다. 디버그 로그를 기본으로 끄고 로그를 비동기로 기록하도록 바꾸자, 게임 시작 14초째의 명령도 4 ~ 8 µs면 충분해졌습니다.
우리는 추측이 아니라 측정한 숫자를 믿습니다.
모든 레인에는 역할이 있습니다
소유권 검증과 시야 필터링은 런타임에서 합니다. 엔진의 실행 계층 자체는 유닛 소유권을 검증하지 않으므로 여기서 보완할 수밖에 없습니다. 두 AI를 대결시키려면 같은 게임에서 player 레인을 두 개 열면 됩니다.
| 역할 | 볼 수 있는 것 | 할 수 있는 것 |
|---|---|---|
| player | 아군 전체 + 시야 안의 적과 중립(공정 모드) | 아군 유닛만 지휘 가능 선수, 여러분의 Bot |
| observer | 맵 전체 | 명령 불가. 조회, 카메라 제어, HUD 읽기 가능 중계 연출, 해설, 복기 |
추측하지 않고, 죽지 않는다
1.24 ~ 1.28은 같은 엔진 구조라 “런타임 하나 + profile 여러 개” 방식에 적합합니다. 1.29 이후 버전과 리포지드는 다른 엔진이므로 자동 호환을 약속하지 않습니다.
- 식별 Game.dll의 버전 리소스와 파일 해시를 읽어 profile을 선택
- 심볼 테이블 레시피는 숫자가 아니라 심볼 이름을 참조하며, 심볼마다 호출 규약과 인수 형태를 명시
- 시그니처 스캔 폴백 알 수 없는 버전에서는 함수 시작 부분의 바이트 패턴으로 스캔하고, 유일하게 일치할 때만 사용
- 시작 시 자가 점검 부작용 없는 방법으로 심볼마다 검증하고, 통과하지 못한 기능은 사용 불가로 표시
- 기능 목록 SDK가 어떤 기능이 사용 불가임을 읽으면, 조용히 0을 반환하는 대신 호출 시 명확한 오류를 던짐
Bot 하나의 버그가 게임 전체를 무너뜨려서는 안 됩니다
모든 AI가 독립 프로세스에서 실행되는 이유이기도 합니다. 같은 프로세스 안의 널 포인터 하나는 게임 전체의 크래시로 이어지지만, 독립 프로세스가 죽으면 그쪽만 멈출 뿐입니다.
| 메커니즘 | 방식 | 상태 |
|---|---|---|
| 게임을 무너뜨리지 않음 | 모든 게임 호출을 예외 보호로 감쌈. 한 번 비울 때 4 ms 시간 예산(고정밀 타이머) | 구현됨 |
| 클라이언트 간 격리 | 클라이언트마다 레인 하나와 개별 할당량. 하나가 멈춰도 다른 클라이언트를 막지 않음 | 구현됨 |
| 만료되면 실행 안 함 | 모든 명령에 마감 시간이 있고, 만료되면 표시만 하고 실행하지 않음. 일시 정지 후 재개해도 재실행되지 않음 | 구현됨 |
| 관측 가능성 | 명령별 실행 시간, 수집 1회의 구간별 소요 시간을 월드 블록 헤더에 기록 | 구현됨 |
| 자동 재연결 | SDK가 게임 전환, 프로세스 전환을 따라가며 자동 재연결 | 일부 |
| 기능 차단기 | 어떤 기능이 연속 N회 실패하면 → 사용 불가로 표시하고 이벤트 발생, 다른 기능은 그대로 동작 | 계획 |
게임 안에서 나만의 것을 만드세요
시맨틱 명령은 AI가 플레이어처럼 조작하게 하고, 확장 레이어는 플레이어가 보고 겪는 것을 바꿉니다. 클릭할 수 있는 나만의 UI를 그리고, 맵 제작자가 쓰는 함수 전부를 호출할 수 있습니다. 두 경로는 “멀티플레이 게임에서 안전한가”를 기준으로 나뉩니다.
- 텍스트 상자, 패널, 진행 바, 이미지, 지형에 붙는 원, 화살표가 달린 경로
- 유닛, 월드 좌표, 화면 위치에 고정. CJK 폰트, 둥근 모서리, 반투명, 원하는 색 모두 가능
- 런타임이 직접 그리므로 게임 오브젝트를 만들지 않고 게임 상태도 바꾸지 않음
- Python에서는 요소 하나에 한 줄. HTTP를 쓰거나 공유 메모리에 직접 써도 됨
- 버튼과 선택 카드를 클릭할 수 있고, 마우스를 올리면 강조됨. 마우스 포인터 아래에 그려지며, 버튼을 누른 클릭은 게임에 전달되지 않음
- 유닛 생성, 속성 변경, 이펙트, 패널, 대화 상자, 사운드, 카메라, 전장의 안개, 날씨……
- Farsight 콘솔, 명령줄, HTTP, Python에서 같은 스크립트 문법
- 자주 쓰는 효과는 한 줄에 하나: 떠오르는 텍스트, 연결선, 범위 원, 초상화 대사, 전체 화면 필터
- 멀티플레이 게임에서는 읽기 전용 함수만 허용해 동기화 어긋남(desync)을 방지
| 캔버스 | JASS 화면 함수 | |
|---|---|---|
| 그리는 주체 | 런타임 자체 드로잉 | 게임 자체 |
| 멀티플레이 | 안전: 내 화면에만 그림 | 싱글플레이 전용 |
| 스타일 | 자유: 폰트, 둥근 모서리, 반투명, 이미지 | 게임 기본 스타일 |
| 따라다니기 | 유닛 / 월드 좌표 / 화면 위치 | 함수마다 다름 |
| 1회 비용 | 프레임당 0.2 ~ 0.35 ms | 호출당 약 13 ms |
플레이어가 한 일이 바로 이벤트 스트림에 들어옵니다
런타임이 플레이어의 조작을 직접 보고합니다. 화면에 그린 어떤 버튼을 클릭했는지, 어떤 단축키를 눌렀는지, 지면의 어디를 클릭했는지, 무엇을 선택했는지, 어떤 스킬을 썼는지, 채팅창에 무엇을 입력했는지. 게임 자체의 트리거 이벤트(지역 진입, 대화 상자 버튼, 방향키)는 빈 트리거로도 받을 수 있습니다. 이벤트만 등록하고 조건과 액션은 쓰지 않은 채, 몇 번 실행됐는지 세면 됩니다.