3D 게임 메뉴 구성요소 빌드

반응형, 적응형, 접근성을 갖춘 3D 게임 메뉴를 빌드하는 방법에 관한 기본적인 개요입니다.

이 게시물에서는 3D 게임 메뉴 구성요소를 빌드하는 방법에 관한 생각을 공유하고자 합니다. 데모 사용해 보기

데모

동영상을 선호한다면 이 게시물의 YouTube 버전을 참조하세요.

개요

비디오 게임은 사용자에게 창의적이고 독특한 메뉴, 애니메이션 및 3D 공간을 제공하는 경우가 많습니다. 신규 AR/VR 게임에서 메뉴가 우주에 떠 있는 것처럼 보이게 하는 것이 널리 사용됩니다. 오늘은 이 효과의 핵심 요소를 다시 만들고 모션 감소를 선호하는 사용자를 위해 적응형 색 구성표와 조정 기능을 추가해 보겠습니다.

HTML

게임 메뉴는 버튼 목록입니다. 이를 HTML로 표현하는 가장 좋은 방법은 다음과 같습니다.

<ul class="threeD-button-set">
  <li><button>New Game</button></li>
  <li><button>Continue</button></li>
  <li><button>Online</button></li>
  <li><button>Settings</button></li>
  <li><button>Quit</button></li>
</ul>

버튼 목록은 스크린 리더 기술에 자체적으로 잘 전달되며 JavaScript나 CSS 없이도 작동합니다.

항목으로서 일반 버튼이 있는 매우 일반적인 글머리기호 목록을 사용합니다.

CSS

버튼 목록의 스타일 지정은 다음과 같은 대략적인 단계로 나뉩니다.

  1. 맞춤 속성을 설정하는 중입니다.
  2. Flexbox 레이아웃
  3. 장식용 유사 요소가 있는 맞춤 버튼
  4. 요소를 3D 공간에 배치

맞춤 속성 개요

커스텀 속성은 무작위로 보이는 값에 의미 있는 이름을 지정하고, 반복되는 코드를 피하고, 하위 요소 간에 값을 공유하는 것을 방지하여 값을 구별하는 데 도움이 됩니다.

다음은 CSS 변수로 저장된 미디어 쿼리이며 맞춤 미디어라고도 합니다. 이는 전역적이며 코드를 간결하고 읽기 쉽게 유지하기 위해 다양한 선택기에서 사용됩니다. 게임 메뉴 구성요소는 디스플레이의 모션 환경설정, 시스템 색 구성표색상 범위 기능을 사용합니다.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);

다음 맞춤 속성은 색 구성표를 관리하고 마우스 위치 값을 유지하여 게임 메뉴를 마우스 오버할 수 있는 대화형으로 만듭니다. 커스텀 속성의 이름을 지정하면 값의 사용 사례나 값 결과의 친숙한 이름을 알 수 있으므로 코드 가독성에 도움이 됩니다.

.threeD-button-set {
  --y:;
  --x:;
  --distance: 1px;
  --theme: hsl(180 100% 50%);
  --theme-bg: hsl(180 100% 50% / 25%);
  --theme-bg-hover: hsl(180 100% 50% / 40%);
  --theme-text: white;
  --theme-shadow: hsl(180 100% 10% / 25%);

  --_max-rotateY: 10deg;
  --_max-rotateX: 15deg;
  --_btn-bg: var(--theme-bg);
  --_btn-bg-hover: var(--theme-bg-hover);
  --_btn-text: var(--theme-text);
  --_btn-text-shadow: var(--theme-shadow);
  --_bounce-ease: cubic-bezier(.5, 1.75, .75, 1.25);

  @media (--dark) {
    --theme: hsl(255 53% 50%);
    --theme-bg: hsl(255 53% 71% / 25%);
    --theme-bg-hover: hsl(255 53% 50% / 40%);
    --theme-shadow: hsl(255 53% 10% / 25%);
  }

  @media (--HDcolor) {
    @supports (color: color(display-p3 0 0 0)) {
      --theme: color(display-p3 .4 0 .9);
    }
  }
}

밝은 테마 및 어두운 테마 배경 원뿔 배경

밝은 테마에는 생동감 넘치는 cyan~deeppink 원뿔 그라데이션이 있으며 어두운 테마에는 희미한 원뿔 그라데이션이 있습니다. 원뿔 그라데이션으로 할 수 있는 작업에 대한 자세한 내용은 conic.style을 참고하세요.

html {
  background: conic-gradient(at -10% 50%, deeppink, cyan);

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
밝은 색상 환경설정과 어두운 색상 환경설정 간에 변경되는 배경

3D 원근법 사용 설정

요소가 웹페이지의 3D 공간에 존재하려면 원근 기능이 있는 표시 영역을 초기화해야 합니다. body 요소에 원근감을 배치하고 표시 영역 단위를 사용해 원하는 스타일을 만들었습니다.

body {
  perspective: 40vw;
}

이는 관점이 미칠 수 있는 영향의 유형입니다.

<ul> 버튼 목록 스타일 지정

이 요소는 전체 버튼 목록 매크로 레이아웃뿐만 아니라 대화형 3D 플로팅 카드 역할을 담당합니다. 이를 실현할 수 있는 방법이 있습니다.

버튼 그룹 레이아웃

Flexbox는 컨테이너 레이아웃을 관리할 수 있습니다. flex-direction를 사용하여 flex의 기본 방향을 행에서 열로 변경하고 align-items의 경우 stretch에서 start로 변경하여 각 항목이 콘텐츠 크기인지 확인합니다.

.threeD-button-set {
  /* remove <ul> margins */
  margin: 0;

  /* vertical rag-right layout */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2.5vh;
}

다음으로 컨테이너를 3D 공간 컨텍스트로 설정하고 카드가 읽을 수 있는 회전 범위를 벗어나 회전하지 않도록 CSS clamp() 함수를 설정합니다. 클램프의 중간 값은 맞춤 속성이며, 이러한 --x--y 값은 나중에 마우스 상호작용 시 자바스크립트에서 설정됩니다.

.threeD-button-set {
  …

  /* create 3D space context */
  transform-style: preserve-3d;

  /* clamped menu rotation to not be too extreme */
  transform:
    rotateY(
      clamp(
        calc(var(--_max-rotateY) * -1),
        var(--y),
        var(--_max-rotateY)
      )
    )
    rotateX(
      clamp(
        calc(var(--_max-rotateX) * -1),
        var(--x),
        var(--_max-rotateX)
      )
    )
  ;
}

다음으로, 방문하는 사용자의 모션이 정상이라면 이 항목의 변환이 will-change를 통해 계속 변경된다는 힌트를 브라우저에 추가합니다. 또한 변환에 transition를 설정하여 보간 유형을 사용 설정합니다. 이 전환은 마우스가 카드와 상호작용할 때 발생하므로 회전 변경사항으로 원활하게 전환할 수 있습니다. 애니메이션은 마우스가 구성요소와 상호작용할 수 없거나 상호작용하지 않더라도 카드가 포함된 3D 공간을 보여주는 지속적으로 실행되는 애니메이션입니다.

@media (--motionOK) {
  .threeD-button-set {
    /* browser hint so it can be prepared and optimized */
    will-change: transform;

    /* transition transform style changes and run an infinite animation */
    transition: transform .1s ease;
    animation: rotate-y 5s ease-in-out infinite;
  }
}

브라우저에서 0%100%를 요소의 기본 스타일로 기본 설정하므로 rotate-y 애니메이션에서는 50%의 가운데 키프레임만 설정합니다. 이는 번갈아 나는 애니메이션의 약식 표현으로, 동일한 위치에서 시작하고 끝나야 합니다. 무한 번갈아 재생되는 애니메이션을 명확하게 표현할 수 있는 좋은 방법입니다.

@keyframes rotate-y {
  50% {
    transform: rotateY(15deg) rotateX(-6deg);
  }
}

<li> 요소의 스타일 지정

각 목록 항목 (<li>)에는 버튼과 테두리 요소가 포함되어 있습니다. 항목에 ::marker가 표시되지 않도록 display 스타일이 변경되었습니다. position 스타일은 relative로 설정되므로 다음 버튼 유사 요소가 버튼이 사용하는 전체 영역 내에 배치될 수 있습니다.

.threeD-button-set > li {
  /* change display type from list-item */
  display: inline-flex;

  /* create context for button pseudos */
  position: relative;

  /* create 3D space context */
  transform-style: preserve-3d;
}

3D 공간에서 회전된 목록의 스크린샷. 원근감을 표시하고 각 목록 항목에는 더 이상 글머리기호가 없습니다.

<button> 요소의 스타일 지정

버튼의 스타일 지정은 어려운 작업일 수 있으며 고려해야 할 상태와 상호작용 유형이 많습니다. 이러한 버튼은 유사 요소, 애니메이션, 상호작용의 균형을 맞추기 때문에 빠르게 복잡해집니다.

초기 <button> 스타일

다음은 다른 상태를 지원하는 기본 스타일입니다.

.threeD-button-set button {
  /* strip out default button styles */
  appearance: none;
  outline: none;
  border: none;

  /* bring in brand styles via props */
  background-color: var(--_btn-bg);
  color: var(--_btn-text);
  text-shadow: 0 1px 1px var(--_btn-text-shadow);

  /* large text rounded corner and padded*/
  font-size: 5vmin;
  font-family: Audiowide;
  padding-block: .75ch;
  padding-inline: 2ch;
  border-radius: 5px 20px;
}

3D 관점의 버튼 목록 스크린샷. 이번에는 스타일이 지정된 버튼이 있습니다.

버튼 유사 요소

버튼의 테두리는 기본 테두리가 아니며, 테두리가 있는 의사 요소의 절대 위치입니다.

::before 및 ::after 요소가 있는 버튼이 표시된 Chrome Devtools Elements 패널 스크린샷

이러한 요소는 확립된 3D 원근을 표현하는 데 중요합니다. 이러한 유사 요소 중 하나는 버튼에서 멀리 밀려나고 하나는 사용자에게 더 가까이 이동합니다. 이 효과는 상단 및 하단 버튼에서 가장 두드러집니다.

.threeD-button button {
  …

  &::after,
  &::before {
    /* create empty element */
    content: '';
    opacity: .8;

    /* cover the parent (button) */
    position: absolute;
    inset: 0;

    /* style the element for border accents */
    border: 1px solid var(--theme);
    border-radius: 5px 20px;
  }

  /* exceptions for one of the pseudo elements */
  /* this will be pushed back (3x) and have a thicker border */
  &::before {
    border-width: 3px;

    /* in dark mode, it glows! */
    @media (--dark) {
      box-shadow:
        0 0 25px var(--theme),
        inset 0 0 25px var(--theme);
    }
  }
}

3D 변환 스타일

아래 transform-stylepreserve-3d로 설정되어 하위 요소가 z 축에서 자신을 간격을 둘 수 있습니다. transform--distance 맞춤 속성으로 설정되며 마우스 오버 및 포커스 시 증가합니다.

.threeD-button-set button {
  …

  transform: translateZ(var(--distance));
  transform-style: preserve-3d;

  &::after {
    /* pull forward in Z space with a 3x multiplier */
    transform: translateZ(calc(var(--distance) / 3));
  }

  &::before {
    /* push back in Z space with a 3x multiplier */
    transform: translateZ(calc(var(--distance) / 3 * -1));
  }
}

조건부 애니메이션 스타일

사용자가 모션을 사용해도 괜찮다면 버튼은 변환 속성을 변경할 준비가 되어 있어야 하고 transformbackground-color 속성에 전환이 설정되어 있다고 브라우저에 알립니다. 기간의 차이를 주목하세요. 미묘한 시차를 두고 효과를 낼 수 있는 것 같았습니다.

.threeD-button-set button {
  …

  @media (--motionOK) {
    will-change: transform;
    transition:
      transform .2s ease,
      background-color .5s ease
    ;

    &::before,
    &::after {
      transition: transform .1s ease-out;
    }

    &::after    { transition-duration: .5s }
    &::before { transition-duration: .3s }
  }
}

마우스 오버 및 포커스 상호작용 스타일

상호작용 애니메이션의 목표는 평면으로 표시되는 버튼을 구성하는 레이어를 넓히는 것입니다. 이렇게 하려면 --distance 변수를 처음에 1px로 설정합니다. 다음 코드 예에 표시된 선택기는 포커스 표시기가 표시되어야 하지만 활성화되지 않은 기기가 버튼에 마우스를 가져가거나 포커스를 맞추고 있는지 확인합니다. 이 경우 CSS를 적용하여 다음 작업을 실행합니다.

  • 마우스 오버 배경 색상을 적용합니다.
  • 거리를 늘립니다 .
  • 바운스 이징 효과를 추가합니다.
  • 유사 요소 전환 시차를 지정합니다.
.threeD-button-set button {
  …

  &:is(:hover, :focus-visible):not(:active) {
    /* subtle distance plus bg color change on hover/focus */
    --distance: 15px;
    background-color: var(--_btn-bg-hover);

    /* if motion is OK, setup transitions and increase distance */
    @media (--motionOK) {
      --distance: 3vmax;

      transition-timing-function: var(--_bounce-ease);
      transition-duration: .4s;

      &::after  { transition-duration: .5s }
      &::before { transition-duration: .3s }
    }
  }
}

3D 원근감은 여전히 reduced 모션 환경설정에서 매우 훌륭했습니다. 상단과 하단 요소는 섬세하게 효과를 표시합니다.

JavaScript를 사용한 소규모 개선

이 인터페이스는 이미 키보드, 스크린 리더, 게임패드, 터치, 마우스에서 사용할 수 있지만 JavaScript를 약간 추가하여 몇 가지 시나리오를 완화할 수 있습니다.

지원 화살표 키

Tab 키는 메뉴를 탐색하는 좋은 방법이지만 방향 패드 또는 조이스틱을 사용하여 게임패드에서 포커스를 이동할 수 있습니다. GUI 챌린지 인터페이스에 자주 사용되는 roving-ux 라이브러리가 화살표 키를 자동으로 처리합니다. 아래 코드는 .threeD-button-set 내에서 포커스를 트랩하고 버튼 하위 요소에 포커스를 전달하도록 라이브러리에 지시합니다.

import {rovingIndex} from 'roving-ux'

rovingIndex({
  element: document.querySelector('.threeD-button-set'),
  target: 'button',
})

마우스 시차 상호작용

마우스를 추적하고 메뉴를 기울이도록 하는 것은 AR 및 VR 비디오 게임 인터페이스를 모방하기 위한 것이며, 마우스 대신 가상 포인터가 있을 수 있습니다. 요소가 포인터를 과도하게 인식하면 재미있을 수 있습니다.

이것은 작은 추가 기능이므로 사용자의 모션 환경설정에 관한 쿼리 뒤에 상호작용을 배치하겠습니다. 또한 설정의 일부로 querySelector를 사용하여 버튼 목록 구성요소를 메모리에 저장하고 요소의 경계를 menuRect에 캐시합니다. 이 경계를 사용하여 마우스 위치를 기반으로 카드에 적용되는 회전 오프셋을 결정합니다.

const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
)

다음으로 마우스의 xy 위치를 허용하고 카드를 회전하는 데 사용할 수 있는 값을 반환하는 함수가 필요합니다. 다음 함수는 마우스 위치를 사용하여 상자의 어느 쪽이 안쪽에 있고 얼마나 많이 있는지 확인합니다. 델타는 함수에서 반환됩니다.

const getAngles = (clientX, clientY) => {
  const { x, y, width, height } = menuRect

  const dx = clientX - (x + 0.5 * width)
  const dy = clientY - (y + 0.5 * height)

  return {dx,dy}
}

마지막으로 마우스 움직임을 확인하고 위치를 getAngles() 함수에 전달한 후 델타 값을 맞춤 속성 스타일로 사용합니다. 델타 값을 20으로 나누어 부족함을 줄이고 더 나은 방법이 있을 수 있습니다. 앞에서 설명했듯이 --x--y 속성을 clamp() 함수의 중간에 배치하면 마우스 위치가 카드를 읽을 수 없는 위치로 과도하게 회전하는 것을 방지할 수 있습니다.

if (motionOK) {
  window.addEventListener('mousemove', ({target, clientX, clientY}) => {
    const {dx,dy} = getAngles(clientX, clientY)

    menu.attributeStyleMap.set('--x', `${dy / 20}deg`)
    menu.attributeStyleMap.set('--y', `${dx / 20}deg`)
  })
}

번역 및 경로

다른 쓰기 모드와 언어로 게임 메뉴를 테스트할 때는 한 가지 실수가 있었습니다.

<button> 요소에는 사용자 에이전트 스타일시트의 writing-mode에 대한 !important 스타일이 있습니다. 즉, 원하는 디자인을 수용하도록 게임 메뉴 HTML을 변경해야 했습니다. 버튼 목록을 링크 목록으로 변경하면 논리 속성으로 메뉴 방향을 변경할 수 있습니다. <a> 요소에는 브라우저에서 제공하는 !important 스타일이 없기 때문입니다.

결론

이제 제가 어떻게 했는지 알았으니 어떻게 해야 할까요?‽ 🙂 메뉴에 가속도계 상호작용을 추가하여 휴대전화를 타일식으로 배치하면 메뉴가 회전할 수 있나요? 모션이 없는 환경을 개선할 수 있을까요?

접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다. 데모를 만들고 링크를 트윗해 주세요. 그러면 아래의 커뮤니티 리믹스 섹션에 추가하겠습니다.

커뮤니티 리믹스

표시할 항목이 없습니다.