핀 끼우기 게임
회전하는 원판에 바늘을 쏘아 다른 바늘과 부딪히지 않도록 하는 10단계 속도 게임
핀 끼우기 게임이란?
핀 끼우기는 모바일에서 인기를 끈 AA, Pin Out과 같은 계열의 가벼운 캐주얼 게임입니다. 화면 가운데에는 흰색 원판이 일정한 속도로 회전하고 있고, 화면 아래에서 바늘을 한 발씩 쏘아 원판에 꽂아 넣어야 합니다. 새로 쏘는 바늘은 이미 꽂혀 있는 바늘과 절대 부딪히면 안 됩니다. 단계마다 정해진 발사 횟수가 있으며, 모두 안전하게 꽂으면 다음 단계로 넘어가고, 단 한 번이라도 부딪히면 즉시 실패합니다. 규칙은 몇 초면 익힐 만큼 단순하지만, 단계가 올라갈수록 빈틈이 좁아지고 회전이 빨라져 타이밍 판단력과 자제력을 진지하게 시험하는 게임이 됩니다. 본 도구는 가입이나 다운로드 없이 브라우저 안에서 모두 작동하며, PC에서는 마우스 클릭, 모바일에서는 화면 터치로 발사할 수 있습니다.
사용 방법
조작 순서
- 페이지를 열면 1단계가 자동으로 시작되고 원판이 이미 돌고 있습니다
- 게임 영역을 클릭하거나 스페이스 또는 엔터 키를 누르면 바늘이 한 발 발사됩니다
- 바늘은 중심을 향해 날아가 원판에 꽂힙니다. 이미 꽂혀 있는 바늘에 닿으면 안 됩니다
- 해당 단계의 모든 바늘을 안전하게 꽂으면 다음 단계로 자동 이동합니다
- 한 번이라도 부딪히면 그 즉시 실패이며, 「이 단계 다시 도전」 또는 「처음부터 다시」를 선택할 수 있습니다(주의: 실패 화면에서 스페이스 / 엔터 키를 누르면 「처음부터 다시」가 작동해 1단계로 돌아갑니다. 「이 단계 다시 도전」에는 키보드 단축키가 없으므로, 같은 단계를 다시 하려면 버튼을 눌러야 합니다)
클리어 요령
- 원판은 일정한 속도로 회전하므로 클릭 속도보다 박자를 잡는 게 더 중요합니다. 빈틈의 가운데 선을 노리세요.
- 옆 바늘이 막 지나간 순간에 발사하는 게 좋습니다. 바늘이 날아가는 데 약 120ms가 걸리니 조금 일찍 쏴 두세요.
- 연속 클릭 시 최대 3발까지 큐에 쌓여 차례로 날아가지만, 그 이상의 클릭은 버려지고 큐에 들어가지 않으니 앞 바늘이 꽂히기 전에 마구 누를 필요가 없습니다.
- 후반부일수록 빈틈이 좁고 회전이 훨씬 빨라집니다. 욕심내다 부딪히느니 위험한 한 발을 거르는 편이 낫습니다.
단계별 난이도
- 1~3단계: 바늘 수가 많고 빈틈이 넓어 조작 박자에 익숙해지는 단계입니다.
- 4~7단계: 원판에 꽂힌 바늘이 늘어나면서 회전을 미리 예측해야 합니다.
- 8~10단계: 원판이 거의 가득 차므로 모든 발사를 정확히 맞춰야 합니다.
활용 사례
기술 원리
메인 루프는 requestAnimationFrame으로 구동됩니다. 매 프레임마다 내부 rotation 값에 고정 speed(도/프레임)를 더한 뒤, 꽂혀 있는 모든 바늘 요소를 순회하며 transform을 rotate(baseAngle + rotation)으로 설정합니다. transform은 합성 레이어 속성이라 브라우저가 회전 처리를 GPU 합성 스레드에 넘기고 레이아웃이나 페인트를 일으키지 않으므로, 마지막 단계에서 15개 바늘이 모두 동시에 회전해도 60fps를 유지할 수 있습니다. 충돌 판정은 「각도 차를 현 길이로 변환」하는 방식을 씁니다. 꽂힌 바늘은 각각 원판 좌표계에서의 baseAngle로 표현됩니다(원판에 대한 고정 오프셋이며 회전과 무관합니다). 새로 발사된 바늘이 중심에 도달했을 때 월드 좌표계에서의 각도는 TARGET_ANGLE = 0이고, 원판 좌표계로 환산하면 normalizeAngle(0 - rotation)이 됩니다. 그다음 새 baseAngle을 기존의 모든 baseAngle과 비교해 각도 차 Δθ를 바늘 끝점 점의 중심 사이 현 길이 2R·sin(Δθ/2)(R = 111px는 각 끝점 점의 중심에서 원판 중심까지의 거리)로 변환합니다. 이 값이 24px 임계값보다 작으면 충돌로 판정해 라운드를 종료합니다. 발사 동작은 작은 상태 머신으로 제어합니다. shootLocked는 비행 중인 바늘을 표시해 transitionend 이벤트와 180ms setTimeout 백업이 같은 발사를 두 번 정산하지 않도록 막습니다. CSS transition은 120ms 동안 진행되며, 180ms setTimeout은 transitionend가 드물게 발화하지 않는 경우(예를 들어 트랜지션이 끝나기 전에 DOM이 변경된 경우)를 위한 안전망입니다. pendingShots는 비행 중에 들어온 클릭 요청을 큐에 쌓되 최대 3발로 제한하며, 그 이상의 클릭은 버려지고 큐에 들어가지 않습니다. 이를 통해 빠른 연사 감각은 유지하면서도 여러 바늘이 동시에 중심으로 향하는 상황을 방지합니다. 실패와 클리어 모두 gameOver를 true로 만들고, 메인 루프는 계속 돌지만 rotation은 더 이상 갱신되지 않아 화면이 멈춰 있는 듯 보이게 됩니다. 표시 스케일링을 위해 게임은 420×720의 고정 디자인 크기를 유지하고, 외곽 래퍼가 뷰포트 크기로부터 스케일 계수를 계산해 transform: scale로 적용합니다. 현재 구현은 스케일 상한을 1로 잡고 있어, 작은 뷰포트는 비례하여 축소되지만 데스크톱 뷰포트는 원래 크기를 유지하고 더 커지지는 않습니다. 덕분에 충돌 판정은 항상 디자인 좌표계에서 단일한 픽셀 임계값 세트로 이루어지며, 어떤 화면에서도 판정 결과가 똑같습니다.
- 메인 루프: requestAnimationFrame, 매 프레임 rotation += speed, 모든 바늘에 transform: rotate(baseAngle + rotation) 적용.
- GPU 가속: 회전을 left/top이 아닌 transform으로 처리해 브라우저 합성기가 다루며, 레이아웃·재페인트가 발생하지 않습니다.
- 충돌 판정: 월드 좌표 0°를 normalizeAngle(0 - rotation)로 원판 좌표에 역사상한 뒤, 현 길이 2R·sin(Δθ/2)을 24px 임계값과 비교합니다.
- 상태 머신: shootLocked로 이중 정산을 막고, pendingShots는 최대 3발로 제한하며 초과분은 버려지고 큐에 들어가지 않습니다. gameOver 후에는 rotation 갱신을 멈춥니다.
- 정산 콜백: 120ms CSS 비행, 주 처리는 transitionend, 보조로 180ms setTimeout을 두어 transitionend가 드물게 발화하지 않는 경우의 안전망 역할을 합니다.
- 스케일링: 420×720 고정 디자인 시스템을 사용하고, 래퍼가 뷰포트 크기에 맞춰 transform: scale을 적용하되 상한이 1로 제한되어 데스크톱은 확대되지 않고 원래 크기를 유지합니다.
- 데이터 저장: localStorage에 최고 클리어 단계 하나만 기록하며, 어떤 데이터도 업로드하지 않고 계정도 순위표도 없습니다.
예시
10단계 속도 및 바늘 수 설정
단계 초기 바늘 발사 수 회전 속도(deg/frame)
1 2 8 1.35
2 3 9 1.50
3 3 10 1.65
4 4 11 1.80
5 4 12 2.00
6 5 13 2.20
7 5 14 2.45
8 6 15 2.70
9 7 15 3.00
10 8 15 3.35전형적인 한 판: 5단계에서 실패
5단계: 초기 바늘 4개, 발사 12개.
1~7번 발사: 빈틈에 깔끔히 꽂혀 5개 남음.
8번 발사: 직전 바늘이 꽂히기 전에 성급히 발사.
판정: 새 baseAngle이 6번 발사로부터 8°, DOT_HIT_DISTANCE 미만.
결과: 충돌, 「게임 오버!」 메시지로 라운드 종료.
「이 단계 다시 도전」을 누르면 5단계부터 다시 시작합니다.전 단계 클리어 결과
클리어 시간: 약 2분 40초
누적 발사 수: 117
실패 횟수: 3
최고 기록: 10단계
메시지: 「모든 단계 클리어!」, 버튼은 「다시 플레이」.
최고 기록은 localStorage에 저장되어 페이지를 새로고침해도 유지됩니다.자주 묻는 질문
분명히 빈 곳을 눌렀는데 왜 실패했나요?
바늘은 즉시 꽂히는 것이 아니라, 클릭부터 실제로 원판에 닿기까지 약 120ms의 비행 시간이 걸립니다. 그 사이 원판은 계속 돌고 있어, 클릭 시점에는 빈틈이 충분히 컸더라도 비행 중에 다른 바늘이 목표 선까지 회전해 들어오면 새 바늘이 그 위에 떨어집니다. 간단한 해법은 빈틈이 막 선을 지나간 직후에 발사해 회전 시간을 미리 빼 두는 것입니다.
화면을 길게 눌러 연사할 수 있나요?
빠르게 클릭하는 것은 가능합니다. 최대 3발까지 큐에 쌓여 차례로 자동 발사되지만, 앞 바늘이 비행 중이면 다음 바늘은 출발하지 않습니다. 3발을 초과한 클릭은 그대로 버려지고 큐에 쌓이지 않습니다. 후반 단계에서는 마구 누르기보다 한 발이 꽂힌 뒤 다음 타이밍을 판단하는 편이 좋습니다.
난이도는 어떤 식으로 올라가나요?
각 단계마다 회전 속도, 초기 바늘 수, 발사해야 하는 바늘 수가 따로 설정되어 있습니다. 회전 속도는 1단계 1.35 deg/frame에서 10단계 3.35 deg/frame으로 약 2.5배까지 올라가며, 3배에는 미치지 않습니다. 초기 바늘 수도 2개에서 8개로 늘어나 남는 빈틈이 좁아집니다. 그래서 후반에는 손이 빠른 것보다 침착함이 더 중요해집니다.
실패 후에는 처음부터인가요, 같은 단계부터인가요?
둘 다 가능하지만 작동 방식에 주의가 필요합니다. 「이 단계 다시 도전」 버튼을 누르면 실패한 그 단계부터 같은 바늘 수와 같은 속도로 다시 시작합니다. 「처음부터 다시」 버튼, 그리고 실패 화면에서 스페이스 / 엔터 키를 누르면 모두 1단계로 돌아갑니다. 「이 단계 다시 도전」에는 키보드 단축키가 없으므로, 반사적으로 스페이스를 누르면 1단계로 떨어지게 됩니다. 최고 클리어 단계는 항상 localStorage에 남아 있습니다.
게임 영역이 왜 이렇게 좁아 보이나요?
디자인 크기가 420×720으로 모바일 세로 화면에 맞춰져 있기 때문입니다. 모바일에서는 자연스럽게 전체 화면으로 보이고, 데스크톱 브라우저에서는 뷰포트 크기에 맞춰 비율을 유지한 채 축소되지만, 스케일 상한이 1로 잠겨 있어 큰 데스크톱 창에서도 게임이 420×720 이상으로 커지지는 않습니다. 창 크기가 변해도 판정 규칙은 달라지지 않습니다.
플레이 데이터가 서버로 전송되나요?
전송되지 않습니다. 게임 전체가 브라우저 안에서만 동작하며, 저장되는 것은 「최고 클리어 단계」라는 숫자 하나로 localStorage에 들어갑니다. 사이트 데이터를 지우거나 브라우저를 바꾸면 사라집니다. 순위표는 없고, 점수나 입력 기록도 저희 쪽에서는 일절 수집하지 않습니다.
모바일에서 즐길 때 주의할 점이 있나요?
브라우저의 전체 화면 모드를 켜 두면 아래로 당기는 제스처로 인해 페이지가 새로고침되는 사고를 막을 수 있습니다. 가로 화면도 즐길 수 있지만 세로 비율이 원본 조작감에 더 가깝습니다. 저사양 기기에서 약간의 프레임 드롭이 있을 때는 다른 탭이나 무거운 애니메이션을 닫아 주세요. 게임 자체는 한 프레임에 transform 계산 몇 개만 하므로 기기 부담이 매우 낮습니다.