Создание компонента меню 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;
}

Снимок экрана списка кнопок в 3D-перспективе, на этот раз со стилем кнопки.

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

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

Снимок экрана: панель «Элементы 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 , предоставляемого браузером.

Заключение

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

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

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

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