HTML6 Games설치없는 무료 웹 게임 / devlog

설치 없이, 지금 바로 플레이

브라우저에서 즐기는 무료 미니게임 모음. 로그인하면 기록이 계정에 저장되어 어느 기기에서든 이어서 볼 수 있어요.

게임 목록
게임 목록
모든 개발 이야기

라이브러리 없이 3D 주사위를 굴려 원하는 면에서 멈추기

2026년 11월 1일 · 기술 · English

뱀과 사다리는 주사위를 굴려 컴퓨터보다 먼저 100번 칸에 닿는 게임이다. 판 아래 굴리기 버튼 옆에 작은 주사위가 하나 있다. 게임에서 가장 자주 보는 움직임이 주사위 굴림인데, 이 작은 연출을 네 번 고쳤다.

처음 주사위는 ⚀ 같은 유니코드 주사위 글자였다. 굴리면 0.48초 동안 한 바퀴 돌며 튀고, 그동안 글자를 빠르게 바꾸다가 나온 수에서 멈췄다. 두 가지가 걸렸다. 하나는 이 글자가 기기마다 크기와 높이가 달라서 버튼보다 아래로 처져 보였다는 것이다. 그래서 주사위를 SVG 로 그렸고, 나중에 다른 그림과 맞추려고 생성 그림 여섯 장으로 바꿨다.

다른 하나는 움직임이었다. 평면 그림이 화면 안에서 한 바퀴 도니 주사위가 아니라 동전처럼 보였다. 면은 같은 간격으로 바뀌다가 갑자기 멈췄고, 떨어지는 느낌도 그림자도 없었다. 시안 세 가지를 게임 안에서 굴려 보고, 위로 던져 두 번 튕기며 멈추는 쪽을 골랐다. 그림자를 넣고 면이 바뀌는 속도를 점점 늦췄다.

그래도 어색했다. 주사위가 사실 위에서 본 면 그림 한 장이라서, 숫자를 바꾸려면 그림을 갈아 끼우는 수밖에 없다. 바닥에 떨어진 다음에 숫자가 바뀌니 굴러서 나온 숫자가 아니라 누가 바꿔 놓은 숫자처럼 보였다. 진짜 주사위는 숫자가 바뀌지 않는다. 돌다가 어느 면이 위로 올 뿐이다.

그림 여섯 장으로 정육면체 만들기

three.js 는 이미 이 사이트의 3D 게임에 쓰고 있다. 하지만 뱀과 사다리는 HTML 요소로 그리는 2D 게임이고, 버튼 옆 52px 주사위 하나 때문에 WebGL 캔버스를 하나 더 띄우는 것은 과했다. CSS 만으로도 정육면체는 만들 수 있다.

CSS 의 transform 은 요소를 3D 공간에서 돌리고 옮길 수 있다. 면 그림 여섯 장을 각각 돌린 다음 한 변의 절반만큼 앞으로(translateZ) 밀면 상자의 여섯 자리에 붙는다. 부모에 transform-style: preserve-3d 를 주면 자식들이 평면으로 눌리지 않고 같은 3D 공간에 놓이고, 그 위에 perspective 를 주면 브라우저가 원근을 그린다. 정육면체 자체는 상자 요소 하나와 면 여섯 장, HTML 요소 일곱 개면 된다.

// where to attach face n (opposite faces add up to 7)
const PLACE = {
  1: 'rotateY(0)', 2: 'rotateY(90deg)', 3: 'rotateX(90deg)',
  4: 'rotateX(-90deg)', 5: 'rotateY(-90deg)', 6: 'rotateY(180deg)',
};
cube.innerHTML = [1, 2, 3, 4, 5, 6]
  .map((n) => `<i style="transform:${PLACE[n]} translateZ(23px)"></i>`) // half of the 46px side
  .join('');
.die  { width: 52px; height: 52px; perspective: 220px; }
.cube { width: 46px; height: 46px; transform-style: preserve-3d; }
.cube > i { position: absolute; inset: 0; backface-visibility: hidden; }

원근 값은 220px, 한 변의 약 5배로 잡았다. 작을수록 원근이 세져서 가까운 모서리가 늘어나 보인다.

정육면체를 비스듬히 돌려 보니 모서리에 틈이 보였다. 생성한 면 그림은 모서리가 둥글고 바깥이 투명해서, 면 여섯 장을 붙여도 모서리가 비어 있었다. 면보다 조금 작은 회색 상자를 하나 더 안쪽에 만들어 틈을 메웠다. 같은 자리에 같은 방식으로 붙이되, 앞으로 미는 거리만 23px 대신 21px 이다.

결과를 먼저 정하고 각도를 맞춘다

나올 숫자는 굴리는 순간 이미 정해져 있다. 이 숫자를 바꿀 생각은 없으니, 주사위가 그 면에서 멈추게 만들면 된다. 면마다 그 면이 앞을 향하는 정육면체 각도를 표로 둔다. 1은 그대로, 6은 Y축으로 180°, 2는 Y축으로 −90° 처럼 붙인 각도의 반대다.

끝 각도는 이 각도에 360° 의 정수배를 더한 값이다. 360° 를 몇 번 더하든 같은 면이 앞을 향하므로, 지금 각도에서 충분히 먼 끝 각도를 고르면 여러 바퀴 돌다가 반드시 그 면에서 멈춘다. 숫자 그림은 한 번도 바꾸지 않는다. 돌던 주사위에서 결과 면이 돌아 나오는 것처럼 보인다.

// cube angle [rotateX, rotateY] that turns face d to the front
const FRONT = { 1: [0, 0], 2: [0, -90], 3: [-90, 0], 4: [90, 0], 5: [0, 90], 6: [0, 180] };
const turn = ([x, y]) => `rotateX(${x}deg) rotateY(${y}deg)`;
let angle = FRONT[5];

function roll(d) {                // d is already decided
  const [bx, by] = FRONT[d], [cx, cy] = angle;
  // at least one full turn on X and three quarters on Y, ending on face d
  const tx = bx + 360 * Math.ceil((cx + 360 - bx) / 360);
  const ty = by + 360 * Math.ceil((cy + 270 - by) / 360);
  cube.animate([
    { offset: 0, transform: turn([cx, cy]), easing: 'cubic-bezier(.25,.6,.35,1)' },
    { offset: 0.82, transform: turn([tx + 7, ty - 5]) }, // overshoot a little
    { offset: 0.94, transform: turn([tx, ty]) },
    { offset: 1, transform: turn([tx, ty]) },
  ], { duration: 660 });
  angle = FRONT[d];
  cube.style.transform = turn(angle); // same direction, small numbers
}

회전은 빠르게 시작해 느려지는 곡선을 쓰고, 끝에서 결과 면을 몇 도 지나쳤다가 돌아오게 했다. 이 작은 흔들림이 있어야 무게가 있는 물건이 멈추는 것처럼 보인다.

여기서 주의할 것이 있다. 처음과 끝 키프레임이 모두 rotateX() rotateY() 처럼 같은 함수를 같은 순서로 가지면, 브라우저는 각도 숫자를 그대로 보간한다. 0° 에서 720° 로 가면 정말 두 바퀴를 돈다. 함수의 종류나 순서가 다르면 브라우저가 둘 다 행렬로 바꿔서 보간한다. 행렬에는 바퀴 수가 남지 않아서 가장 짧은 길로 돌아 버린다.

애니메이션이 끝나면 style.transform 을 360° 의 배수를 뺀 원래 각도로 둔다. 같은 방향이라 화면은 그대로이고, 다음 굴림은 그 작은 각도에서 시작한다. 이렇게 하지 않으면 굴릴 때마다 각도 숫자가 계속 커진다.

rotateX(a) rotateY(b) 가 실제로 어느 면을 앞으로 보내는지는 헷갈리기 쉽다. 표를 머리로만 맞추지 않고 브라우저에서 확인했다. 정육면체의 transform 과 면의 transform 을 DOMMatrix 로 곱한 다음, 앞을 보는 방향 (0, 0, 1) 을 옮겨 z 가 1 이 되는 면을 찾았다. 여섯 면 모두 표대로 나왔고, 실제 판에서도 열두 번 굴려 나온 숫자와 앞면이 같은 것을 확인했다.

회전, 튕김, 그림자를 따로 움직인다

정육면체 하나에 모든 움직임을 넣으면 키프레임이 복잡해진다. 그래서 요소 셋에 나눴다. 정육면체는 회전만 한다. 그 바깥 감싸개는 위로 30px 던져졌다가 330ms 에 착지하며 살짝 납작해지고, 8px 한 번 더 튕긴 뒤 내려앉는다. 결과가 나온 590ms 에는 1.14배로 톡 커졌다가 돌아온다. 바닥의 흐린 타원 그림자는 주사위가 높이 있을 때 작고 옅어지며, 바닥에 닿으면 원래대로 돌아온다. 그림자만 있어도 눈이 높이를 읽는다.

세 요소 모두 Element.animate()(Web Animations API)로 움직이고 전체 660ms 다. 착지하는 330ms 에는 이미 대부분 돌아서 결과 면이 거의 보인다. 말은 580ms 에 걷기 시작해서, 주사위가 완전히 멈추기 조금 전에 다음 동작이 이어진다. transform 과 opacity 만 바꾸므로 브라우저가 레이아웃을 다시 계산하지 않는다.

한계도 있다. 조명이 없어서 옆면도 앞면과 밝기가 같다. 물리 계산이 아니라서 매번 같은 모양으로 구른다. 바닥 위를 굴러다니거나 주사위 여러 개가 부딪히는 연출이라면 WebGL 과 물리 엔진이 필요하다. 버튼 옆 작은 주사위에는 이 정도로 충분했다. 숫자가 바뀌는 대신 돌아서 나오는 것만으로 어색함이 거의 사라졌다.

참고한 문서

이 글과 관련된 게임

뱀과 사다리주사위를 굴려 컴퓨터보다 먼저 100번 칸에 도착하세요. 사다리는 올라가고 뱀은 미끄러져요! 해 보기

다른 글

홈 화면에 추가하기

  1. 화면 아래(아이패드는 위)의 공유 버튼을 누르세요. 네모에서 화살표가 위로 나온 모양이에요.
  2. 목록을 내려 "홈 화면에 추가"를 누르세요.
  3. 오른쪽 위 "추가"를 누르면 홈 화면에 HTML6 Games 아이콘이 생겨요.

Safari 에서만 추가할 수 있어요. 홈 화면에서 열면 주소창 없이 앱처럼 보이고, 인터넷이 끊겨도 열립니다.