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-itemsstretchstart로 변경하여 각 항목이 콘텐츠 크기인지 확인합니다.

.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 값은 나중에 마우스 상호작용 시 JavaScript에서 설정됩니다.

.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>)에는 버튼과 그 테두리 요소가 포함됩니다. display 스타일이 변경되어 항목에 ::marker가 표시되지 않습니다. 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 }
    }
  }
}

reduced 모션 환경설정의 경우 3D 관점이 여전히 매우 깔끔했습니다. 상단 및 하단 요소는 효과를 섬세하게 보여줍니다.

JavaScript를 통한 일부 기능 개선

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

화살표 키 지원

탭 키는 메뉴를 탐색하는 데 좋은 방법이지만 방향 패드나 조이스틱이 게임패드에서 포커스를 이동하는 것이 좋습니다. 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 스타일이 없으므로 논리적 속성에서 메뉴 방향을 변경할 수 있습니다.

결론

이제 제가 어떻게 했는지 알았으니, 괜찮을 건가요?‽ 입찰 휴대전화에 가속도계 상호작용을 추가하여 휴대전화에서 메뉴를 회전할 수 있나요? 모션이 없는 환경을 개선할 수 있을까요?

접근방식을 다각화하고 웹에서 빌드하는 방법을 모두 알아보겠습니다. 데모를 만들어 트윗해 주시면 아래의 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.

커뮤니티 리믹스

아직 표시할 내용이 없습니다.