GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

벽돌깨기 게임 9종 — 패들 반사각부터 빠른 공 스윕 충돌과 연쇄 폭발까지

벽돌깨기는 공이 어디에 맞았는지를 어떻게 방향으로 바꿀까 — 01번은 패들 중심에서 맞은 자리까지의 거리로 반사각을 정하고, 02번은 한 프레임의 이동 구간을 쪼개 벽돌을 건너뛰는 공을 잡으며, 08번은 벽 반사를 펴서 낙하 지점을 미리 계산한다.

벽돌깨기는 공을 튕기는 그림이 아니라 맞은 위치와 이동 구간 두 값으로 반사 방향·충돌 순서·벽돌 상태를 모두 계산하는 장치다 — 벽돌깨기 게임하기는 어느 값이 공의 방향을 정하는지 아는 데서 시작한다

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 장면은 같은 계산 틀을 쓰고 장면마다 스위치 한두 개와 상수 몇 개만 다르다. 벽돌깨기를 화면이 아니라 상태로 보면 공은 위치·속도·회전, 벽돌은 체력과 시각, 패들은 가로 위치와 속도일 뿐이고, 캔버스는 그 값을 칠하는 일만 한다.

공·패들·벽돌은 한 계산 틀에서만 나온다

World 가 Ball·Brick·Mode 를 들고 있고, 한 걸음 stepWorld 가 패들 조종 → 공마다 moveBall → 벽돌 reflectBricks 순으로 값을 바꾼다. drawWorld 는 그 값을 읽어 그리기만 하므로 장면이 달라도 충돌 결과는 같은 함수에서 나오고, 오른쪽 대비판은 Mode 의 스위치만 다르게 켠 같은 계산이다.

고정 걸음 1/120초와 DT_MAX 0.05

rAF 가 알려 주는 경과 시간은 DT_MAX 0.05초에서 자르고, 쌓인 시간을 STEP_H 1/120초 걸음으로 쪼개 stepWorld 를 부른다. 모니터가 60Hz 든 144Hz 든 한 걸음의 계산 시간이 같아서 공의 궤적이 같고, 탭이 오래 숨었다 돌아와도 한 번에 0.05초 넘게 진행하지 않는다. 예외는 02 하나다. 02 는 w.step 을 FRAME_H 1/60초로 바꾸고 advanceWorld(w, dt × SLOW) 로 시간을 0.3배로 늦춰 부른다.

    // 시간을 고정 걸음으로 쪼개 부른다 — 한 번에 DT_MAX 를 넘는 시간은 이어 붙이지 않는다
    function advanceWorld(w, dt) {
        w.acc += Math.min(dt, DT_MAX);
        while (w.acc >= w.step - 1e-9) {
            stepWorld(w, w.step);
            w.acc -= w.step;
        }
    }

난수는 균열 모양에만 쓴다

공의 움직임에는 난수가 없다. 발사 각도는 LAUNCH_DEG, 패들에 맞는 자리는 AIMS·aims, 분열 각도는 ANGLE_TABLE 처럼 미리 적은 표에서 차례로 꺼낸다. 난수는 곱 1664525·더함 1013904223 의 시드 고정 생성기 seeded 하나뿐이고, 벽돌 줄·칸으로 시드를 잡아 균열 선의 모양에만 쓴다.

    function crackLines(k, n) {
        const rng = seeded(k.row * 131 + k.col * 17 + 7);
        for (let i = 0; i < n; i += 1) {
            const sx = k.x + k.w * (0.2 + 0.6 * rng());
            const top = (i % 2) === 0;
            let cx = sx;
            let cy = top ? k.y : k.y + k.h;
            const ty = top ? k.y + k.h * 0.9 : k.y + k.h * 0.1;
            const steps = 3;
            for (let s = 0; s < steps; s += 1) {
                const nx = cx + (rng() - 0.5) * k.w * 0.34;
                const ny = cy + (ty - cy) / (steps - s);
                seg(cx, cy, nx, ny, 1.4, INK, 0.62);
                cx = nx;
                cy = ny;
            }
        }
    }

벽돌깨기 9종 — 패들 위치로 정하는 반사각·한 프레임을 쪼개는 스윕 충돌·모서리 법선 고르기·패들 속도가 만드는 휘는 궤적·고정 각도표 멀티볼·체력별 균열·속도 단계와 잔상·낙하 지점 예측 패들·문자열 격자 레벨과 연쇄 폭발

아홉 장면을 렌더해서 잰 면적·프레임·강도

장면 누적면적 움직인 프레임 강도 평균
01 반사각 11.2% 23/23 90.7
02 스윕 충돌 29.8% 22/23 62.5
03 모서리 법선 11.3% 23/23 87.4
04 회전 전달 15.1% 23/23 67.9
05 멀티볼 12.9% 23/23 88.9
06 체력 균열 7.9% 21/23 86.8
07 속도 단계 9.1% 23/23 93.9
08 낙하 예측 14.8% 23/23 87.4
09 문자열 레벨 12.4% 23/23 86.8

누적면적이 가장 넓은 것은 02 의 29.8% 이고 가장 좁은 것은 06 의 7.9% 다. 움직인 프레임은 02 가 22/23, 06 이 21/23 이고 나머지 일곱 장면은 23/23 이며, 강도 평균은 07 이 93.9 로 가장 높다.

01패들 맞은 자리로 정하는 반사각

패들로 공의 방향을 조준하는 규칙을 처음 배울 때 가장 먼저 만지는 값이라, 패들 중심에서 공까지의 거리를 반폭으로 나눈 hitOffset 이 −1~1 이고 반사각은 u × MAX_ANGLE 58° 다. 오른쪽 판은 vy = −|vy| 거울 반사만 써서 같은 줄을 오르내린다. 맞은 자리 −1·−0.5·0·0.5·1 은 −58.0°·−29.0°·0°·29.0°·57.9° 였고 12초 동안 왼쪽은 벽돌 11장, 오른쪽은 4장을 깼다.

패들 맞은 자리로 정하는 반사각
새 창에서 열기

01 · 패들 맞은 자리로 정하는 반사각 — 측정 누적면적 11.156% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

PADDLE_WhitOffsetMAX_ANGLE
                b.y = w.py - BALL_R;
                const u = hitOffset(w, b);
                if (m.angle === 0) {
                    const a = u * MAX_ANGLE;
                    b.vx = Math.sin(a) * w.speed;
                    b.vy = -Math.cos(a) * w.speed;
                }
                else {
                    b.vy = -Math.abs(b.vy);
                }

02빠른 공을 잡는 스윕 충돌

공이 빨라져도 벽돌을 건너뛰지 않게 하는 충돌 처리라서, 한 프레임의 이동을 STEP_MAX 4px 이하로 올림해 쪼개고 조각마다 벽돌을 검사한다. 이 장면은 프레임당 42px 를 가서 11조각이 되고, 벽돌 두께 6 에 공 지름 10 을 더한 16px 보다 길면 끝 위치만 보는 판은 한 줄을 건너뛴다. 시작 위상 200가지로 쏘면 스윕은 0%, 끝 위치만 검사하는 판은 62.0% 가 통과해 식 (이동 − 16) ÷ 이동 의 61.9% 와 맞고, 화면 장면 12초에는 오른쪽만 12번 통과했다.

빠른 공을 잡는 스윕 충돌
새 창에서 열기

02 · 빠른 공을 잡는 스윕 충돌 — 측정 누적면적 29.834% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STEP_MAXFRAME_HBALL_SPEED
    // 한 프레임에 공이 가는 길이(px) = BALL_SPEED × FRAME_H. 벽돌 두께 + 공 지름보다 길면 한 줄을 통째로 건너뛴다
    const FRAME_H = 1 / 60;
    const BALL_SPEED = 2520;
    const BRICK_H = 6;
    // 스윕은 한 프레임 이동을 STEP_MAX 이하 조각(올림(이동 ÷ STEP_MAX) 개)으로 쪼갠다
    const STEP_MAX = 4;
    // 장면 시간을 늦춰 보여 준다 — 공이 너무 빨라 눈으로 따라갈 수 없다
    const SLOW = 0.3;

03모서리에서 반사 면을 고르는 법선

벽돌 모서리를 스친 공이 엉뚱한 쪽으로 가지 않게 하는 반사 규칙으로, 겹친 깊이를 가로 ox·세로 oy 로 재서 얕은 축의 면에서 밀어내고 그 축의 속도만 뒤집는다. 기둥과 막대는 hp 99 라 깨지지 않고 hurt 가 맞힌 횟수만 늘리며, 오른쪽 판은 vy = −vy 만 써서 옆구리를 맞으면 벽돌 안으로 파고든다. 기둥 하나를 5곳×41각도 205번 쏴서 공 중심이 기둥 안에 6걸음 이상 박힌 경우를 세면 면 고르기는 0번, 속도 y 만 뒤집기는 66번이었다.

모서리에서 반사 면을 고르는 법선
새 창에서 열기

03 · 모서리에서 반사 면을 고르는 법선 — 측정 누적면적 11.336% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

reflectBricksLAUNCH_DEGface
        if (w.mode.face === 0) {
            // 겹침 깊이가 얕은 축의 면이 맞은 면 — 그 면의 바깥 쪽으로 밀어내고 그 축의 속도만 뒤집는다
            const ox = BALL_R + k.w / 2 - Math.abs(b.x - cx);
            const oy = BALL_R + k.h / 2 - Math.abs(b.y - cy);
            if (ox < oy) {
                const dir = b.x < cx ? -1 : 1;
                b.x += dir * ox;
                b.vx = dir * Math.abs(b.vx);
            }
            else {
                const dir = b.y < cy ? -1 : 1;
                b.y += dir * oy;
                b.vy = dir * Math.abs(b.vy);
            }
        }
        else {
            // 대비판 — 면을 고르지 않고 위아래 속도만 뒤집는다. 옆구리를 맞으면 벽돌 안으로 파고든다
            if (w.t - k.hitT < h * 1.5) {
                w.stuck += 1;
            }
            b.vy = -b.vy;
        }

04패들 속도가 공을 휘게 하는 회전 전달

패들을 움직이며 치는 기술에 의미를 주려고 맞는 순간 패들 속도에 SPIN_GAIN 0.15 를 곱해 회전으로 넘기고, 회전은 걸음마다 SIDE_PUSH 만큼 공을 옆으로 밀며 SPIN_DECAY 로 줄어든다. 왼쪽 패들은 −300px/초 로 달려 회전 −45 를 받았고 오른쪽은 선 채로 회전 0 이다. 같은 자리 x 111 에 공 가로 속도 86.7px/초 로 맞아도 0.5초 뒤 가로 이동은 +19.3px 대 +43.4px 로 갈렸다.

패들 속도가 공을 휘게 하는 회전 전달
새 창에서 열기

04 · 패들 속도가 공을 휘게 하는 회전 전달 — 측정 누적면적 15.136% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPIN_GAINSPIN_DECAYSIDE_PUSH
        const m = w.mode;
        if (m.spin > 0) {
            // 회전은 프레임마다 옆으로 미는 힘이 되고 서서히 줄어든다
            b.vx += b.spin * m.push * h;
            b.spin *= Math.exp(-m.decay * h);
        }

05고정 각도표로 갈라지는 멀티볼

아이템으로 공이 늘어나는 규칙을 난수 없이 만들려고, + 벽돌이 깨질 때 ANGLE_TABLE 의 다음 값을 새 공 SPLIT_N 개의 각도로 쓰고 BALL_MAX 에서 멈춘다. 같은 판을 두 번 돌려 공 위치와 깨진 수를 해시로 묶어 비교하면 12초 동안 초마다 12/12 가 일치했다. 12초 동안 분열 5번에 최대 공 5개, 벽돌 23장이 깨졌고 한 걸음의 검사 횟수는 공마다 같다.

고정 각도표로 갈라지는 멀티볼
새 창에서 열기

05 · 고정 각도표로 갈라지는 멀티볼 — 측정 누적면적 12.935% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ANGLE_TABLESPLIT_NBALL_MAX
    function onSplit(w, k) {
        if (k.kind !== 1) {
            return;
        }
        for (let i = 0; i < SPLIT_N; i += 1) {
            if (w.balls.length >= BALL_MAX) {
                return;
            }
            const b = newBall(w, w.balls.length + splits * 10);
            const a = ANGLE_TABLE[tableAt % ANGLE_TABLE.length] * Math.PI / 180;
            tableAt += 1;
            b.x = k.x + k.w / 2;
            b.y = k.y + k.h + BALL_R;
            b.vx = Math.sin(a) * w.speed;
            b.vy = Math.cos(a) * w.speed;
            b.st = 0;
            w.balls.push(b);
        }
        splits += 1;
    }

06체력에 따라 갈라지는 벽돌 균열

여러 번 쳐야 깨지는 단단한 벽돌은 체력 정수 하나로 표현해, hurt 가 맞을 때마다 hp -= 1 을 하고 남은 체력은 색 단계, 맞은 횟수는 균열 선 개수로 바뀐다. 위에서 hp 1·2·3·4 로 쌓은 10×4 판 40장은 hp 1 이 16장, 2·3·4 가 각 8장이라 모두 깨려면 88번을 맞혀야 한다. 12초에는 맞힘 28번에 벽돌 9장이 깨졌고 동시에 금간 벽돌은 최대 6장이었다.

체력에 따라 갈라지는 벽돌 균열
새 창에서 열기

06 · 체력에 따라 갈라지는 벽돌 균열 — 측정 누적면적 7.872% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

HP_MAXCRACK_NTINT_STEP
    function hurt(w, k) {
        k.hitT = w.t;
        w.hits += 1;
        if (k.hp >= 90) {
            return;
        }
        k.hp -= 1;
        if (k.hp <= 0) {
            k.deadT = w.t;
            w.broken += 1;
            w.lastBreak = w.t;
            if (w.onBreak) {
                w.onBreak(w, k);
            }
        }
    }

07속도 단계에 따라 길어지는 잔상

라운드가 진행될수록 긴장이 오르는 속도 곡선은 speedFor 가 깬 수 ÷ CLEAR_N 3 의 몫만큼 SPEED_STEP 70 을 더해 SPEED_MAX 590 에서 자르고, 잔상 길이는 round(속도 × TRAIL_LEN) + 2 로 따라 늘린다. 깬 수 0 이면 속도 190 에 잔상 16, 18장이면 속도 590 에 잔상 46 이다. 12초 동안 24장을 깼고 한계 속도에는 10.0초에 닿았으며 걸음이 STEP_MAX 조각으로 쪼개져 통과는 0번이었다.

속도 단계에 따라 길어지는 잔상
새 창에서 열기

07 · 속도 단계에 따라 길어지는 잔상 — 측정 누적면적 9.067% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPEED_STEPTRAIL_LENCLEAR_N
    function update(dt) {
        const w = WS[0];
        // 단계가 오르면 속도와 잔상 길이가 같이 바뀐다 — 한 걸음 이동은 늘 4px 이하 조각으로 쪼개진다(02번 스윕 충돌과 같다)
        w.speed = speedFor(w.broken);
        w.trailN = Math.round(w.speed * TRAIL_LEN) + 2;
        advanceWorld(w, dt);
    }

08벽 반사를 펴서 낙하 지점을 예측하는 패들

자동 플레이 시연과 연습 상대 패들은 벽 반사를 펴서 낙하 지점을 구한다. 공이 패들 높이에 닿을 시각 t 에서 b.x + b.vx × t 를 foldW 로 판 폭 안에 접어 넣고 패들은 FOLLOW_EASE 로 그 지점에 따라가며, 오른쪽 판은 공의 지금 가로 위치만 따라간다. 24초 동안 놓침은 0 대 7 이었고 낙하 0.25초 전에 낸 예측과 실제 맞은 가로 위치의 차는 17번에 평균 1.36px, 최대 2.67px 였다.

벽 반사를 펴서 낙하 지점을 예측하는 패들
새 창에서 열기

08 · 벽 반사를 펴서 낙하 지점을 예측하는 패들 — 측정 누적면적 14.792% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

foldWlandXFOLLOW_EASE
    function foldW(v, lo, hi) {
        const len = hi - lo;
        const per = 2 * len;
        const u = ((v - lo) % per + per) % per;
        return lo + (u <= len ? u : per - u);
    }
    // 공이 패들 높이에 닿을 때까지 남은 시간(초)과 그때의 가로 위치 — 벽돌은 무시, 벽만 편다
    function landing(w, b) {
        const down = w.py - BALL_R;
        let t = 0;
        if (b.vy > 0) {
            t = (down - b.y) / b.vy;
        }
        else {
            t = ((b.y - BALL_R) + (down - BALL_R)) / Math.max(1e-6, -b.vy);
        }
        return [foldW(b.x + b.vx * t, BALL_R, w.w - BALL_R), t];
    }

09문자열 격자 레벨과 연쇄 폭발 파장

레벨 편집과 연쇄 연출, 랜딩 히어로 인트로에는 레벨을 LEVEL_ROWS 문자열 여섯 줄로 적는 방식이 맞다. 폭발 벽돌이 깨지면 BLAST_R 1.25칸 안의 벽돌에 거리(칸) × WAVE_DELAY 0.08 × 2 만큼 늦은 도화선을 걸고, 도화선 시각이 지나면 체력과 상관없이 깨진다. 벽돌 60장 중 폭발 8장이고 12초 동안 폭발 2번(0.80초·0.97초)에 도화선으로 깨진 9장이 0.87~1.14초에 모두 깨졌다.

문자열 격자 레벨과 연쇄 폭발 파장
새 창에서 열기

09 · 문자열 격자 레벨과 연쇄 폭발 파장 — 측정 누적면적 12.41% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LEVEL_ROWSWAVE_DELAYBLAST_R
    function onBlast(w, k) {
        if (k.kind !== 2) {
            return;
        }
        const cx = k.x + k.w / 2;
        const cy = k.y + k.h / 2;
        blasts.push([cx, cy, w.t]);
        const cell = k.w + 2;
        for (const o of w.bricks) {
            if (o.deadT >= 0 || o === k) {
                continue;
            }
            const d = Math.hypot(o.x + o.w / 2 - cx, o.y + o.h / 2 - cy) / cell;
            if (d > BLAST_R) {
                continue;
            }
            const f = w.t + d * WAVE_DELAY * 2;
            if (o.fuse < 0 || f < o.fuse) {
                o.fuse = f;
            }
        }
    }

벽돌깨기를 코드로 짜면 공 하나의 한 프레임은 어떤 값 몇 개로 정해질까?

공 하나가 가진 값

공은 위치 x·y, 속도 vx·vy, 회전 spin, 상태 st, 자취 trail 로 나뉜다. 상태는 0 이 날아감, 1 이 패들 위, 2 가 빠짐이고, 속도의 크기는 공이 아니라 판의 w.speed 가 정한다.

값 뜻 바꾸는 곳
vx·vy 방향 패들 맞은 자리·벽·벽돌 면
spin 옆으로 미는 힘 패들 속도(04)
st 날아감·패들 위·빠짐 발사·바닥
hp·deadT·fuse 벽돌 체력·깨진 시각·도화선 hurt·onBlast(06·09)

스위치 하나가 대비판을 만든다

Mode 의 sweep·face·angle·spin 은 계산의 한 곳만 바꾸는 스위치다. 01 의 angle, 02 의 sweep, 03 의 face, 04 의 spin 이 오른쪽 판에서 다르게 켜지고 나머지 계산은 같아서 두 판의 차이가 그 규칙 하나의 몫이다.

공의 위치와 속도를 받아 이동 구간을 나누고, 가장 먼저 닿는 벽돌을 찾고, 맞은 면의 법선으로 속도를 반사하는 — 벽돌깨기 한 프레임이 계산되는 순서

한 걸음의 순서

moveBall 은 회전을 옆으로 미는 힘으로 더하고, 속도 크기를 w.speed 로 맞추며, 거의 수평이면 세로 성분을 20% 로 살린다. 이어 이동 길이를 STEP_MAX 이하 조각으로 쪼개 조각마다 벽 반사, 패들 윗면 선 통과 검사, reflectBricks 를 차례로 한다. 가장 깊이 겹친 벽돌 하나만 골라 한 조각에 벽돌을 하나만 처리한다.

조각 수는 올림으로 정한다

조각 수는 이동 길이를 STEP_MAX 로 나눠 올린 값이다. 02 의 프레임당 42px 는 11조각이고, 07 의 한계 속도 590px/초 는 걸음 1/120초에 4.92px 라 2조각이 된다. 조각이 작을수록 공이 벽돌 상자를 건너뛸 틈이 줄지만 계산은 그만큼 늘어난다. 이동 구간을 쪼개는 방식은 MDN 의 2D 충돌 감지 문서가 다루는 충돌 검사의 연장이다.

벽돌깨기 로그라이크처럼 규칙을 얹어 실제 게임에 붙일 때 — 라이브러리 없이 만들기·레벨을 문자열로 적는 법·공 속도를 올리는 시점

라이브러리 없이 만들기

캔버스 2D 만 있으면 되고 아홉 장면은 라이브러리·WebGL·CDN 을 쓰지 않는다. 벽돌깨기 게임에 점수·목숨·메뉴를 붙이는 것은 이 계산 틀 바깥의 일이라 World 의 값을 읽는 쪽에서 얹으면 된다.

레벨을 문자열로 적는 법

09 는 . 이 빈 칸, 1~3 이 체력, B 가 폭발 벽돌인 12칸 × 6줄 문자열로 판을 만든다. 로그라이크처럼 판이 자주 바뀌는 게임이라면 줄 문자열만 바꿔 레벨을 늘릴 수 있고, parseLevel 은 글자 하나를 벽돌 한 칸으로 읽는다.

공 속도를 올리는 시점

속도는 speedFor 가 깬 수에서 계산하고 올리는 순간 trailN 도 같이 바꾼다. 속도를 올려도 걸음 조각 수가 따라 늘어 통과는 0번이므로, 난이도 곡선은 SPEED_STEP 과 CLEAR_N 두 값으로 조절한다.

핀볼 편과 다른 점

핀볼 편은 플리퍼와 범퍼를 고르는 선택기를 다루고, 이 편은 패들 위치·이동 구간·벽돌 상태를 한 계산 틀에서 뽑는 구조를 다룬다. 격자 규칙을 시간표로 시연하는 방식이 궁금하면 지뢰찾기 편도 같은 틀을 쓴다.

벽돌깨기가 깨지는 순간 — 공이 빨라지면 벽돌을 뚫고 지나가는 함정·모서리에 맞아 엉뚱한 쪽으로 튕기는 반사·패들 끝에서 공이 수평으로 갇히는 각도

공이 빨라지면 벽돌을 뚫고 지나간다

02 의 오른쪽 판처럼 프레임 끝 위치만 검사하면 이동이 16px 를 넘는 순간부터 벽돌 한 줄을 건너뛴다. 프레임당 20px 에서 20.5%, 30px 에서 46.5%, 42px 에서 62.0% 가 통과했고 10px 에서는 0% 였다. 고치는 방법은 이동을 STEP_MAX 이하로 쪼개는 것 하나다.

모서리에 맞아 엉뚱한 쪽으로 튕긴다

03 의 오른쪽 판은 속도 y 만 뒤집기 때문에 벽돌 옆구리를 맞은 공이 벽돌 안에서 되풀이해 맞는다. 화면 장면 12초에 오른쪽은 맞힘 254번 가운데 파고든 걸음이 242였다. 겹침 깊이가 얕은 축을 고르면 모서리를 스쳐도 가까운 면에서 튕긴다.

패들 끝에서 공이 수평으로 갇힌다

01 의 오른쪽 판은 거울 반사만 써서 같은 세로줄 벽돌 네 장을 깨고 같은 줄을 오르내리다 멈췄다. 공이 거의 수평이 되면 한 줄에 갇히기 쉬워서, moveBall 은 세로 성분을 속도의 20% 아래로 두지 않는다.

아래 상자에서 받는 zip 에는 아홉 장면의 완성 소스와 편집용 파일이 들어 있고, 열 때 쓰는 압축 비밀번호는 fj24x8mt 이다.

패들 폭과 공 속도 단계와 회전 전달 비율을 바꾸면 느낌은 얼마나 달라질까 — PADDLE_W·SPEED_STEP·SPIN_GAIN 을 바꿔 본 차이

이름 바꾼 값 느낌
패들 폭을 좁히기 01 PADDLE_W 96 → 56 PADDLE_W 를 96 에서 56 으로 줄여도 맞은 자리 비율은 그대로라 반사각 표(−1~1 이 ±58°)는 같다. 달라지는 건 px 폭이다 — 같은 0.7 자리가 패들 위에서 33.6px 에서 19.6px 로 좁아지고, 공을 받을 수 있는 반폭은 51px 에서 31px 로 줄어든다. 자동 조종은 낙하 지점에 맞춰 서므로 12초 결과는 왼쪽 11장·오른쪽 4장 그대로였지만, 사람이 몰 때 여유는 그만큼 줄어든다
속도 단계를 가파르게 07 SPEED_STEP 70 → 150 SPEED_STEP 을 70 에서 150 으로 올리면 속도가 190 → 340 → 490 → 590(SPEED_MAX 에서 잘림)으로 세 단계에 끝나 최고 속도에 닿는 시각이 10.0초에서 5.3초로 당겨지고, 12초 동안 깬 벽돌은 24장에서 27장으로 늘며 잔상 길이(TRAIL_LEN 비례)도 46개에 일찍 도달한다. 속도가 높아도 걸음이 STEP_MAX 조각으로 쪼개져 통과는 0번이지만, 프레임을 1/30초로 늘리면 590px/초 공은 한 프레임에 19.7px 가서 벽돌 두께+지름 16px 을 넘으므로 02번 공식으로 19% 가 통과한다
회전 전달을 키우기 04 SPIN_GAIN 0.15 → 0.45 SPIN_GAIN 을 0.15 에서 0.45 로 올리면 같은 첫 맞힘(패들 속도 300px/초, 공 가로 속도 86.7px/초)에서 회전이 45 에서 135 가 되고, 맞은 뒤 0.5초 가로 이동이 +19.3px(오른쪽으로 덜 감)에서 −28.2px(맞은 자리를 지나 왼쪽으로 되돌아감)로 바뀌며 가로 속도는 −4.8 에서 −163px/초 가 된다. 회전을 넘기지 않는 오른쪽 판은 +43.4px 로 곧게 간다

값을 바꾸기 전에 볼 것

세 값은 서로 건드리지 않는다. PADDLE_W 는 px 폭만, SPEED_STEP 은 속도 단계만, SPIN_GAIN 은 회전만 바꾼다. 한 번에 하나만 바꿔야 어느 값이 느낌을 바꿨는지 보인다.

움직임 줄이기에서는 멈춘 한 장의 벽돌 판 — 장면 이름과 남은 벽돌 수를 문장으로 읽어 주는 접근성

방향키로 패들을 옮기고 Enter 로 발사한다

무대 상자는 아홉 장면 모두 role="application" 에 tabindex 0 이라 포커스를 받고, 방향키를 처음 누르면 자동 시연이 멈추고 사람이 몰기 시작한다. Enter·Space 는 멈춘 공을 발사하고 멈춘 공이 없으면 일시정지이며, P 는 일시정지, N 은 0.1초 한 걸음, R 은 처음부터다. 마우스·터치는 누르면 패들이 그 자리로 따라간다.

상태 줄은 문장이 바뀔 때만 다시 쓴다

상태 줄은 시연 중 aria-live="off" 라 읽히지 않고, 직접 하는 순간 polite 로 바뀌며 문장이 바뀔 때만 다시 쓴다. 한 판이 끝나기 0.6초 전에는 role="status" 숨김 문단에 결과 문장을 한 번만 쓴다.

움직임 줄이기와 숨은 탭

prefers-reduced-motion 이 켜져 있으면 루프 없이 showRest 가 REST_S 초 지점의 정지 장면 한 장만 그린다. 탭이 숨으면 루프를 멈추고 돌아오면 다시 시작한다. 이 처리는 MDN 의 Page Visibility API 와 prefers-reduced-motion 문서를 따른다.

    // 탭이 숨으면 멈추고, 돌아오면 시계를 다시 맞춰 시작 — 숨은 동안의 몇 초가 한 프레임에 몰리지 않게
    document.addEventListener('visibilitychange', function () {
        if (document.hidden) {
            stopLoop();
        }
        else if (!paused && !still) {
            startLoop();
        }
    }, { signal: off.signal });
    // 설정이 바뀌면 — 켜지면 정지 장면 한 장, 꺼지면 다시 움직인다
    reduceQuery.addEventListener('change', function () {
        still = reduceQuery.matches;
        if (still) {
            showRest();
        }
        else {
            lap = 0;
            play();
        }
    }, { signal: off.signal });

벽돌깨기 FAQ — 라이브러리 없이 만들기·공이 벽돌을 통과할 때 어떻게 할지·웹에 올릴 때 필요한 것

벽돌깨기 게임을 라이브러리 없이 만들 수 있나요

캔버스 2D 와 rAF 만 있으면 된다. 아홉 장면은 라이브러리·WebGL·CDN 을 쓰지 않고, 장면 소스는 한 파일에 계산 틀과 장면을 같이 담았다.

공이 벽돌을 통과할 때는 어떻게 하나요

이동 구간을 쪼개는 것이 답이다. STEP_MAX 를 벽돌 두께와 공 지름 합보다 작게 두면 02 의 스윕처럼 통과가 0번이다.

웹에 올릴 때는 무엇이 필요한가요

정적 파일 한 벌이면 된다. 캔버스는 devicePixelRatio 를 2 까지만 곱하고 외부 글꼴·서버 호출이 없어서 어느 정적 호스팅에도 올라간다.

출처는 MDN 의 requestAnimationFrame 문서다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.