반사신경 게임에서 난이도는 결국 "얼마나 빨리, 얼마나 자주 떨어지느냐"다. 처음에는 느긋해야 손에 익고, 어느 순간부터는 확실히 버거워야 한 판이 끝난다. 「네온 다이브」는 이 곡선을 공식 두 줄로 만들었다.

그리고 빨라진다는 사실을 숫자 대신 색으로 알려 준다. 20초마다 「PHASE 2」 글자가 뜨고 배경과 블록 색이 통째로 바뀐다.

속도 공식: 직선으로 오르다가 페이즈 상한에 걸린다

int phase = 1 + (int)(_t / PhaseSeconds);   // 20초마다 +1
if (phase != _phase) OnPhase(phase);

float cap = 2.2f + 0.3f * _phase;
_speed = Mathf.Min(cap, 1f + _t * 0.045f);

속도 배율은 1.0에서 시작해 초당 0.045씩 오른다. 동시에 페이즈마다 상한이 있다. 1페이즈 상한은 2.5, 페이즈가 하나 오를 때마다 0.3씩 올라간다.

초반에는 상한에 닿지 않아서 속도가 시간에 비례해 매끄럽게 오른다. 3페이즈 중간인 47초쯤부터 직선이 상한을 넘어서고, 그 뒤로는 페이즈가 바뀌는 순간에만 0.3씩 계단처럼 오른다.

이렇게 나눈 이유는 후반 난이도 때문이다. 직선만 쓰면 80초쯤에는 배율이 4.6이 되어 사람이 반응할 수 있는 속도를 넘는다. 상한을 걸어 두면 오래 버틴 플레이어도 "다음 페이즈까지는 이 속도"라는 구간을 얻는다. 페이즈가 바뀌는 순간이 진짜 고비가 되고, 그 고비를 넘으면 잠깐 적응할 시간이 생긴다.

숫자로 보면

블록 낙하 속도는 기본 620px/s에 배율을 곱하고, 블록마다 0.85~1.25배를 한 번 더 곱해 속도를 섞는다. 블록이 생기는 위치(y=1060)부터 바(y=-680)까지는 1,740px이다. 아래 표는 개체별 편차를 빼고 계산한 값이다.

시간 페이즈 속도 배율 낙하 속도 바까지 걸리는 시간 스폰 간격
0초 1 1.00 620px/s 2.8초 0.62초
10초 1 1.45 899px/s 1.9초 0.43초
20초 2 1.90 1,178px/s 1.5초 0.33초
30초 2 2.35 1,457px/s 1.2초 0.26초
40초 3 2.80 1,736px/s 1.0초 0.22초
50초 3 3.10 (상한) 1,922px/s 0.9초 0.20초
60초 4 3.40 (상한) 2,108px/s 0.8초 0.18초
80초 5 3.70 (상한) 2,294px/s 0.8초 0.17초

시작할 때는 블록이 화면 위에서 바까지 3초 가까이 걸린다. 40초가 되면 1초다. 이때부터는 보고 피하는 게 아니라 패턴을 읽고 미리 움직여야 한다.

스폰 간격도 속도에 묶었다

블록이 빨라지기만 하고 생기는 간격이 그대로면 화면에 블록이 점점 드문드문해진다. 빨라질수록 화면이 비어 보여 오히려 쉬워진다. 그래서 스폰 간격을 속도로 나눴다.

_spawnIn = Mathf.Max(0.14f, 0.62f / _speed) * Random.Range(0.8f, 1.2f);

이렇게 하면 화면에 동시에 떠 있는 블록 수가 속도와 상관없이 비슷하게 유지된다. 대신 블록이 지나가는 시간이 짧아지니 체감 밀도는 계속 올라간다. 0.14초는 바닥값이다. 상한이 계속 오르는 8페이즈(140초) 이후에도 간격이 더 줄지 않게 막아 둔다. ±20% 무작위를 곱해서 박자가 규칙적으로 느껴지지 않게 했다.

2페이즈부터 나오는 것들

같은 블록만 빨라지면 단조롭다. 페이즈마다 조금씩 다른 것을 섞었다.

블록이 넓어진다. 블록 너비는 90px부터 최대 너비 사이에서 고르는데, 최대 너비가 페이즈마다 20px씩 커진다. 1페이즈 260px에서 5페이즈 340px까지 오르고 거기서 멈춘다.

float maxW = 240f + 20f * Mathf.Min(_phase, 5);

좁은 틈만 남긴 벽이 나온다. 2페이즈부터는 블록 대신 15% 확률로 양쪽 벽 두 개가 동시에 떨어진다. 가운데 틈은 260~340px이다. 바 너비가 150px이니 틈 안으로 정확히 들어가야 한다.

// 페이즈 2 부터 가끔 좁은 틈만 남기는 벽 두 개
if (_phase >= 2 && Random.value < 0.15f)
{
    float gap = Random.Range(260f, 340f);
    float gx = Random.Range(-HW + gap * 0.5f + 60, HW - gap * 0.5f - 60);
    float h = Random.Range(50f, 70f);
    float lw = gx - gap * 0.5f + HW, rw = HW - (gx + gap * 0.5f);
    if (lw > 20) SpawnBlock(-HW + lw * 0.5f, lw, h);
    if (rw > 20) SpawnBlock(HW - rw * 0.5f, rw, h);
    return;
}

틈의 중심 gx는 화면 가장자리에서 60px 이상 떨어지게 잡는다. 틈이 화면 끝에 붙어 버리면 벽이 하나뿐인 셈이라 의미가 없다. 벽 두 개도 블록처럼 개체마다 속도를 따로 뽑는다. 그래서 한쪽 벽이 조금 먼저 내려오며 틈이 점점 어긋난다. 이 어긋남 때문에 틈으로 들어가는 타이밍이 매번 조금씩 다르다.

페이즈는 색으로 알려 준다

페이즈가 오르면 화면 가운데에 「PHASE 2」가 크게 떠올랐다 사라지고, 효과음과 진동이 함께 나온다.

20초가 지나 「PHASE 2」로 넘어가는 순간

동시에 팔레트가 바뀐다. 팔레트는 배경 위·아래 두 색과 블록 색, 세 가지로 이뤄진다.

static readonly Palette[] Palettes =
{
    new Palette("#0B1020", "#12204A", "#FF5F6D"),   // 1 남색 · 산호
    new Palette("#0D0B24", "#2B1657", "#FF6FB5"),   // 2 보라 · 분홍
    new Palette("#031A1C", "#0E4A4A", "#FF8C42"),   // 3 청록 · 주황
    new Palette("#1B0A1A", "#5A1440", "#FF4747"),   // 4 자주 · 빨강
    new Palette("#0A0A0A", "#3A0F0F", "#FF3B3B"),   // 5 검정 · 진홍
    new Palette("#041022", "#0B3B6B", "#FF5F6D"),   // 6 파랑 · 산호
};

여섯 개를 돌아가며 쓴다. 페이즈가 올라갈수록 블록은 빨개지고, 4·5페이즈에서는 배경까지 붉은 기가 돈다. 여기까지 버텼다는 것을 점수보다 먼저 색으로 느끼게 하고 싶었다.

2페이즈 보라색 팔레트. 블록이 분홍으로 바뀌었다

배경색은 한 번에 바뀌지 않고 1.25초 동안 이전 색에서 새 색으로 섞인다.

void UpdatePalette(float dt)
{
    if (_palT < 1f) _palT = Mathf.Min(1f, _palT + dt * 0.8f);
    _bgBottom.color = Color.Lerp(_from.bottom, _to.bottom, _palT);
    _bgTop.color = Color.Lerp(_from.top, _to.top, _palT);
}

블록 색은 섞지 않는다. 새로 생기는 블록부터 새 색으로 나오고, 이미 떨어지고 있던 블록은 원래 색 그대로 내려간다. 그래서 전환 직후 1~2초 동안은 두 색 블록이 섞여 보이고, 그 사이 배경이 천천히 바뀐다. 화면 전체가 한 프레임에 바뀌면 가장 빠르게 움직여야 할 순간에 눈이 색 변화에 끌려간다.

블록 색은 배경과 반대 계열로 골랐다. 배경은 모두 채도 낮은 어두운 남색·보라·청록이고, 블록은 채도 높은 산호·분홍·주황·빨강이다. 별(노랑)과 바(기본 시안)는 어느 팔레트에서도 바뀌지 않는다. 바뀌는 것은 피해야 할 것뿐이고, 내 바와 먹어야 할 것은 항상 같은 색이다.

슬로우 아이템을 먹은 5초 동안은 배경 위쪽에 보라색을 25% 섞는다. 속도가 절반이 된 걸 화면 색으로도 알 수 있게 했다.

정리

  • 속도 배율은 min(2.2 + 0.3 × 페이즈, 1 + 0.045 × 시간)이다. 47초쯤까지는 직선, 그 뒤로는 페이즈마다 계단처럼 오른다.
  • 스폰 간격은 0.62 / 속도(최소 0.14초)로 속도에 묶어 화면 밀도를 유지한다.
  • 페이즈마다 블록 최대 너비가 20px씩 커지고, 2페이즈부터는 260~340px 틈만 남긴 벽이 섞인다.
  • 페이즈 전환은 배경 1.25초 페이드와 블록 색 교체로 알린다. 바·별 색은 고정해 두고 피해야 할 블록만 바꾼다.

다음 글 4편 (10월 14일 공개)에서는 한 판이 끝난 뒤에도 다시 켜게 만드는 데일리 미션과 스킨 상점을 다룬다.