Tworzenie komponentu menu gry 3D

Podstawowe informacje o tym, jak stworzyć elastyczne, elastyczne i łatwo dostępne menu gier 3D.

W tym poście chcę pokazać, jak stworzyć komponent menu gry 3D. Zobacz prezentację.

Prezentacja

Jeśli wolisz film, oto wersja tego posta w YouTube:

Przegląd

Gry wideo często mają kreatywne, nietypowe menu, animowane i w 3D. W nowych grach AR/VR to menu popularne jest właśnie w tych grach, które sprawiają wrażenie unoszących się w przestrzeni kosmicznej. Dziś przypomnimy podstawy tego efektu, ale uzupełnimy go o adaptacyjną kolorystykę i dostosowujemy go do potrzeb użytkowników, którzy preferują ograniczenie ruchu.

HTML

Menu gry to lista przycisków. Najlepszym sposobem odzwierciedlenia tego w kodzie HTML jest następujący:

<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>

Lista przycisków będzie dobrze widoczna dla technologii czytników ekranu i będzie działać bez JavaScriptu ani CSS.

bardzo ogólna lista punktowana
ze zwykłymi przyciskami jako elementami.

CSS

Styl listy przycisków dzieli się na te ogólne kroki:

  1. Konfiguruję właściwości niestandardowe.
  2. Układ flexbox.
  3. Własny przycisk z ozdobnymi pseudoelementami.
  4. Umieszczanie elementów w przestrzeni 3D.

Omówienie właściwości niestandardowych

Właściwości niestandardowe pomagają identyfikować wartości, nadając im rozpoznawalne nazwy, które wyglądają losowo. Pozwala to uniknąć powtarzania kodu i udostępniania wartości przez elementy podrzędne.

Poniżej znajdziesz zapytania o media zapisane jako zmienne CSS, znane też jako niestandardowe media. Są one globalne i będą używane w różnych selektorach, aby kod był zwięzły i czytelny. Komponent menu gry korzysta z preferencji ruchu, schematu kolorów i zakresu kolorów wyświetlacza.

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

Poniższe właściwości niestandardowe zarządzają schematem kolorów i przytrzymują wartości pozycji kursora, aby menu gry było interaktywne i najeżdżało na nie kursorem. Nazwy właściwości niestandardowych ułatwiają czytelność kodu, ponieważ ujawniają przypadek użycia wartości lub przyjazną nazwę wyniku tej wartości.

.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);
    }
  }
}

Tło w kształcie stożka w jasnym i ciemnym motywie

Jasny motyw ma jasny gradient stożkowy od cyan do deeppink, a ciemny – ciemny, subtelny stożkowy gradient. Więcej informacji o tym, co można zrobić przy użyciu gradientów stożkowych, znajdziesz na stronie conic.style.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Prezentacja zmiany tła między jasnym i ciemnym kolorem.

Włączanie perspektywy 3D

Aby elementy pojawiły się w przestrzeni 3D strony internetowej, trzeba zainicjować widoczny obszar z perspektywą. Postawiłem perspektywę na element body i użyłem jednostek widocznego obszaru, by stworzyć styl, który mi się podoba.

body {
  perspective: 40vw;
}

Właśnie taki wpływ może wywrzeć ta perspektywa.

Określanie stylu listy przycisków <ul>

Ten element odpowiada za ogólny układ makra listy przycisków, a także za interaktywną i pływającą kartę 3D. Oto sposób, aby to osiągnąć.

Układ grupy przycisków

Flexbox może zarządzać układem kontenera. Zmień domyślny kierunek układu elastycznego z wierszy na kolumny z atrybutem flex-direction i upewnij się, że każdy element ma taki sam rozmiar jak jego zawartość, zmieniając wartość z stretch na start w kolumnie 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;
}

Następnie ustaw kontener jako kontekst przestrzeni 3D i skonfiguruj funkcje CSS clamp(), by karta nie obracała się poza czytelne obroty. Zwróć uwagę, że środkowa wartość ograniczenia jest właściwością niestandardową. Te wartości --x i --y zostaną ustawione przez JavaScript po późniejszej interakcji myszą.

.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)
      )
    )
  ;
}

Następnie, jeśli ruch jest prawidłowy u odwiedzającego użytkownika, dodaj wskazówkę do przeglądarki, że przekształcenie tego elementu będzie się stale zmieniać za pomocą will-change. Aby włączyć interpolację, ustaw też transition na przekształceniach. Przejście to nastąpi, gdy mysz wejdzie w interakcję z kartą, umożliwiając płynne przejście do zmian obrotu. Animacja to ciągła animacja pokazująca przestrzeń 3D, w której znajduje się karta, nawet jeśli mysz nie może lub nie wchodzi w interakcję z komponentem.

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

Animacja rotate-y ustawia tylko środkową klatkę kluczową w miejscu 50%, bo przeglądarka ustawi domyślny styl 0% i 100% elementu. Jest to skrót od naprzemiennych animacji, które muszą zaczynać się i kończyć w tym samym miejscu. To świetny sposób na przedstawienie nieskończonych naprzemiennych animacji.

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

Określanie stylu elementów <li>

Każdy element listy (<li>) zawiera przycisk i jego elementy obramowania. Styl display zostanie zmieniony i element nie wyświetla ::marker. Styl position ma wartość relative, więc pseudoelementy kolejnych przycisków mogą się umieszczać w całym obszarze zajmowanym przez przycisk.

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

Zrzut ekranu listy obróconej w przestrzeni 3D w celu pokazania perspektywy. Każdy element listy nie ma już punktora.

Określanie stylu elementów <button>

Stylizowanie przycisków może być pracochłonne, trzeba wziąć pod uwagę wiele stanów i typów interakcji. Przyciski te szybko się komplikują dzięki równoważeniu pseudoelementów, animacji i interakcji.

Początkowe style (<button>)

Poniżej znajduje się lista stylów podstawowych, które będą obsługiwane w innych stanach.

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

Zrzut ekranu przedstawiający listę przycisków w widoku 3D, tym razem z wybranymi stylami.

Pseudoelementy przycisku

Obramowanie przycisku to nie tradycyjne obramowanie, tylko pseudoelementy o pozycji bezwzględnej z obramowaniem.

Zrzut ekranu przedstawiający panel Elementy narzędzi deweloperskich w Chrome z przyciskiem zawierającym elementy::before i ::after.

Te elementy mają kluczowe znaczenie w prezentowaniu dotychczasowej perspektywy 3D. Jeden z tych pseudoelementów zostanie odsunięty od przycisku, a jeden – bliżej użytkownika. Efekt jest najbardziej zauważalny w przypadku przycisków u góry i na dole.

.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);
    }
  }
}

Style przekształceń 3D

Opcja poniżej transform-style ma wartość preserve-3d, aby dzieci mogły się znajdować na osi z. Pole transform ma ustawioną właściwość niestandardową --distance, która zmienia się po najechaniu kursorem i zaznaczeniu.

.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));
  }
}

Style animacji warunkowych

Jeśli użytkownik wyraża zgodę na ruch, przycisk podpowiada przeglądarce, że właściwość przekształcenia powinna być gotowa do zmiany i ustawione jest przejście we właściwościach transform i background-color. Zwróć uwagę na różnicę w czasie trwania. Wydawało mi się to subtelny, rozłożony efekt.

.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 }
  }
}

Style interakcji z najeżdżaniem kursorem i zaznaczeniem

Celem animacji interakcji jest rozłożenie warstw składających się na płaski przycisk. Aby to zrobić, ustaw zmienną --distance na początku na 1px. Selektor widoczny w przykładowym kodzie sprawdza, czy urządzenie nie powoduje najechania kursorem albo zaznaczenia przycisku przez urządzenie, które powinno widzieć wskaźnik ostrości, i czy nie został on aktywowany. Jeśli tak jest, korzysta z CSS, by wykonać te czynności:

  • Zastosuj kolor tła po najechaniu kursorem.
  • Zwiększ dystans .
  • Dodaj efekt łatwego odbijania odbijania.
  • Rozłóż przejścia pseudoelementów.
.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 }
    }
  }
}

Perspektywa 3D nadal świetnie wyglądała w przypadku preferencji ruchu w usłudze reduced. Elementy górne i dolne prezentują efekt w subtelny sposób.

Małe ulepszenia w języku JavaScript

Interfejs jest obsługiwany przez klawiatury, czytniki ekranu, pady do gier, dotykowe i myszki, ale dla ułatwienia w kilku sytuacjach możemy dodać kilka drobnych zmian w JavaScripcie.

Dodatkowe klawisze strzałek

Klawisz tabulacji sprawdza się w poruszaniu się po menu, ale powinien obsługiwać pad kierunkowy lub joysticki na padzie do gier. Biblioteka roving-ux często używana w interfejsach GUI Challenge będzie obsługiwać klawisze strzałek. Poniższy kod informuje bibliotekę, aby utrzymać fokus w obrębie elementu .threeD-button-set i przekierować go na elementy podrzędne przycisków.

import {rovingIndex} from 'roving-ux'

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

Interakcja z paralaksą kursora myszy

Śledzenie myszy i pochylanie jej menu ma naśladować interfejsy AR i VR, w których zamiast myszy może znajdować się wirtualny wskaźnik. Może być zabawnie, gdy elementy są zbyt świadome wskaźnika.

Ze względu na to, że jest to niewielka funkcja, za pomocą zapytań określamy preferencje użytkownika dotyczące ruchu. Dodatkowo w ramach konfiguracji zapisz w pamięci komponent listy przycisków w pamięci za pomocą funkcji querySelector i umieść granice elementu w pamięci podręcznej w menuRect. Te granice pozwalają określić opóźnienie obrotu karty w zależności od pozycji myszy.

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

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

Następnie potrzebujemy funkcji, która akceptuje pozycje x i y myszy oraz zwraca wartość, której możemy użyć, aby obrócić kartę. Ta funkcja korzysta z pozycji kursora myszy, aby określić, po której stronie ramki się znajduje i o ile się na nią znajduje. Wartość delta jest zwracana z funkcji.

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

Na koniec spójrz na ruch myszą, przekaż położenie do naszej funkcji getAngles() i użyj wartości delta jako stylów właściwości niestandardowych. Podzieliłem delta przez 20, aby zmniejszyć drgania i to może być lepsze rozwiązanie. Jeśli zapamiętasz je od początku, umieszczamy rekwizyty --x i --y w środku funkcji clamp(), aby zapobiec nadmiernym obróceniu karty w nieczytelną pozycję, gdy mysz znajduje się w pozycji myszki.

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`)
  })
}

Tłumaczenia i wskazówki dojazdu

Podczas testowania menu gry w różnych trybach pisania i językach wystąpiła pewna niedogodność.

Elementy <button> mają w arkuszu stylów klienta użytkownika styl !important dla writing-mode. Oznaczało to, że trzeba było zmienić kod HTML menu gry, aby dostosować go do pożądanego projektu. Zmiana listy przycisków na listę linków umożliwia właściwościom logicznym zmianę kierunku menu, ponieważ elementy <a> nie mają stylu !important obsługiwanego przez przeglądarkę.

Podsumowanie

Wiesz już, jak to robiłem. Jak to zrobisz‽ 🙂 Czy możesz dodać do menu interakcję z akcelerometrem, aby ułożenie kafelków na telefonie spowodowało obrót menu? Czy możemy poprawić doświadczenie bez ruchu?

Stosujmy różne podejścia i poznajmy sposoby budowania obecności w internecie. Przygotuj wersję demonstracyjną, a potem dodam linki do tweetów, a ja dodam ją do poniższej sekcji na temat remiksów na karcie Społeczność.

Remiksy utworzone przez społeczność

Na razie nic tu nie ma.