# Elemental Sandbox

> 이펙트가 가장 안 값나갑니다. 가져올 규칙은 하나 — 기록은 무차원으로, 치수는 매 프레임 해석.

- 수치: 규칙 1 → 43곳
- 사람이 보는 화면: https://aitryed.com/ko/teardowns/games/elemental-sandbox/
- 반대 언어: https://aitryed.com/en/teardowns/games/elemental-sandbox/index.md
- 저장소: https://github.com/achrefelouafi/LinearAbiltyCastingThreeJS
- 이 사이트 안내(AI용): https://aitryed.com/ko/llms.txt

---

**게임의 「화려한 효과」를 눈으로 보면서 숫자를 만질 수 있게 만든 연습장입니다. 저는 여기서 코드를 한 줄도 안 가져왔는데, 문장 하나를 가져와서 제 코드 43군데를 고치게 됐습니다.**

게임에서 불꽃이 터지고 번개가 내리치는 장면을 「이펙트」라고 부릅니다. 이걸 만드는 사람은 늘 같은 고생을 합니다 — **효과가 0.5초 만에 지나가 버려서, 어디가 이상한지 볼 수가 없다**는 것입니다.

이 사람이 만든 건 게임이 아닙니다. 적도 없고 체력도 없습니다. **효과를 한복판에서 멈춰 세워 놓고, 멈춘 그림을 보면서 크기와 속도를 조절하는 연습장**입니다.

> **[도판]** 이 레포의 모든 구조가 요구 하나에서 나왔다 — 이펙트를 한복판에서 얼려 놓고, 정지 화면을 보면서 실루엣·팔레트·타이밍을 고친다.
>
> 일시정지 중 편집이 가능한 이유는 세 가지가 동시에 성립하기 때문이다. 설정 객체 하나가 유일한 상태 소유자이고, 인스턴스는 비율과 난수만 기억하며, 일시정지는 루프 중단이 아니라 길이 0인 프레임이다.

## 멈춘 그림을 보면서 숫자를 바꾸는 것이 왜 어려운 일인가

이게 왜 대단한지 감이 안 오실 수 있습니다. 비유를 하나 들겠습니다.

불꽃놀이 사진을 찍었다고 해 봅시다. 사진은 멈춰 있으니 볼 수는 있습니다. 그런데 **「이 불꽃을 두 배 크게」 하고 싶으면 어떻게 할까요.** 사진으로는 안 됩니다. 다시 쏴야 합니다.

보통 게임 프로그램이 딱 이렇습니다. 멈추면 **볼 수는 있는데 못 고치고**, 고치려면 처음부터 다시 틀어야 합니다. 그래서 「조금 키워 볼까」를 백 번 하려면 백 번 다시 틀어야 합니다.

이 연습장은 **멈춘 상태에서 크기를 두 배로 올리면 그 자리에서 커집니다.** 다시 틀지 않아도 됩니다.

## 그게 가능한 이유는 「숫자를 기억하지 않기」 때문입니다

핵심이 여기 있습니다. 그리고 이게 제가 가져온 전부입니다.

얼음 가시 하나가 화면에 있다고 해 봅시다. 보통은 이렇게 만듭니다.

- **보통 방식**: 가시가 자기 크기를 기억합니다 — 「나는 3미터짜리다」
- **이 연습장 방식**: 가시가 **비율만** 기억합니다 — 「나는 전체 길이의 30% 지점에 있다」

차이가 뭐냐면, 설정에서 「전체 길이를 10미터로」 바꿨을 때입니다.

| | 「나는 3미터」라고 기억한 경우 | 「나는 30% 지점」이라고 기억한 경우 |
|---|---|---|
| 설정을 바꾸면 | 3미터 그대로. 안 따라옴 | **알아서 3미터 → 3미터가 아니라 새 비율로 다시 계산됨** |
| 고치려면 | 처음부터 다시 만들어야 함 | 그냥 바뀜 |

즉 **실제 길이·시간·각도를 아무것도 저장하지 않고, 화면을 그릴 때마다 설정에서 새로 계산합니다.** 유일하게 저장하는 실제 값은 「언제 생겼나」 하나인데, 그건 크기가 아니라 **일어난 사건**이라서 바뀔 일이 없습니다.

## 문서를 안 믿고 직접 돌려서 확인했습니다

「그렇다더라」로 끝내지 않고 받아서 직접 돌려 봤습니다. 도중에 문제가 하나 있었는데, **창이 화면에 안 떠 있으면 그림을 안 그려서** 화면 저장이 계속 실패했습니다. 그림 그리는 부분을 직접 한 칸씩 불러내는 방식으로 돌아갔습니다.

덕분에 시간을 정확히 같게 맞출 수 있었고, 이런 결과가 나왔습니다.

| | 흐른 시간 | 높이 | 기울기 |
|---|---:|---:|---:|
| 바꾸기 전 | 13.95 | 3.1 | 0.42 |
| 바꾼 뒤 | **13.95 (똑같음)** | **7.44** | **0.97** |

시간은 한 칸도 안 흘렀는데 모양만 바뀌었습니다. **말이 맞다는 걸 눈으로 확인한 것**입니다.

## 그 자로 제 코드를 재 보니 43군데가 반대로 돼 있었습니다

여기가 이 편에서 제일 아팠던 자리입니다.

배운 문장 — **「실제 크기를 저장하지 말고 비율만 저장해라」** — 을 들고 제 프로젝트를 훑었습니다. **정확히 반대로 짜인 곳이 43군데** 나왔습니다.

남의 것을 뜯어보는 일이 이래서 값을 합니다. 배운 문장 자체보다, **그 문장이 자로 쓰여서 내 것을 재 준다**는 게 더 큽니다.

## 코드는 안 가져왔습니다 — 규격이 안 맞아서

「그렇게 좋으면 코드를 가져오지」 싶은데 못 가져옵니다.

법적으로는 자유롭게 써도 되는 조건입니다. 문제는 **기술 규격**입니다. 이건 웹 브라우저에서 도는 3차원 물건이고, 제가 만드는 건 다른 프로그램에서 도는 2차원 물건입니다. **엔진이 다르고 차원이 다릅니다.** 부품을 뜯어 와도 들어갈 자리가 없습니다.

## 이 사람이 잘한 것 하나 더 — 「못한 것」을 자기가 적어 뒀습니다

마지막으로 배운 게 하나 더 있는데, 기술이 아니라 **태도**입니다.

이 사람은 창고에 두 가지를 같이 넣어 뒀습니다.

1. **목표로 삼은 참고 그림 넉 장** — 「무엇에 맞춰 만들었는지」를 남긴 것입니다
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 차이가 났다.

## 그리고 제 코드를 대봤다

> **[도판]** 가져온 것은 이 한 문장뿐이고, 그것이 가리킨 자리가 43곳이었다.
>
> 내 프로젝트에서 시간을 다루는 코드를 전수 조사한 결과, 시작 시점에 값을 굳히는 방식이 43곳이고 매 프레임 다시 읽는 방식이 22곳이었다.

넘어온 규칙은 이것 하나다.

> **어빌리티 인스턴스는 seed와 비율만 기억한다. 미터·초·라디안은 매 프레임 설정에서 다시 계산한다.**

제 무협 게임 프로젝트는 **정확히 반대로 짜여 있었다.** 보스가 초식을 낼 때 **전조 시간이 코루틴 시작 시점에 굳는다.** 플레이 중에 전조 시간을 바꿔도 **이미 진행 중인 전조에는 반영되지 않는다.**

전체를 세니 **시작 시점에 굳히는 방식 43곳, 매 프레임 다시 읽는 방식 22곳**이었다.

**이펙트 코드는 한 줄도 안 가져왔는데 제 코드 43곳의 성격이 정해졌다.** 이 분류에서 기대할 건 그런 종류다.

## 라이선스는 두 곳을 봐야 한다

**코드 포팅은 기각이다.** 라이선스상 자유롭지만 웹 셰이더와 3D 평면 전제라 **제 엔진의 2D 파이프라인으로 옮길 값이 없다.**

그리고 **통째로 가져가는 건 위험하다.** 같은 레포가 라이선스 파일에는 관대한 조건을 적어 두고 **README에는 "이 프로젝트 목적으로 있는 그대로 제공된다"고 다른 말을 한다.** 번들된 캐릭터 모델과 환경 프로브는 **원 라이선스 유지라고만 적혀 있다.**

**의존 대상이 아니라 교재로 다룰 것.** 5일 된 레포에 기여자 한 명이다. 게임 쪽 레포는 수명이 짧아 이 구분이 특히 중요하다.

## 마지막으로 — 이 레포가 잘한 것 하나 더

**목표로 삼은 참고 이미지 넉 장을 레포 루트에 넣어 뒀다.** "무엇에 맞춰 튜닝했는지"를 남긴 것이다. 그리고 README에 **"알려진 거친 부분" 일곱 건을 스스로 적었다** — 정렬 아티팩트, 평면 바닥 전제, 비어 있는 패스까지.

기법으로 가장 이식성 높은 건 **무차원 기록, 길이 0 프레임, 그리고 이 마지막 것**이다. **엔진과 언어에 무관한 설계 규칙**이기 때문이다. 나머지는 수동 엔진이라서 직접 해야 하는 일이라 — **엔진이 이미 해 주는 환경에서는 값이 급락한다.**
