홈 › 읽을거리 › 개발노트

슬리더 블룸을 만들며 겪은 일: 첫 틱에서 멈추던 충돌 격자

개발노트 · 2026-10-10 · GamePool 편집팀

슬리더 블룸은 2026년 10월 10일에 올라간 게임으로, 외부 라이브러리 없이 한 파일(public/games/slitherbloom/index.html, 작성 시점에 약 72KB)에 모두 들어 있습니다. 이 글은 그 안의 구조와 만들면서 실제로 겪은 일, 직접 실행해서 얻은 수치를 적은 기록입니다. 수치는 게임 소스를 브라우저(헤드리스 Chromium)에서 불러와 시뮬레이션을 반복 실행하거나 화면을 그려 보며 얻은 값이고, 확인하지 못한 것은 적지 않았습니다. 소리는 실제로 들어 보지 못했습니다.

구조: 시뮬레이션과 화면을 나눠 둔 한 파일

파일 안은 GameStorage, World(시뮬레이션), BotAI, NetworkManager, 렌더러, 입력(PlayerController), HUD와 순위표로 나뉘어 있습니다. 시뮬레이션인 World 와 BotAI 는 DOM 이나 캔버스를 전혀 쓰지 않아서, 나중에 서버에서 돌릴 수 있게 의도적으로 분리했습니다. NetworkManager 는 지금 "오프라인" 문구와 접속자 수 1을 돌려주는 로컬 구현뿐입니다. 사람과 겨루는 온라인 서버는 만들지 않았습니다. 이 저장소의 서버가 닉네임 랭킹용 Cloudflare Worker 하나뿐이고, 실시간 방을 붙이려면 Durable Object 구성과 배포 설정을 새로 정해야 하기 때문입니다. 그 설계 메모는 저장소의 docs/slitherbloom-online.md 에 두었고 구현은 하지 않았습니다.

몸통은 앞 마디를 따라가기만 한다

몸통은 마디의 좌표 배열이고, 매 틱 머리를 움직인 뒤 각 마디가 바로 앞 마디와의 거리가 간격보다 멀면 그 초과분만큼만 끌려가도록 했습니다. 간격은 머리 반지름의 0.52배입니다. 앞 마디를 따라가기만 하므로 급하게 방향을 꺾어도 마디가 갑자기 튀거나 끊기지 않고, 길이가 늘 때는 새 마디가 꼬리 위치에서 겹친 채 생겨 꼬리가 움직이면서 자연스럽게 펴집니다. 마디 수는 8에 길이의 0.8제곱에 0.7을 곱한 값을 더한 수이고 최대 320개입니다. 불러 본 값은 길이 10에서 12개, 100에서 35개, 450에서 100개, 1,800에서 289개였습니다. 간격을 반지름에 비례시켰기 때문에 몸이 커져도 마디 겹침 정도가 일정합니다.

충돌 격자와 비용 실측

충돌은 매 틱 모든 몸통 마디를 100유닛 칸의 격자(연결 리스트, 배열 재사용)에 넣어 두고, 각 머리가 자기 주변 칸의 마디만 검사합니다. 머리와 마디는 원으로, 머리 반지름 0.7배와 마디 반지름 0.8배의 합 안이면 닿은 것으로 판정합니다. 사망은 한 틱 안에서 모아 두었다가 한꺼번에 처리하기 때문에 같은 지렁이가 두 번 죽거나 같은 먹이를 둘이 먹는 일이 없습니다. 먹이는 200유닛 칸의 격자에 들어 있고 먹히기 전에 목록에서 먼저 지웁니다.

렌더링 없이 봇 22마리, 먹이 7,000개로 300초를 돌리는 시뮬레이션의 한 틱 평균 비용은 먹이 4,200개일 때 0.17밀리초, 7,000개일 때 0.19밀리초였습니다. 봇 22마리가 60초 동안 돌아다닌 시점의 몸통 마디 수는 941개였습니다. 화면을 그리는 비용은 캔버스가 소프트웨어 렌더링인 헤드리스 Chromium 에서 한 프레임에 약 7~9밀리초였고(1280×720, 강제로 화면을 읽어 와 잰 값), 실제 기기에서의 프레임률은 확인하지 못했습니다. 성능이 낮은 기기를 위해 시작 화면에 낮음 화질을 두고, 120프레임 중 60프레임이 26밀리초를 넘으면 자동으로 낮춥니다. 이 자동 전환이 실제 기기에서 잘 작동하는지는 확인하지 못했습니다.

봇 판단: 열여섯 방향에 점수를 매긴다

봇은 일정 간격(성격에 따라 0.12~0.30초)마다 현재 방향을 중심으로 16개 방향 후보를 만들고 각각에 점수를 매깁니다. 먹이 가치를 거리로 나눈 값(희귀 먹이는 성격에 따라 가중), 사냥 대상 쪽으로의 정렬, 큰 상대에게서 멀어지는 정도, 그리고 앞쪽 세 지점에서 다른 지렁이 몸통까지의 여유 거리와 경계와의 거리를 반영한 위험을 합산합니다. 가장 점수가 높은 방향을 목표로 잡으면 실제 머리는 플레이어와 같은 회전 속도 제한 안에서 돌아갑니다. 시야 밖의 지렁이와 먹이는 보지 못합니다. 성격은 다섯 가지 가중치 묶음이고 시야, 판단 간격, 사냥 예측, 부스터 사용 확률만 다릅니다.

실제로 겪은 버그: 로드가 끝나지 않던 이유

처음 브라우저에서 열어 보니 페이지가 로드 완료에 이르지 못하거나 탭이 갑자기 사라지는 일이 간헐적으로 있었습니다. 같은 파일을 다섯 번 열었을 때 두 번이 그랬습니다. 처음에는 그리기 부담을 의심해 패널의 backdrop-filter 와 경계선의 shadowBlur 를 걷어 냈지만 증상은 그대로였습니다(이 두 가지는 원인이 아니었음이 확인됐고, 성능 영향은 따로 재지 않았습니다). 시뮬레이션만 따로 돌리면 문제가 없었고, 그리기 코드를 단독으로 한 번 불러도 문제가 없어서 둘이 합쳐진 첫 프레임이 의심스러웠습니다.

원인은 충돌 격자의 머리 배열이 처음에 0으로 채워져 있다는 점이었습니다. 배열의 비어 있음 표시는 -1 인데 Int32Array 는 0으로 만들어지므로, 아직 한 번도 격자를 만들지 않은 상태에서 격자를 읽으면 0번 마디의 "다음"이 다시 0번이 되어 연결 리스트를 영원히 돌았습니다. 그런 읽기는 봇의 판단 코드(앞쪽 여유 거리 검사)가 첫 틱에 실행될 때만 일어납니다. 봇의 첫 판단 시점은 0~0.3초 사이에서 무작위로 정하는데, 그 값이 한 틱(1/60초) 이하로 걸리면 격자가 만들어지기 전에 판단이 실행됩니다. 봇 한 마리가 그럴 확률은 약 5.6%이고, 봇이 9마리인 시작 화면 배경에서는 계산상 약 40%, 22마리인 본 게임에서는 약 71%입니다. 다섯 번 중 두 번 걸린 관찰과 맞습니다.

수정은 한 줄입니다. 머리 배열을 만들 때 -1로 채웠습니다. 수정 후에는 같은 방식으로 반복해서 열어도 멈추지 않았고, 이어서 봇만 돌리는 시뮬레이션을 300초씩 세 번 돌릴 때도 멈추지 않았습니다. 이 버그를 찾는 과정에서 배운 점은, 기본값이 0인 타입 배열을 연결 리스트의 머리에 쓸 때는 0이 유효한 인덱스라는 사실을 잊기 쉽다는 것입니다.

화면을 보고 고친 값

길이를 420으로 만들어 캡처해 보니 몸이 너무 가늘게 보였습니다. 반지름 식을 8에 길이 제곱근의 0.42배(상한 16)에서 0.5배(상한 18)로, 몸이 클 때의 줌 감소 계수를 0.042에서 0.03으로 바꿨습니다. 또 먹이를 4,200개로 두었더니 풀밭이 아닌 곳에서는 한 화면에 열댓 개밖에 보이지 않아 7,000개로 늘렸고, 낮음 화질은 그 60%인 4,200개를 쓰도록 했습니다. 이 값들은 눈으로 본 판단이고 많은 플레이로 검증한 것은 아닙니다.

이 게임에서 확인한 것과 못한 것

  • 몸통 충돌: 길이 80의 내가 봇 몸통에 부딪히면 사망 판정과 함께 봇의 처치가 1 오르고 먹이 26개(길이를 3으로 나눈 값)가 떨어졌습니다.
  • 머리 정면충돌: 양쪽이 모두 죽고 서로 처치 1씩이 올랐습니다.
  • 경계: 가장자리 쪽으로 직진하면 0.6초 안에 죽고, 결과 창에 "경기장 경계에 부딪혔습니다"가 표시됐습니다.
  • 부스터: 길이 60에서 1초에 332.5유닛(기본 190의 1.75배)을 움직였고, 길이 11에서는 켜지지 않았습니다.
  • 재시작: 결과 창에서 R 키를 누르면 페이지를 새로 읽지 않고 새 판이 바로 시작됐습니다. ESC 일시정지와 재개도 정상이었습니다.
  • 터치: 에뮬레이션한 터치 이벤트(기준점에서 오른쪽 위로 40,−30 이동)가 진행 방향 −0.64라디안으로 반영됐습니다. 실제 터치 기기에서는 확인하지 못했습니다.
  • 소리: 기존 효과음(먹이 pop, 희귀 먹이 powerup, 부스터 whoosh, 처치 kill, 사망 explode와 lose, 최고 기록 win과 levelup)을 가져다 썼고 배경음은 space 입니다. 실제로 들어 보지는 못했습니다.

봇 성격 시뮬레이션 결과와 한계

봇 22마리, 먹이 7,000개로 화면 없이 300초씩 세 번 돌렸습니다. 봇 사망은 모두 271번이었고 몸통 충돌 213번, 머리 정면충돌 58번, 경계 사망은 0번이었습니다. 성격별 평균 생존은 초보 73초, 회피 61초, 수집 50초, 숙련 43초, 공격 39초였고, 평균 최고 길이는 초보 519, 회피 373, 수집 355, 공격 234, 숙련 200이었습니다. 300초 시점의 1위 길이는 세 번 각각 1,329, 1,381, 1,656이었습니다.

기대와 어긋난 점은 이름과 달리 초보형이 가장 오래 살고 가장 크게 자란다는 것입니다. 위험한 추격을 하지 않고 먹이만 먹는 단순한 행동이 이 규칙에서는 가장 안정적이었습니다. 이 결과가 한 번의 설계에서 나온 것이라 성격 이름과 실제 강약이 일치하지 않으며, 균형을 맞추는 일은 아직 하지 않았습니다. 그 밖의 한계는 이렇습니다. 닉네임 랭킹에는 이 기기에서 계산한 점수를 그대로 보고하기 때문에 서버가 점수를 검증하지 못합니다. 사람과 겨루는 멀티플레이와 접속 끊김·재접속 처리는 없습니다. 봇은 시야 밖을 보지 못하는 대신 포위나 협공 같은 계획 행동은 하지 않고, 공격형과 숙련형의 가로막기도 한 번에 한 상대만 노립니다.

관련 게임 플레이하기

다른 읽을거리

가이드정적 사이트에 워커 하나: GamePool 구조의 선택과 한계게임 32개가 파일 하나씩인 이유, /api/* 만 처리하는 워커를 둔 이유, 저장소가 막힌 시크릿 모드 대응, 복제 방지 장치까지. 실제 소스와 설정 파일을 근거로 선택과 한계를 적었습니다.입문슬리더 블룸 입문: 보호막 2.5초부터 첫 처치까지슬리더 블룸의 화면 읽는 법, 먹이 값과 몸 크기, 부스터가 쓰는 길이, 정면충돌과 가로막기의 차이, 경계, 봇 성격별 대처를 게임 소스와 직접 실행한 수치로 정리한 입문 글입니다.개발노트군단 점령전 연타 배수문, 세 번 고쳐 맞춘 기록군단 점령전의 연타 배수문과 부서지는 벽을 어떤 수치로 세 번 조정했는지, 실패한 두 설정과 최종 상수, 내구도 공식을 소스와 커밋 근거로 되짚은 개발 노트입니다.개발노트알까기에서 승리 소리가 끝없이 울린 이유게임이 끝난 뒤에도 승패 효과음이 반복되고 승리 횟수가 계속 오르던 알까기 버그를 settle() 흐름과 한 줄 수정, 같은 유형 점검 결과로 정리했습니다.개발노트게임 소개 문구와 소스가 어긋난 열세 곳 정정 기록게임 소개 문구를 소스 코드와 하나씩 맞춰 보며 바로잡은 열세 가지 사례를 변경 전후 문장, 근거가 된 소스 값, 어긋난 이유, 점검 방식과 함께 적었습니다.개발노트솔저 러쉬 개발 노트: 넓은 도로와 난이도 조정도로를 9에서 15로 넓히자 게이트 글씨가 늘어났고, 쉬워졌다는 의견에 체력 곡선과 무리 크기를 다시 맞춘 과정을 변경 전후 수치로 적었습니다.