Elemental Sandbox
이펙트가 가장 안 값나갑니다. 가져올 규칙은 하나 — 기록은 무차원으로, 치수는 매 프레임 해석.
게임의 「화려한 효과」를 눈으로 보면서 숫자를 만질 수 있게 만든 연습장입니다. 저는 여기서 코드를 한 줄도 안 가져왔는데, 문장 하나를 가져와서 제 코드 43군데를 고치게 됐습니다.
게임에서 불꽃이 터지고 번개가 내리치는 장면을 「이펙트」라고 부릅니다. 이걸 만드는 사람은 늘 같은 고생을 합니다 — 효과가 0.5초 만에 지나가 버려서, 어디가 이상한지 볼 수가 없다는 것입니다.
이 사람이 만든 건 게임이 아닙니다. 적도 없고 체력도 없습니다. 효과를 한복판에서 멈춰 세워 놓고, 멈춘 그림을 보면서 크기와 속도를 조절하는 연습장입니다.
멈춘 그림을 보면서 숫자를 바꾸는 것이 왜 어려운 일인가
이게 왜 대단한지 감이 안 오실 수 있습니다. 비유를 하나 들겠습니다.
불꽃놀이 사진을 찍었다고 해 봅시다. 사진은 멈춰 있으니 볼 수는 있습니다. 그런데 「이 불꽃을 두 배 크게」 하고 싶으면 어떻게 할까요. 사진으로는 안 됩니다. 다시 쏴야 합니다.
보통 게임 프로그램이 딱 이렇습니다. 멈추면 볼 수는 있는데 못 고치고, 고치려면 처음부터 다시 틀어야 합니다. 그래서 「조금 키워 볼까」를 백 번 하려면 백 번 다시 틀어야 합니다.
이 연습장은 멈춘 상태에서 크기를 두 배로 올리면 그 자리에서 커집니다. 다시 틀지 않아도 됩니다.
그게 가능한 이유는 「숫자를 기억하지 않기」 때문입니다
핵심이 여기 있습니다. 그리고 이게 제가 가져온 전부입니다.
얼음 가시 하나가 화면에 있다고 해 봅시다. 보통은 이렇게 만듭니다.
- 보통 방식: 가시가 자기 크기를 기억합니다 — 「나는 3미터짜리다」
- 이 연습장 방식: 가시가 비율만 기억합니다 — 「나는 전체 길이의 30% 지점에 있다」
차이가 뭐냐면, 설정에서 「전체 길이를 10미터로」 바꿨을 때입니다.
| 「나는 3미터」라고 기억한 경우 | 「나는 30% 지점」이라고 기억한 경우 | |
|---|---|---|
| 설정을 바꾸면 | 3미터 그대로. 안 따라옴 | 알아서 3미터 → 3미터가 아니라 새 비율로 다시 계산됨 |
| 고치려면 | 처음부터 다시 만들어야 함 | 그냥 바뀜 |
즉 실제 길이·시간·각도를 아무것도 저장하지 않고, 화면을 그릴 때마다 설정에서 새로 계산합니다. 유일하게 저장하는 실제 값은 「언제 생겼나」 하나인데, 그건 크기가 아니라 일어난 사건이라서 바뀔 일이 없습니다.
문서를 안 믿고 직접 돌려서 확인했습니다
「그렇다더라」로 끝내지 않고 받아서 직접 돌려 봤습니다. 도중에 문제가 하나 있었는데, 창이 화면에 안 떠 있으면 그림을 안 그려서 화면 저장이 계속 실패했습니다. 그림 그리는 부분을 직접 한 칸씩 불러내는 방식으로 돌아갔습니다.
덕분에 시간을 정확히 같게 맞출 수 있었고, 이런 결과가 나왔습니다.
| 흐른 시간 | 높이 | 기울기 | |
|---|---|---|---|
| 바꾸기 전 | 13.95 | 3.1 | 0.42 |
| 바꾼 뒤 | 13.95 (똑같음) | 7.44 | 0.97 |
시간은 한 칸도 안 흘렀는데 모양만 바뀌었습니다. 말이 맞다는 걸 눈으로 확인한 것입니다.
그 자로 제 코드를 재 보니 43군데가 반대로 돼 있었습니다
여기가 이 편에서 제일 아팠던 자리입니다.
배운 문장 — 「실제 크기를 저장하지 말고 비율만 저장해라」 — 을 들고 제 프로젝트를 훑었습니다. 정확히 반대로 짜인 곳이 43군데 나왔습니다.
남의 것을 뜯어보는 일이 이래서 값을 합니다. 배운 문장 자체보다, 그 문장이 자로 쓰여서 내 것을 재 준다는 게 더 큽니다.
코드는 안 가져왔습니다 — 규격이 안 맞아서
「그렇게 좋으면 코드를 가져오지」 싶은데 못 가져옵니다.
법적으로는 자유롭게 써도 되는 조건입니다. 문제는 기술 규격입니다. 이건 웹 브라우저에서 도는 3차원 물건이고, 제가 만드는 건 다른 프로그램에서 도는 2차원 물건입니다. 엔진이 다르고 차원이 다릅니다. 부품을 뜯어 와도 들어갈 자리가 없습니다.
이 사람이 잘한 것 하나 더 — 「못한 것」을 자기가 적어 뒀습니다
마지막으로 배운 게 하나 더 있는데, 기술이 아니라 태도입니다.
이 사람은 창고에 두 가지를 같이 넣어 뒀습니다.
- 목표로 삼은 참고 그림 넉 장 — 「무엇에 맞춰 만들었는지」를 남긴 것입니다
- 「아직 거친 부분」 일곱 가지 — 자기 물건의 약점을 자기가 먼저 적어 뒀습니다
두 번째가 특히 드뭅니다. 보통은 잘된 것만 보여 주는데, 약점을 먼저 적어 두면 읽는 사람이 속을 일이 없습니다. 저도 그렇게 씁니다.
여기부터는 자세한 기록입니다
이펙트 코드는 한 줄도 안 가져왔는데, 규칙 한 문장이 제 코드 43곳의 성격을 정했습니다. 이 레포의 화려한 부분은 웹 전용이라 제 엔진으로 안 넘어옵니다. 넘어온 건 “인스턴스는 seed와 비율만 기억하고, 미터·초·라디안은 매 프레임 설정에서 다시 계산한다” 한 줄입니다. 그리고 그 자로 제 프로젝트를 재보니 — 정확히 반대로 짜여 있었습니다.
무엇을 만든 것인가
브라우저에서 도는 스킬샷 이펙트 놀이터다. 캐릭터가 바닥에 서 있고, 키로 스킬을 뽑으면 지면 조준 표시가 마우스를 따라 움직이고, 클릭하면 발사된다. 게임이 아니다 — 적도 체력도 없다. 목적은 “이펙트를 눈으로 보면서 숫자를 만지는 것” 하나다.
그리고 간판 주장이 사실이었다. “전부 코드로 생성” — 클론해서 전수 확인하니 바이너리는 열 개뿐이고 그중 이펙트용이 하나도 없다. 스프라이트시트도, 플립북도, 이펙트 텍스처도 0개다.
| 무엇 | 어떻게 만들었나 |
|---|---|
| 얼음 결정 | 절차적 지오메트리 |
| 번개 | 버텍스 셰이더가 배치하는 리본 스트립 한 장 |
| 운석 | CPU에서 파쇄면으로 깎은 구체 |
| 빔 | 반지름 셋으로 세 번 그린 파라메트릭 튜브 |
| 조준선·화상·용암 균열 | 전부 거리장 + 노이즈 셰이더 |
| 안개·불티·파편 | GPU 파티클 — 실루엣도 절차적 |
런타임 의존성이 두 개다. 그리고 레포 나이가 5일, 커밋 18개, 기여자 1명이다.
왜 일시정지 중에 슬라이더가 먹히나
위 도판의 셋이 동시에 성립하기 때문인데, 두 번째가 핵심이다. 얼음 가시 하나가 기억하는 것에 미터도 라디안도 초도 없다. 직선 위 위치가 비율이고, 좌우 오프셋이 비율이고, 나머지는 난수 몇 개다. 저장하는 유일한 실단위는 “발생 시각” — 그건 치수가 아니라 사건이라서다.
그러니 매 프레임 update()가 설정 객체에 대고 다시 곱한다. 높이 슬라이더를 당기면 이미 서 있는 얼음밭이 다시 자란다.
표현 못 하는 값의 예외 처리도 좋다. 면 개수나 휘어짐처럼 위치·크기 곱하기로 표현할 수 없는 파라미터 넷은 지오메트리에 구워야 해서, 보통은 “재시작해야 반영되는 상수”가 된다. 저자는 그 넷만 문자열 키로 해싱해서 바뀔 때만 통째로 다시 만들고, 인스턴스의 seed와 발생 시각은 그대로 옮겨 태운다 — 모양은 새로, 정체성은 유지.
그리고 주석에 판단 근거가 적혀 있다. “결정 하나가 108삼각형이라 근사하느니 그냥 다시 만드는 게 싸다.” 비싸서 못 하던 걸 비용을 실제로 재보고 뚫은 사례다.
직접 돌려서 확인했다
문서를 믿지 않고 클론해서 로컬에서 돌렸다. 브라우저 창이 화면에 안 떠 있으면 프레임을 안 그려서 캡처가 계속 실패했는데 — 프레임 루프를 고정 스텝으로 직접 호출하는 방식으로 우회했다. 부수 효과로 결정론적 타임스텝을 얻었고, 그래서 아래 검증이 가능해졌다.
얼음을 쏘고 54프레임 진행한 뒤 일시정지하고, 시간을 1틱도 안 흘린 채 설정값 셋만 바꿔 세 프레임을 더 그렸다.
| 경과 시각 | 높이 | 기울기 | |
|---|---|---|---|
| BEFORE | 13.95 | 3.1 | 0.42 |
| AFTER | 13.95 (동일) | 7.44 | 0.97 |
주장은 참이었다. 경과 시각이 완전히 같은데 이미 서 있던 결정들이 더 높이 자라고 바깥으로 기울었다. 연기 기둥과 눈송이 입자와 캐릭터 포즈는 픽셀 단위로 그대로다 — 시뮬레이션이 다시 돈 게 아니라 같은 순간을 새 치수로 다시 해석한 것이다. 드로우콜도 두 프레임 모두 115로 같았다.
그리고 실측이 자기신고와 갈렸다. 대기 상태 드로우콜은 32로 일치했는데, 얼음밭이 서 있을 때는 제 쪽이 95~115로 자기신고(~69)보다 높았다. 캡처 시점과 해상도 차이다 — 자기신고를 그대로 인용하면 안 된다. 슬라이더 개수도 실측 918개로 주장(938)과 20 차이가 났다.
그리고 제 코드를 대봤다
넘어온 규칙은 이것 하나다.
어빌리티 인스턴스는 seed와 비율만 기억한다. 미터·초·라디안은 매 프레임 설정에서 다시 계산한다.
제 무협 게임 프로젝트는 정확히 반대로 짜여 있었다. 보스가 초식을 낼 때 전조 시간이 코루틴 시작 시점에 굳는다. 플레이 중에 전조 시간을 바꿔도 이미 진행 중인 전조에는 반영되지 않는다.
전체를 세니 시작 시점에 굳히는 방식 43곳, 매 프레임 다시 읽는 방식 22곳이었다.
이펙트 코드는 한 줄도 안 가져왔는데 제 코드 43곳의 성격이 정해졌다. 이 분류에서 기대할 건 그런 종류다.
라이선스는 두 곳을 봐야 한다
코드 포팅은 기각이다. 라이선스상 자유롭지만 웹 셰이더와 3D 평면 전제라 제 엔진의 2D 파이프라인으로 옮길 값이 없다.
그리고 통째로 가져가는 건 위험하다. 같은 레포가 라이선스 파일에는 관대한 조건을 적어 두고 README에는 “이 프로젝트 목적으로 있는 그대로 제공된다”고 다른 말을 한다. 번들된 캐릭터 모델과 환경 프로브는 원 라이선스 유지라고만 적혀 있다.
의존 대상이 아니라 교재로 다룰 것. 5일 된 레포에 기여자 한 명이다. 게임 쪽 레포는 수명이 짧아 이 구분이 특히 중요하다.
마지막으로 — 이 레포가 잘한 것 하나 더
목표로 삼은 참고 이미지 넉 장을 레포 루트에 넣어 뒀다. “무엇에 맞춰 튜닝했는지”를 남긴 것이다. 그리고 README에 “알려진 거친 부분” 일곱 건을 스스로 적었다 — 정렬 아티팩트, 평면 바닥 전제, 비어 있는 패스까지.
기법으로 가장 이식성 높은 건 무차원 기록, 길이 0 프레임, 그리고 이 마지막 것이다. 엔진과 언어에 무관한 설계 규칙이기 때문이다. 나머지는 수동 엔진이라서 직접 해야 하는 일이라 — 엔진이 이미 해 주는 환경에서는 값이 급락한다.