Podstawowe informacje o tym, jak utworzyć responsywne, adaptacyjne i dostępne menu gry 3D.
W tym poście chcę podzielić się przemyśleniami na temat sposobu tworzenia komponentu menu gry 3D. Wypróbuj wersję demonstracyjną.
Jeśli wolisz film, obejrzyj tę wersję posta w YouTube:
Przegląd
Gry wideo często prezentują użytkownikom kreatywne i nietypowe menu, animowane i w przestrzeni 3D. Jest to popularne rozwiązanie w nowych grach AR/VR, w których menu wydaje się unosić w przestrzeni. Dziś odtworzymy podstawowe elementy tego efektu, ale dodamy do niego adaptacyjną kolorystykę i ułatwienia dla użytkowników, którzy wolą ograniczone animacje.
HTML
Menu gry to lista przycisków. Najlepszym sposobem na przedstawienie tego w HTML jest:
<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 odczytywana przez technologie czytników ekranu i będzie działać bez JavaScriptu ani CSS.

CSS
Stylizowanie listy przycisków obejmuje te główne kroki:
- Konfigurowanie właściwości niestandardowych.
- Układ flexbox.
- Niestandardowy przycisk z dekoracyjnymi pseudoelementami.
- Umieszczanie elementów w przestrzeni 3D.
Omówienie właściwości niestandardowych
Właściwości niestandardowe pomagają rozróżniać wartości, nadając znaczące nazwy wartościom, które w przeciwnym razie wyglądałyby na losowe. Pozwala to uniknąć powtarzania kodu i udostępniania wartości elementom podrzędnym.
Poniżej znajdziesz zapytania o media zapisane jako zmienne CSS, czyli 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 ustawień ruchu, schematu kolorów systemu 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 przechowują wartości pozycji myszy, dzięki czemu menu gry reaguje na najechanie kursorem. Nadawanie niestandardowym właściwościom nazw zwiększa czytelność kodu, ponieważ ujawnia przypadek użycia wartości lub przyjazną nazwę wyniku 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);
}
}
}
Stożkowe tła motywów jasnego i ciemnego
Jasny motyw ma żywy cyan–deeppink gradient stożkowy, a ciemny motyw ma ciemny, subtelny gradient stożkowy. Więcej informacji o tym, co można zrobić za pomocą gradientów stożkowych, znajdziesz w artykule conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
Włączanie perspektywy 3D
Aby elementy mogły istnieć w przestrzeni 3D strony internetowej, należy zainicjować widoczny obszar z perspektywą. Zastosowałem perspektywę do elementu body i użyłem jednostek widocznego obszaru, aby uzyskać pożądany styl.
body {
perspective: 40vw;
}
To jest rodzaj wpływu, jaki może mieć perspektywa.
Stylizowanie 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 jak to zrobić.
Układ grupy przycisków
Flexbox może zarządzać układem kontenera. Zmień domyślny kierunek flex z wierszy na kolumny za pomocą flex-direction i upewnij się, że każdy element ma rozmiar zawartości, zmieniając stretch na start w przypadku 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(), aby karta nie obracała się poza czytelne zakresy. Zwróć uwagę, że środkowa wartość funkcji clamp to właściwość niestandardowa. Wartości --x i --y zostaną ustawione w JavaScript później, po interakcji z 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 użytkownik nie ma nic przeciwko animacjom, dodaj do przeglądarki wskazówkę, że przekształcenie tego elementu będzie się stale zmieniać za pomocą will-change.
Dodatkowo włącz interpolację, ustawiając transition w przypadku przekształceń. Ta
zmiana nastąpi, gdy mysz wejdzie w interakcję z kartą, co umożliwi płynne
przejścia do zmian rotacji. Animacja jest ciągła i pokazuje przestrzeń 3D, w której znajduje się karta, nawet jeśli mysz 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ą na 50%, ponieważ przeglądarka domyślnie ustawi 0% i 100% na domyślny styl elementu. Jest to skrót oznaczający animacje, które są naprzemienne i muszą zaczynać się i kończyć w tym samym miejscu. To świetny sposób na wyrażenie nieskończonych animacji naprzemiennych.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Stylizowanie elementów <li>
Każdy element listy (<li>) zawiera przycisk i elementy jego obramowania. Styl display jest zmieniany, aby element nie wyświetlał znaku ::marker. Styl position jest ustawiony na relative, dzięki czemu pseudoelementy przycisku mogą pozycjonować się w całym obszarze, który zajmuje 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;
}

Stylizowanie elementów <button>
Stylizowanie przycisków może być trudne, ponieważ trzeba uwzględnić wiele stanów i typów interakcji. Te przyciski szybko stają się złożone ze względu na równoważenie elementów pseudoelementów, animacji i interakcji.
Style początkowe <button>
Poniżej znajdziesz podstawowe style, które będą obsługiwać inne stany.
.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;
}

Pseudoelementy przycisku
Obramowania przycisku nie są tradycyjnymi obramowaniami, ale pseudoelementami o położeniu bezwzględnym.

Te elementy są kluczowe w prezentowaniu ustalonej perspektywy 3D. Jeden z tych pseudoelementów zostanie odepchnięty od przycisku, a drugi zostanie przyciągnięty bliżej użytkownika. Efekt jest najbardziej widoczny w przypadku przycisków u góry i u dołu.
.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
Wartość transform-style jest ustawiona na preserve-3d, dzięki czemu dzieci mogą rozmieścić się wzdłuż osi z. Wartość transform jest ustawiona na --distance
właściwość niestandardową, która będzie zwiększana po najechaniu kursorem i skupieniu.
.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));
}
}
Warunkowe style animacji
Jeśli użytkownik nie ma nic przeciwko animacji, przycisk sugeruje przeglądarce, że właściwość transform powinna być gotowa do zmiany, a przejście jest ustawione dla właściwości transform i background-color. Zwróć uwagę na różnicę w czasie trwania. Uważam, że daje to ładny, subtelny efekt opóźnienia.
.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 najechania kursorem i zaznaczenia
Celem animacji interakcji jest rozłożenie warstw, z których składa się przycisk o płaskim wyglądzie. Aby to zrobić, ustaw zmienną --distance na wartość początkową 1px. Selektor w przykładzie kodu poniżej sprawdza, czy przycisk jest zaznaczony lub czy urządzenie, które powinno wyświetlać wskaźnik zaznaczenia, najeżdża na niego kursorem, a nie czy jest on aktywowany. W takim przypadku CSS wykonuje te czynności:
- Zastosuj kolor tła po najechaniu kursorem.
- Zwiększ odległość .
- Dodaj efekt odbicia.
- Rozłóż w czasie 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 była nadal bardzo przydatna w przypadku reduced preferencji dotyczących ruchu.
Górne i dolne elementy pokazują efekt w subtelny sposób.
Drobne ulepszenia za pomocą JavaScriptu
Interfejs jest już obsługiwany przez klawiatury, czytniki ekranu, pady do gier, urządzenia dotykowe i myszy, ale możemy dodać kilka drobnych elementów JavaScript, aby ułatwić korzystanie z niego w kilku scenariuszach.
Obsługa klawiszy strzałek
Klawisz Tab to dobry sposób na poruszanie się po menu, ale oczekiwałbym, że na gamepadzie zaznaczenie będzie można przenosić za pomocą pada kierunkowego lub joysticków. Biblioteka roving-ux, która jest często używana w przypadku interfejsów wyzwań GUI, obsługuje klawisze strzałek. Poniższy kod informuje bibliotekę, że ma przechwytywać fokus w elemencie .threeD-button-set i przekazywać go do elementów podrzędnych przycisku.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interakcja paralaksy myszy
Śledzenie myszy i przechylanie menu ma naśladować interfejsy gier AR i VR, w których zamiast myszy możesz mieć wirtualny wskaźnik. Gdy elementy są bardzo czułe na wskaźnik, może to być ciekawe.
Jest to niewielka dodatkowa funkcja, dlatego interakcję umieścimy za zapytaniem o preferencje użytkownika dotyczące ruchu. W ramach konfiguracji zapisz też listę przycisków
w pamięci za pomocą querySelector i zapisz w pamięci podręcznej granice elementu
menuRect. Użyj tych granic, aby określić przesunięcie obrotu zastosowane do karty na podstawie 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 przyjmuje pozycje myszy x i y oraz zwraca wartość, której możemy użyć do obrócenia karty. Ta funkcja wykorzystuje pozycję myszy, aby określić, po której stronie pola się znajduje i o ile. Funkcja zwraca wartość delta.
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 obserwuj ruch myszy, przekaż pozycję do funkcji getAngles() i użyj wartości delta jako stylów właściwości niestandardowych. Podzieliłem przez 20, aby zwiększyć różnicę i zmniejszyć jej zmienność. Może istnieje lepszy sposób na to. Jeśli pamiętasz z początku, umieściliśmy właściwości --x i --y w środku funkcji clamp(). Zapobiega to nadmiernemu obracaniu karty przez pozycję myszy do nieczytelnej pozycji.
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
Podczas testowania menu gry w innych trybach pisania i językach pojawił się jeden problem.
Elementy <button> mają styl !important dla writing-mode w arkuszu stylów przeglądarki. Oznaczało to, że kod HTML menu gry musiał zostać zmieniony, aby dostosować się 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 dostarczanego przez przeglądarkę.
Podsumowanie
Teraz, gdy wiesz już, jak to zrobiłem, jak Ty byś to zrobił? 🙂 Czy możesz dodać do menu interakcję z akcelerometrem, aby przechylenie telefonu powodowało obracanie menu? Czy możemy ulepszyć tryb bez ruchu?
Urozmaićmy nasze podejście i poznajmy wszystkie sposoby tworzenia treści w internecie. Utwórz demo, wyślij mi na Twitterze linki, a ja dodam je do sekcji remiksów społeczności poniżej.
Remiksy społeczności
Na razie jest tu pusto