Создание компонента меню 3D-игры

Базовый обзор того, как создать отзывчивое, адаптивное и доступное 3D-меню игры.

В этом посте я хочу поделиться мыслями о том, как создать компонент меню 3D-игры. Попробуйте демо .

Демо

Если вы предпочитаете видео, вот версия этого поста на YouTube:

Обзор

Видеоигры часто дарят пользователям креативное и необычное меню, анимированное и в трехмерном пространстве. В новых 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. Макет флексбокса.
  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-перспективы

Чтобы элементы существовали в трехмерном пространстве веб-страницы, необходимо инициализировать область просмотра с перспективой . Я решил разместить перспективу на элементе body и использовал единицы видового экрана, чтобы создать стиль, который мне нравился.

body {
  perspective: 40vw;
}

Вот какое влияние может оказать перспектива.

Стилизация списка кнопок <ul>

Этот элемент отвечает за общий макет макроса списка кнопок, а также является интерактивной и плавающей трехмерной карточкой. Вот способ добиться этого.

Расположение группы кнопок

Flexbox может управлять макетом контейнера. Измените направление flex по умолчанию со строк на столбцы с помощью flex-direction и убедитесь, что размер каждого элемента соответствует размеру его содержимого, изменив с stretch на start для align-items .

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

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

Затем установите контейнер в качестве контекста трехмерного пространства и настройте функции 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 при преобразованиях. Этот переход будет происходить при взаимодействии мыши с картой, обеспечивая плавный переход к изменениям вращения. Анимация — это постоянно движущаяся анимация, демонстрирующая трехмерное пространство, в котором находится карта, даже если мышь не может или не взаимодействует с компонентом.

@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;
  }
}

Анимация rotate-y устанавливает только средний ключевой кадр на уровне 50% , поскольку браузер по умолчанию устанавливает 0% и 100% в соответствии со стилем элемента по умолчанию. Это сокращение для чередующихся анимаций, которым необходимо начинать и заканчивать в одной и той же позиции. Это отличный способ создать бесконечную чередующуюся анимацию.

@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;
}

Снимок экрана списка, повернутого в трехмерном пространстве, чтобы показать перспективу, и каждый элемент списка больше не имеет маркера.

Стилизация элементов <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;
}

Скриншот списка кнопок в трехмерной перспективе, на этот раз со стилизованными кнопками.

Псевдоэлементы кнопок

Границы кнопки не являются традиционными границами, это псевдоэлементы абсолютного положения с границами.

Снимок экрана: панель элементов Chrome Devtools с кнопкой, имеющей элементы ::before и ::after.

Эти элементы имеют решающее значение для демонстрации созданной трехмерной перспективы. Один из этих псевдоэлементов будет отодвинут от кнопки, а другой — ближе к пользователю. Эффект наиболее заметен на верхней и нижней кнопках.

.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-style установлено значение preserve-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));
  }
}

Условные стили анимации

Если пользователя устраивает движение, кнопка намекает браузеру, что свойство преобразования должно быть готово к изменению, а для свойств transform и background-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 — хороший способ навигации по меню, но я бы ожидал, что панель управления или джойстики будут перемещать фокус на геймпад. Библиотека roving-ux, часто используемая для интерфейсов GUI Challenge, будет обрабатывать клавиши со стрелками. Приведенный ниже код сообщает библиотеке захватить фокус внутри .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)'
)

Далее нам нужна функция, которая принимает позиции мыши x и y и возвращает значение, которое мы можем использовать для поворота карты. Следующая функция использует положение мыши, чтобы определить, в какой стороне прямоугольника она находится и на сколько. Дельта возвращается из функции.

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> имеют стиль !important для writing-mode в таблице стилей пользовательского агента. Это означало, что HTML-код игрового меню необходимо было изменить, чтобы он соответствовал желаемому дизайну. Изменение списка кнопок на список ссылок позволяет логическим свойствам изменять направление меню, поскольку элементы <a> не имеют стиля !important предоставляемого браузером.

Заключение

Теперь, когда вы знаете, как я это сделал, как бы вы поступили‽ 🙂 Можете ли вы добавить в меню взаимодействие с акселерометром, чтобы при расположении телефона плиткой меню поворачивалось? Можем ли мы улучшить работу без движения?

Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!

Ремиксы сообщества

Здесь пока нечего смотреть!