블록을 피하는 것만으로는 재미가 오래가지 않는다. 멀찍이 떨어져서 피하면 안전하지만 할 일이 없다. 그래서 위험하게 피할수록 보상을 주기로 했다. 블록을 바 옆으로 아슬아슬하게 스치면 「아슬!」 점수가 붙는다.

이 글에서는 그 니어미스 판정과 콤보 배수, 그리고 반대로 플레이어가 억울하지 않게 만든 장치들을 다룬다.

니어미스는 "지나가는 순간" 한 번만 본다

니어미스를 매 프레임 거리로 판정하면 같은 블록이 여러 번 점수를 준다. 블록 하나에 한 번만 판정하려고 기준 시점을 정했다. 블록의 아랫면이 바의 윗면을 지나가는 순간이다.

// 니어미스: 블록 아랫면이 바 윗면을 지나는 순간, 옆으로 40px 이내
if (o.kind == Kind.Block && !o.passed && oy1 < pt)
{
    o.passed = true;
    float gap = ox1 > pr ? ox1 - pr : (ox2 < pl ? pl - ox2 : 0f);
    if (gap > 0 && gap < NearGap) NearMiss(o);
}

passed 플래그로 블록마다 한 번만 검사한다. gap은 블록이 바의 오른쪽에 있으면 블록 왼쪽 끝과 바 오른쪽 끝의 거리, 왼쪽에 있으면 그 반대다. 둘 다 아니면 겹친 것이니 0이다. 겹친 경우는 이미 앞에서 충돌로 처리되고, 0보다 크고 40px보다 작을 때만 니어미스가 된다.

40px은 바 높이(34px)와 비슷한 값이다. 이보다 넓으면 그냥 피해도 계속 「아슬!」이 떠서 의미가 없고, 좁으면 거의 나오지 않는다.

「아슬! +40」 니어미스가 연달아 나오며 콤보 8, 배수 ×1.8

콤보 배수는 3초 안에 이어 가야 한다

별을 먹거나 니어미스를 내면 콤보가 1 오르고, 콤보 하나에 배수가 0.1씩 커진다.

float Mult => Mathf.Min(3f, 1f + 0.1f * _combo);

void AddCombo()
{
    _combo++;
    _comboTimer = 3f;
}

콤보는 3초 동안 아무것도 못 하면 0으로 돌아간다. 배수는 최대 3.0(콤보 20)에서 멈춘다.

콤보 배수 별 하나 니어미스 하나
0 ×1.0 50 25
6 ×1.6 80 40
8 ×1.8 90 45
12 ×2.2 110 55
20 이상 ×3.0 150 75

이 규칙 덕분에 플레이 방식이 달라진다. 별이 화면 반대편에 있으면 블록 사이로 무리해서 가야 콤보가 이어진다. 블록이 몰려오면 일부러 가장자리에 붙어 스친다. 안전하게 피하기만 하면 초당 점수만 쌓이고, 위험을 감수하면 점수가 몇 배로 붙는다.

콤보는 2 이상일 때만 화면 위에 「콤보 8 ×1.8」처럼 띄운다. 1일 때 보여 주면 별 하나 먹을 때마다 깜빡여서 정신이 없다.

기록을 넘는 순간 바로 알려 준다

결과 화면에서야 신기록을 알게 되면 김이 빠진다. 판이 진행되는 도중 점수가 최고 기록을 넘는 순간 화면 가운데에 「NEW BEST!」를 띄우고 효과음을 낸다. 한 판에 한 번만 띄우고, 첫 판(최고 기록 0)에는 띄우지 않는다.

플레이 중 기록을 넘는 순간 뜨는 「NEW BEST!」

억울하지 않게 — 판정 박스는 블록보다 작게

반사신경 게임에서 가장 나쁜 경험은 "분명 피했는데 죽었다"다. 블록에는 둥근 모서리와 바깥 글로가 있어서, 눈에 보이는 가장자리와 실제 사각형이 조금 다르다. 그래서 충돌 판정에 쓰는 사각형을 블록보다 작게 잡았다.

// 판정 박스는 살짝 작게 (억울한 죽음 방지)
float sx = o.kind == Kind.Block ? o.w * 0.08f : 0, sy = o.kind == Kind.Block ? o.h * 0.15f : 0;
bool overlap = ox1 + sx < pr && ox2 - sx > pl && oy1 + sy < pt && oy2 - sy > pb;

가로는 양쪽 8%씩, 세로는 위아래 15%씩 줄인다. 너비 200px 블록이면 양옆이 16px씩 줄어든다. 별과 아이템은 줄이지 않는다. 먹는 쪽은 넉넉하고 죽는 쪽은 박하게 판정하는 셈이다.

반대로 니어미스 간격은 줄이지 않은 원래 크기로 잰다. 그래서 눈으로 보기에 모서리가 살짝 겹친 경우는 죽지도 않지만 「아슬!」도 뜨지 않는다. 보너스는 겹치지 않고 스쳐 지나간 경우에만 준다. 판정을 너그럽게 한 부분까지 점수로 돌려주면 일부러 겹치게 붙는 플레이가 유리해진다.

쉴드는 깨질 때 확실하게 보여 준다

청록색 링을 먹으면 12초 동안 바 주위에 빛이 깜빡이고, 그동안 블록 한 번을 막는다. 막는 순간에는 블록이 파편으로 터지고, 화면이 흔들리고, 「쉴드 소모」 글자가 뜬다. 쉴드가 있었는지 모르고 지나가면 아이템의 의미가 없어서 효과를 일부러 크게 넣었다.

청록색 링이 쉴드 아이템이다

아이템은 시작하고 8초가 지나야 나온다. 이미 쉴드가 있을 때는 쉴드 대신 슬로우만 나온다.

일시정지는 바로 재개하지 않는다

오른쪽 위 버튼, Android 뒤로가기, 앱 전환 중 어느 것으로든 일시정지된다. Time.timeScale을 0으로 두고 「계속하기 / 그만하기」를 보여 준다.

일시정지 화면

계속하기를 눌러도 바로 이어지지 않는다. 「화면을 눌러 계속」 안내가 뜨고, 화면을 다시 눌러야 시작된다.

재개 안내 「화면을 눌러 계속」

계속하기 버튼은 화면 가운데에 있고 바는 화면 아래에 있다. 버튼을 누르는 순간 바로 판이 흐르면, 손가락이 바 위치로 내려가기 전에 블록이 떨어진다. 판을 처음 시작할 때와 같은 「대기」 상태로 돌려보내서 손가락을 원하는 곳에 둔 다음 이어 가게 했다.

void Resume()
{
    if (_state != State.Paused) return;
    Time.timeScale = 1f;
    SetState(State.Ready);
    _readyText.text = "화면을 눌러 계속";
}

바의 이동도 같은 이유로 상대 이동이다. 손가락이 닿은 곳으로 바가 순간이동하는 게 아니라, 누른 뒤 움직인 거리만큼(감도 1.15배) 따라간다. 화면 어디를 눌러도 바가 튀지 않는다.

정리

  • 니어미스는 블록 아랫면이 바 윗면을 지나는 순간 한 번만, 옆 간격 0~40px로 판정한다.
  • 별과 니어미스가 콤보를 올리고, 배수는 1 + 0.1 × 콤보(최대 3.0)다. 3초 비면 콤보가 끊긴다.
  • 충돌 박스는 블록보다 가로 8%·세로 15% 작게, 니어미스 간격은 원래 크기로 잰다. 살짝 겹치면 살아남기만 하고, 보너스는 겹치지 않고 스쳤을 때만 준다.
  • 일시정지에서 돌아올 때는 대기 상태를 한 번 거친다. 바는 상대 이동이라 손가락을 어디에 두어도 튀지 않는다.

다음 글 3편 (10월 13일 공개)에서는 시간이 갈수록 빨라지는 난이도 곡선과 페이즈마다 바뀌는 색을 다룬다.