Skip to content
목록으로 돌아가기
Pass-Through : 서로 다른 게임을 하나의 세계에 넣어보기

Pass-Through : 서로 다른 게임을 하나의 세계에 넣어보기

2026년 10월 8일엔지니어링
  • Stardew Valley
  • StarCraft
  • OpenBW

펠리컨 마을 광장에 몰려온 10단계 보스 습격

평화로운 펠리컨 마을 광장에 저그 대군이 몰려왔습니다. 마지막 10단계 습격이라 프로토스와 테란 유닛까지 한 편으로 섞여 있는데, 이게 어떻게 가능한지는 뒤에서 이야기하겠습니다.

요즘 게임 모딩 쪽에서 패스스루(passthrough) 모드라는 말이 부쩍 자주 들립니다. 스카이림 안에서 마인크래프트를 하는 모드가 나오더니, 얼마 안 가 폴아웃 4 버전, 아우터 와일즈 버전까지 따라 나왔습니다.

그래서 저도 한 가지 궁금증이 생겼습니다. 이 방식으로 스타듀 밸리 농장에 진짜 스타크래프트 유닛을 들여놓을 수 있을까? 스프라이트만 빌려 오는 게 아니라, 스타크래프트 엔진이 직접 길을 찾고 싸우는 유닛이어야 의미가 있다고 생각했습니다.

이 글은 그렇게 만들게 된 StardewCraft의 구조와 구현 과정, 그리고 중간에 겪은 삽질을 정리한 글입니다.


1. 패스스루 모드란?

1.1. 두 게임을 동시에 돌린다

일반적인 크로스오버 모드는 한 게임 안에 다른 게임의 에셋을 가져다 붙이는 방식입니다. 그래서 겉모습은 바뀌어도, 움직임과 규칙은 원래 게임 그대로인 것입니다.

패스스루 모드는 접근이 완전히 다릅니다. 두 게임을 실제로 동시에 실행하고, 둘이 실시간으로 상태를 주고받게 만듭니다.

크로스오버 모드는 한 게임이 다른 게임의 그림만 빌려 쓴다. 패스스루 모드는 두 게임이 동시에 돌면서 한쪽은 화면을, 다른 쪽은 규칙을 맡는다.

보통 한쪽(호스트)이 화면과 월드를 그리고, 다른 쪽이 이동이나 전투 같은 게임 규칙을 계산합니다. 그래서 어느 한쪽만 켜서는 동작하지 않습니다.

1.2. 왜 갑자기 뜨고 있나

불을 붙인 건 2026년 9월 30일에 공개된 SkyCraft입니다. 스카이림 안에서 마인크래프트를 플레이하는 MIT 라이선스 오픈소스 모드인데, 공개 후 이틀 동안 빌드가 세 번 올라왔고, 세 릴리스를 합쳐 다운로드가 2,800건 가까이 됐습니다.

구조는 이렇습니다.

  • 스카이림이 화면을 그리고, 마인크래프트는 창도 소리도 없이 뒤에서 돌아갑니다.
  • 플레이어의 이동, 인벤토리, 전투는 마인크래프트가 계산합니다.
  • 두 게임은 공유 메모리로 상태를 주고받고, 하트비트로 서로 살아 있는지 확인합니다.
  • 스카이림 NPC는 마인크래프트 안에 보이지 않는 엔티티로 존재합니다.

이후 Fallout 4, Outer Wilds 같은 다른 게임으로 옮긴 파생작들이 이어지면서, SkyCraft는 패스스루 모드의 기준 설계처럼 쓰이고 있습니다.

이게 지금 뜨는 이유는 크게 두 가지라고 봅니다.

  1. 모드 로더가 이미 깔려 있습니다. 스카이림의 SKSE, 마인크래프트의 Fabric, 유니티 게임의 BepInEx처럼, 게임 안에 내 코드를 넣을 수 있는 기반이 인기 게임 대부분에 이미 있습니다.
  2. AI 코딩 에이전트가 진입 장벽을 낮췄습니다. 두 게임의 내부 구조를 파악하고 C++과 C#·Java 양쪽에 맞는 통신 코드를 짜는 일은, 원래 혼자 하기에는 꽤 버거운 작업이었습니다. 위 가이드도 에이전트에게 일을 시키기 전에 SkyCraft의 DESIGN.md부터 읽히라고 안내하고 있을 정도입니다.

2. 왜 스타듀 밸리 + 스타크래프트인가

조합을 고를 때 기준은 두 가지였습니다.

기준 스타듀 밸리 스타크래프트
내 코드를 넣을 수 있는가 SMAPI로 렌더링, 입력, 이벤트를 전부 다룰 수 있음 원본은 어렵지만 OpenBW가 있음
역할 월드와 화면을 그리는 호스트 화면 없이 규칙만 계산하는 게스트

OpenBW는 스타크래프트 1 엔진을 C++로 다시 구현한 오픈소스 프로젝트입니다. 헤더만 포함하면 쓸 수 있고, 그래픽 없이 게임 로직만 돌릴 수 있습니다. 스타크래프트 본체를 띄워 놓고 창을 숨기는 것보다 훨씬 깔끔한 게스트였던 것입니다.

그리고 둘 다 탑뷰 2D 타일 게임이라는 점도 컸습니다. 3D 게임끼리 묶는 것보다 좌표를 맞추기가 훨씬 쉽기 때문입니다.


3. 누가 무엇을 책임지나

패스스루 모드에서 가장 먼저 정해야 하는 건 어떤 상태를 어느 게임이 소유하는가입니다. 이걸 거꾸로 잡으면 나중에 고치기가 정말 어려워집니다.

초록은 스타듀가 쓰는 데이터, 주황은 OpenBW가 쓰는 데이터다. 두 프로세스는 공유 메모리 한 덩어리로만 대화한다.

상태 주인 비고
맵 지형, 충돌 스타듀 통행 가능 격자로 변환해서 전달
농부, 동물, 주민 위치 스타듀 OpenBW 안에 대역을 세워 따라가게 함
작물 스타듀 유닛을 작물 위로 보내고, 일정 시간 서 있으면 작물 파괴
유닛, 이동, 길찾기, 전투, 애니메이션 OpenBW 스타듀는 보고받은 대로 그리기만 함
대역이 입은 피해 OpenBW → 스타듀 누적 피해량을 보고하면 스타듀가 농부 체력을 깎음

원칙은 단순합니다. 스타크래프트 유닛과 관련된 건 전부 OpenBW가 결정하고, 스타듀는 그 결과를 그리기만 합니다. 유닛이 어디로 갈지, 누구를 때릴지 스타듀 쪽 코드는 전혀 모르는 것입니다.


4. 리마스터 데이터를 OpenBW에 먹이기

4.1. MPQ가 아니라 CASC

OpenBW는 1.16.1 시절의 MPQ 파일(StarDat.mpq 등)을 읽도록 만들어져 있습니다. 그런데 지금 구할 수 있는 스타크래프트 리마스터는 블리자드의 CASC 저장소 형식을 쓰기 때문에, 그대로는 파일 하나도 읽을 수 없었습니다.

4.2. 필요한 파일만 뽑아내기

그래서 CascLib으로 작은 추출기를 만들었습니다. 리마스터 저장소에서 OpenBW가 필요로 하는 파일만 골라, 원래 경로 그대로 폴더에 풀어 주는 도구입니다.

리마스터의 CASC 저장소에서 OpenBW가 필요한 파일만 뽑아 폴더로 푼다. 주황색 data/bw는 블리자드 데이터라 내 PC에만 둔다.

OpenBW 쪽에는 MPQ 대신 일반 폴더에서 파일을 읽는 directory_loader를 붙였습니다. 열 줄 정도의 코드라 생각보다 간단했습니다.

4.3. 스모크 테스트

본격적으로 붙이기 전에, 화면 없이 전투부터 돌려봤습니다. 마린 4기와 저글링 6기를 소환하고 저글링에게 공격 이동을 시킨 뒤 30초를 시뮬레이션했습니다.

text
global data loaded
map loaded: 4096x4096 px, tileset 1
battle at start location (3808,2384), walkable=1
spawned: marines=4 zerglings=6
t= 5s marines=0 zerglings=5
t=10s marines=0 zerglings=5
...
  owner 1 zergling at (3824,2376) hp 27
  owner 1 zergling at (3808,2384) hp 22

5초 만에 마린 4기가 전멸했고, 저글링은 6기 중 1기만 잃었습니다. 업그레이드 없는 마린이 저글링한테 붙으면 녹는 것까지 원작 그대로였습니다. 이걸 보고 나서야 "아 이거 되겠다"는 확신이 들게 되었습니다.

4.4. MSVC 없이 빌드하기

근데 개발 PC에 Visual Studio C++ 툴체인이 없었습니다. 그래서 pip install ziglang으로 받은 zig를 C++ 크로스 컴파일러로 썼는데, 파이썬만 있으면 OpenBW와 CascLib을 모두 빌드할 수 있어서 의외로 편했습니다.


5. 두 게임의 대화 창구, 공유 메모리

5.1. 메모리 배치

두 프로세스는 Local\StardewCraft_v1이라는 이름의 128KiB짜리 공유 메모리를 함께 엽니다. 직렬화 라이브러리 없이 고정 오프셋, 고정 크기 구조체만 씁니다.

128KiB 공유 메모리의 배치. 초록은 스타듀가, 주황은 OpenBW가 쓰는 영역이다. 오른쪽 막대는 전체에서 차지하는 비율이다.

헤더의 magic과 버전이 맞지 않으면 양쪽 모두 실행을 거부합니다. 개발하면서 레이아웃을 바꿀 때마다 버전을 올렸고, 지금은 4입니다.

5.2. 명령은 링 버퍼로

스타듀가 OpenBW에 보내는 명령은 7종류입니다.

명령 용도
LOAD_MAP 새 지역의 맵을 만들어라
SPAWN 이 위치에 이 유닛을 N기 소환해라
SPAWN_UNIT 기억해둔 유닛을 남은 체력 그대로 되살려라
CLEAR 습격자를 전부 없애라
DAMAGE 이 유닛에 이만큼 피해를 줘라
MOVE_TO / ATTACK_MOVE_TO 이 유닛을 저기로 보내라

스타듀는 head를, OpenBW는 tail을 올리는 단순한 링 버퍼입니다. 한쪽은 쓰기만 하고 한쪽은 읽기만 하니, 락이 필요 없습니다.

5.3. 상태는 seqlock으로

반대 방향, 즉 OpenBW가 매 프레임 내보내는 이미지 목록과 유닛 표는 seqlock으로 보호합니다.

seq가 홀수인 동안은 쓰는 중이다. 읽는 쪽은 복사 전후의 seq가 같고 짝수일 때만 결과를 쓰고, 아니면 다시 복사한다.

장점은 읽는 쪽이 쓰는 쪽을 절대 기다리지 않는다는 것입니다. 게임 렌더링 루프에서 락을 잡고 기다리는 일만큼은 피하고 싶었습니다.

5.4. 생명주기

  • 스타듀가 게임을 켤 때 OpenBW 프로세스를 띄우고, 끌 때 같이 종료합니다.
  • OpenBW는 매 프레임 스타듀의 PID가 살아 있는지 확인하고, 죽었으면 스스로 종료합니다. 그래서 스타듀가 튕겨도 유령 프로세스가 남지 않습니다.
  • OpenBW는 스타크래프트의 "가장 빠름" 속도와 같은 42ms 간격으로 한 프레임씩 진행합니다.

5.5. 게임 없이 테스트하기

매번 스타듀를 켜서 확인하기에는 너무 느렸습니다. 그래서 파이썬으로 가짜 호스트를 만들었습니다.

가짜 호스트는 다른 이름의 공유 메모리를 열고, 벽이 있는 80×65 맵을 보내고, 시즈탱크와 터렛을 세운 뒤 습격을 보내서 매초 유닛 표를 출력합니다. OpenBW 쪽 버그는 대부분 여기서 잡았습니다.


6. 스타듀 지역을 스타크래프트 맵으로

6.1. 통행 격자 → CHK 맵

농부가 지역을 옮길 때마다 스타듀는 타일마다 걸을 수 있는지만 담은 격자를 공유 메모리에 쓰고 LOAD_MAP을 보냅니다. OpenBW는 이걸 받아서 스타크래프트 맵 파일(CHK)을 그 자리에서 조립합니다.

스타듀 지역을 타일당 1바이트 통행 격자로 바꾸고, OpenBW가 그걸로 스타크래프트 맵을 즉석에서 만든다.

어떤 타일을 평지와 벽으로 쓸지는 하드코딩하지 않았습니다. 타일셋 파일(cv5, vf4)을 뒤져서 "16개 미니타일이 전부 걸을 수 있고, 저지대이며, 건물을 지을 수 있는" 타일 그룹 하나와 "전부 막힌" 타일 그룹 하나를 자동으로 고르게 했습니다.

좌표는 스타듀 1타일(64px)이 스타크래프트 1타일(32px)이 되도록 맞췄습니다. 그래서 BW 좌표 = 스타듀 좌표 ÷ 2라는 공식 하나로 끝납니다.

6.2. 터렛이 안 지어진다

여기서 꽤 오래 막혔습니다. 맵도 잘 만들어지고 유닛도 잘 소환되는데, 미사일 터렛만 지으려고 하면 계속 실패하는 것이었습니다.

로그를 찍어보니 타일 플래그는 멀쩡했고, 문제는 탐색(explored) 비트였습니다. 스타크래프트는 한 번도 밝혀지지 않은 땅에는 건물을 지을 수 없는데, 이 비트는 1이 "아직 탐색 안 함" 이라는 뜻이었습니다. 저는 이름만 보고 정반대로 이해하고 있었던 겁니다.

cpp
// 농장은 이미 아는 땅이다. explored 비트가 1이면 "탐색 안 함"이라는 뜻.
for (auto& tile : st->tiles) tile.explored = 0;

이 한 줄을 넣고 나서야 터렛이 올라갔습니다.

6.3. 진영 설정

맵에는 플레이어 4명을 둡니다.

플레이어 진영
0 농부 측 (대역, 농부가 지은 방어 시설)
1 / 2 / 3 저그 / 프로토스 / 테란 습격자, 서로 동맹

그래서 원래 스타크래프트에서는 있을 수 없는 저그·프로토스·테란 연합군이 여기서 나오게 됩니다. 맨 위 사진에 세 종족이 섞여 있던 이유가 이것입니다.


7. 스타크래프트는 농부를 모른다

7.1. 보이지 않는 대역

OpenBW 안에는 스타듀의 농부가 존재하지 않습니다. 공격할 대상이 없으니, 저글링이 농부에게 달려들 이유도 없는 것입니다.

그래서 SkyCraft가 NPC를 투명 엔티티로 만든 것처럼, 여기서도 대역을 세웠습니다.

  • 농부, 농장 동물, 마을 주민마다 OpenBW 안에 테란 민간인(Civilian) 을 하나씩 만듭니다.
  • 이 민간인은 화면에 그리지 않고, 위치는 매 프레임 스타듀 캐릭터를 따라 순간이동합니다.
  • 맞아서 체력이 깎이면 즉시 최대치로 되돌리고, 깎인 만큼을 누적 피해량에 더합니다.

대역은 절대 죽지 않는다. 깎인 체력은 바로 채우고 누적 피해량만 넘기면, 스타듀가 자기 규칙대로 피해를 적용한다.

대역이 죽지 않으니 습격자는 계속 농부를 쫓아오고, 실제 사망 판정은 스타듀의 규칙대로 처리됩니다.

7.2. 습격자의 기본 행동

습격자는 12프레임마다 농부 대역의 위치를 향해 공격 이동(Attack Move) 명령을 받습니다. 공격 이동이기 때문에 가는 길에 터렛이나 동물을 만나면 그것부터 공격합니다.

이미 교전 중인 유닛은 건드리지 않게 했는데, 덕분에 스타크래프트 특유의 뭉쳐서 싸우는 움직임이 그대로 나옵니다.

7.3. 동물과 주민의 반응

스타듀는 대역별 피해량이 늘어난 걸 보고 이렇게 반응합니다.

  • 농부 : 체력이 깎이고, 다 깎이면 평소처럼 쓰러져서 병원에서 깨어납니다.
  • 농장 동물 : 일정 확률로 도망가거나, 근처 습격자에게 박치기로 반격합니다. 설정에 따라 죽을 수도 있습니다.
  • 마을 주민 : 칼을 뽑아 들고 휘두르며 근처 습격자에게 피해를 줍니다.

닭장에 쳐들어온 저글링


8. OpenBW가 계산한 걸 스타듀에 그리기

8.1. 무엇을 넘기나

OpenBW는 매 프레임 화면에 보여야 할 이미지를 그리기 순서대로 정렬해서 넘깁니다. 이미지 하나는 24바이트입니다.

text
image id   어떤 그림인가 (images.dat 인덱스)
frame      GRP의 몇 번째 프레임인가
x, y       좌상단 위치 (BW 픽셀)
flags      좌우 반전 여부
modifier   그리기 방식 (일반, 그림자 등)
color      플레이어 색
unit id    어느 유닛의 그림인가

정렬 기준은 스타크래프트와 같습니다. 고도(지상, 공중)가 우선이고, 같은 고도에서는 아래쪽에 있는 게 나중에 그려지기 때문에, 공중 유닛이 지상 유닛을 덮고 앞에 있는 유닛이 뒤에 있는 유닛을 덮게 됩니다.

8.2. GRP 디코딩과 색 입히기

스타듀 쪽에서는 사용자 데이터 폴더의 GRP 파일을 필요할 때마다 텍스처로 디코딩해서 캐시합니다.

  • 팔레트는 타일셋 파일(.wpe)에서 읽습니다.
  • 유닛 그림의 8~15번 색은 플레이어 색 자리라서, 진영별로 다른 색으로 바꿔 끼웁니다.
  • 스프라이트를 2배 크기로 그려서, BW 1타일이 스타듀 1타일을 정확히 덮게 합니다.

8.3. 반격 : 칼과 새총

농부가 칼을 휘두르면 스타듀가 무기 판정 범위 안의 유닛을 찾고, 무기 대미지에 배율을 곱해서 DAMAGE 명령을 보냅니다. 새총 탄환도 유닛에 닿으면 같은 방식으로 처리됩니다.

OpenBW는 받은 피해를 보호막부터 깎고, 남은 만큼 체력을 깎습니다. 그래서 프로토스 유닛을 때리면 파란 보호막이 먼저 반짝이는 것까지 원작 그대로 나옵니다.


9. 게임으로 만들기

엔진 연결이 끝나고 나서, 게임 요소를 하나씩 얹었습니다.

9.1. 습격 단계

습격은 10단계로 나눴습니다.

단계 구성
1 저글링 정찰대
2 저글링 + 마린
3 질럿 + 히드라
4 파이어뱃 + 드라군
5 공중 습격 (뮤탈리스크 + 벌처)
6 골리앗 + 히드라 + 저글링
7 울트라리스크 돌격
8 가디언 공성
9 프로토스 본대 (아칸 + 질럿 + 드라군)
10 보스 : 전부 다

밤 7시가 되면 자동으로 습격이 오고, 게임 날짜가 3일 지날 때마다 한 단계씩 강해집니다.

해변에 쏟아지는 5단계 공중 습격

9.2. 작물을 노리는 습격자

일부 지상 유닛은 농부 대신 밭으로 갑니다. 스타듀가 가까운 작물 타일을 골라 MOVE_TO를 보내고, 유닛이 작물 위에 일정 시간 서 있으면 그 작물을 없앱니다.

근데 호스트가 내린 이동 명령은 OpenBW에서 10초 동안만 유지되고, 그 뒤에는 다시 농부를 쫓는 기본 행동으로 돌아갑니다. 그래서 스타듀는 먹을 작물이 남아 있는 동안 명령을 주기적으로 다시 보내 줍니다.

밭에 몰래 들어온 저글링

9.3. 작물로 사는 방어 시설

농부는 시즈탱크와 미사일 터렛을 세울 수 있습니다. 설정을 켜면 손에 든 작물로 값을 치르는데, 시즈탱크는 작물 500g어치, 터렛은 250g어치입니다.

  • 건물은 스타크래프트 맵 에디터처럼 빌드 격자에 맞춰 세워집니다.
  • 유닛이 서 있어서 못 지으면 OpenBW가 실패 횟수를 올리고, 스타듀는 그걸 보고 작물을 돌려줍니다.
  • 세운 방어 시설은 세이브 파일에 저장되고, 부서진 건 부서진 채로 남습니다.

9.4. 지역별 기억

다른 지역에 갔다가 돌아오면 OpenBW 맵은 새로 만들어집니다. 그런데 남아 있던 습격자가 그냥 사라져 버리면 어색합니다.

그래서 스타듀가 지역별로 마지막 유닛 목록을 기억해 뒀다가, 돌아왔을 때 SPAWN_UNIT으로 같은 자리에 남은 체력 그대로 되살립니다. 이 기억은 다음 날 아침에 지워집니다.

습격자와 싸우는 마을 주민

그리고 지면 이렇게 됩니다.

쓰러져서 병원에서 깨어난 농부


10. 저작권은 괜찮을까?

아마 가장 많이 받을 질문일 것 같아서 미리 적어둡니다.

  • 저장소에는 블리자드의 게임 데이터가 한 바이트도 들어 있지 않습니다.
  • 그래픽과 유닛 데이터는 각자 가진 정품 스타크래프트 리마스터에서 직접 추출해서 씁니다. SkyCraft도 마찬가지로 두 게임을 모두 가진 사람만 쓸 수 있습니다.
  • OpenBW는 라이선스가 명시돼 있지 않아서, 빌드된 실행 파일은 배포하지 않고 소스로만 공개했습니다.

11. 마무리

기법 하나하나는 새로운 게 아닙니다. 게임 모딩, 화면 없는 시뮬레이션, 공유 메모리, seqlock은 모두 오래전부터 써온 방식입니다. 패스스루 모드는 이걸 두 게임을 잇는 데 쓴 것이고, SkyCraft가 그게 실제로 된다는 걸 보여준 것입니다.

직접 해보니 가장 중요한 건 코드보다 "누가 무엇을 책임지는가"를 먼저 정하는 것이었습니다. 3장의 표 하나가 정해지고 나니, 나머지는 그 경계를 오가는 데이터를 설계하는 일이 되었습니다.

아직 다듬을 곳은 많습니다. 유닛 밸런스도 거칠고, 맵도 평지와 벽만 구분합니다. 그래도 내 농장에 저글링이 뛰어다니는 걸 처음 봤을 때의 그 기분은 꽤 오래 남을 것 같습니다.

코드는 GitHub에 올려두었습니다. 정품 두 게임을 가지고 계신 분이라면, 한 번쯤 직접 풀어보셔도 재밌을 것입니다.

👉 https://github.com/JeonDaehong/pass-through-stardew-craft

Comments