Ein grundlegender Überblick über das Erstellen eines responsiven, adaptiven und barrierefreien 3D-Spielemenüs.
In diesem Beitrag möchte ich zeigen, wie man eine Menükomponente für ein 3D-Spiel erstellt. Demo ansehen
Falls du lieber ein Video hast, findest du hier eine YouTube-Version dieses Beitrags:
Überblick
Videospiele bieten Nutzern oft ein kreatives und ungewöhnliches Menü, das animiert und in 3D dargestellt ist. In neuen AR-/VR-Spielen ist es beliebt, das Menü so darzustellen, als ob es im Weltraum schwebt. Heute stellen wir die wesentlichen Elemente dieses Effekts nach, allerdings mit dem zusätzlichen Flair eines adaptiven Farbschemas und einer Anpassung an Nutzer, die verminderte Bewegung bevorzugen.
HTML
Ein Spielmenü besteht aus einer Liste von Schaltflächen. So lässt sich dies am besten in HTML darstellen:
<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>
Eine Liste mit Schaltflächen ist für Screenreader-Technologien gut bekannt und funktioniert ohne JavaScript oder CSS.
CSS
Der Stil der Schaltflächenliste gliedert sich in die folgenden übergeordneten Schritte:
- Benutzerdefinierte Eigenschaften einrichten
- Ein Flexbox-Layout.
- Eine benutzerdefinierte Schaltfläche mit dekorativen Pseudoelementen.
- Elemente im 3D-Raum platzieren
Übersicht über benutzerdefinierte Eigenschaften
Benutzerdefinierte Attribute helfen, Werte zu unterscheiden, indem sie ansonsten zufällig aussehende Werte aussagekräftige Namen geben. So vermeiden Sie wiederholten Code und die gemeinsame Nutzung von Werten unter untergeordneten Elementen.
Unten finden Sie Medienabfragen, die als CSS-Variablen gespeichert sind, die auch als benutzerdefinierte Medien bezeichnet werden. Diese sind global und werden in verschiedenen Selektoren verwendet, um den Code prägnant und lesbar zu halten. Die Komponente für das Spielmenü verwendet Bewegungseinstellungen, das Systemfarbschema und die Farbbereichsfunktionen des Displays.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);
Mit den folgenden benutzerdefinierten Eigenschaften können Sie das Farbschema verwalten und Positionswerte der Maus gedrückt halten, um das Spielmenü interaktiv zu gestalten und den Mauszeiger darüber zu bewegen. Das Benennen benutzerdefinierter Eigenschaften verbessert die Lesbarkeit des Codes, da sich der Anwendungsfall für den Wert oder ein Anzeigename für das Ergebnis des Werts erkennen lässt.
.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);
}
}
}
Kegelförmige Hintergründe in hellem und dunklem Design
Das helle Design hat einen konischen Farbverlauf in lebendigen Farben von cyan
bis deeppink
, während das dunkle Design einen dunklen, subtilen Kegelverlauf hat. Weitere Informationen zu den Möglichkeiten mit konischen Farbverläufen finden Sie unter conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
3D-Perspektive aktivieren
Damit Elemente im 3D-Bereich einer Webseite vorhanden sind, muss ein Darstellungsbereich mit Perspektive initialisiert werden. Dabei habe ich das body
-Element perspektivisch gestaltet
und mit Darstellungsbereich-Einheiten einen Stil erstellt, der mir gefällt.
body {
perspective: 40vw;
}
Dies ist die Art der Wirkungsperspektive.
Stil der <ul>
-Schaltflächenliste festlegen
Dieses Element ist für das Gesamtlayout des Makros der Schaltflächenliste verantwortlich und ist eine interaktive und unverankerte 3D-Karte. Dafür gibt es folgende Möglichkeiten:
Layout der Schaltflächengruppe
Flexbox kann das Containerlayout verwalten. Ändern Sie die Standardrichtung der Flex von Zeilen in Spalten mit flex-direction
und achten Sie darauf, dass jedes Element die Größe seines Inhalts hat. Ändern Sie dazu für align-items
von stretch
zu start
.
.threeD-button-set {
/* remove <ul> margins */
margin: 0;
/* vertical rag-right layout */
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2.5vh;
}
Als Nächstes richte den Container als Kontext für einen 3D-Raum ein und richte die CSS-clamp()
-Funktionen ein, damit die Karte nicht über lesbare Rotationen hinaus gedreht wird. Der mittlere Wert für die Einschränkung ist eine benutzerdefinierte Eigenschaft. Diese Werte für --x
und --y
werden später bei einer Mausinteraktion aus JavaScript festgelegt.
.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)
)
)
;
}
Wenn Bewegung für den Besucher in Ordnung ist, weisen Sie dem Browser mit will-change
darauf hin, dass sich die Transformation dieses Elements ständig ändert.
Außerdem können Sie die Interpolation aktivieren, indem Sie für Transformationen einen transition
festlegen. Dieser Übergang erfolgt, wenn die Maus mit der Karte interagiert, wodurch ein reibungsloser Übergang zu Rotationsänderungen möglich ist. Die Animation ist eine kontinuierlich laufende Animation, die den 3D-Raum veranschaulicht, in dem sich die Karte befindet, auch wenn eine Maus nicht mit der Komponente interagieren kann oder nicht.
@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;
}
}
Bei der rotate-y
-Animation wird nur der mittlere Keyframe bei 50%
festgelegt, da der Browser 0%
und 100%
standardmäßig auf den Standardstil des Elements festlegt. Dies ist eine Abkürzung für Animationen, die abwechselnd an derselben Position beginnen und enden. Es ist eine großartige Möglichkeit, sich unendlich abwechselnde Animationen zu artikulieren.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
<li>
-Elemente gestalten
Jedes Listenelement (<li>
) enthält die Schaltfläche und die zugehörigen Rahmenelemente. Der Stil display
wird geändert, sodass für das Element kein ::marker
angezeigt wird. Der Stil position
ist auf relative
gesetzt, damit sich die nachfolgenden Pseudoelemente der Schaltfläche im gesamten von der Schaltfläche belegten Bereich positionieren können.
.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>
-Elemente gestalten
Das Gestalten von Schaltflächen kann mühsam sein. Es gibt viele Status und Interaktionstypen, die berücksichtigt werden müssen. Diese Schaltflächen werden schnell komplex, da Pseudoelemente, Animationen und Interaktionen aufeinander abgestimmt werden.
Ursprüngliche <button>
-Stile
Unten sind die grundlegenden Stile aufgeführt, die die anderen Bundesstaaten unterstützen.
.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;
}
Pseudoelemente der Schaltfläche
Die Rahmen der Schaltfläche sind keine herkömmlichen Rahmen, sondern absolute Positions-Pseudoelemente mit Rahmen.
Diese Elemente sind entscheidend, um die etablierte 3D-Perspektive zu präsentieren. Eines dieser Pseudoelemente wird von der Schaltfläche weggedrückt und eines wird näher an den Nutzer gezogen. Besonders auffällig ist dies bei den Schaltflächen oben und unten.
.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-Transformationsstile
Darunter ist transform-style
auf preserve-3d
gesetzt, sodass die untergeordneten Elemente sich auf der z
-Achse freilegen können. Für transform
ist die benutzerdefinierte Eigenschaft --distance
festgelegt, die bei Mauszeiger und Fokus erhöht wird.
.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));
}
}
Bedingte Animationsstile
Wenn der Nutzer mit Bewegungen einverstanden ist, weist die Schaltfläche dem Browser darauf hin, dass die Transformationseigenschaft bereit für Änderungen sein und für die Eigenschaften transform
und background-color
ein Übergang festgelegt ist. Wie sich die Dauer verändert hat,
hatte ich einen schönen, gestaffelten Effekt.
.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 }
}
}
Interaktionsstile „Hover“ und „Fokus“
Das Ziel der Interaktionsanimation besteht darin, die Ebenen, aus denen die flach erscheinende Schaltfläche besteht, zu verteilen. Dazu setzen Sie die Variable --distance
zuerst auf 1px
. Mit der Auswahl im folgenden Codebeispiel wird geprüft, ob die Schaltfläche von einem Gerät, das eine Fokusanzeige sehen sollte, bewegt wird oder fokussiert ist, und ob das Gerät nicht aktiviert ist. In diesem Fall werden CSS für folgende Aktionen angewendet:
- Wenden Sie die Hintergrundfarbe beim Bewegen des Mauszeigers an.
- Vergrößern Sie den Abstand .
- Fügen Sie einen Bounce-Effekt hinzu.
- Die Pseudoelementübergänge verstreichen
.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 }
}
}
}
Die 3D-Perspektive war immer noch sehr gut für die Bewegungseinstellung reduced
.
Das obere und das untere Element zeigen den Effekt auf dezente Weise.
Kleine Verbesserungen mit JavaScript
Die Benutzeroberfläche kann bereits über Tastaturen, Screenreader, Gamepads, Touch-Gesten und eine Maus bedient werden, wir können jedoch einige kleine Akzente von JavaScript hinzufügen, um einige Szenarien zu vereinfachen.
Unterstützende Pfeiltasten
Mit der Tabulatortaste kannst du gut im Menü navigieren, aber ich erwarte, dass die Richtungstaste oder die Joysticks den Fokus auf ein Gamepad bewegen. Die Bibliothek roving-ux, die häufig für GUI Challenge-Schnittstellen verwendet wird, verarbeitet die Pfeiltasten für uns. Mit dem folgenden Code wird die Bibliothek angewiesen, den Fokus innerhalb von .threeD-button-set
zu erfassen und an die untergeordneten Elemente weiterzuleiten.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interaktion mit Maus-Parallaxe
Das Verfolgen der Maus und das Neigen des Menüs sollen AR- und VR-Videospieloberflächen imitieren, wobei anstelle einer Maus ein virtueller Zeiger verwendet werden kann. Es kann lustig sein, wenn Elemente den Zeiger hyperbewusst machen.
Da dies ein kleines Extra-Feature ist, stellen wir die Interaktion hinter eine Abfrage der Bewegungspräferenz des Nutzers. Außerdem sollten Sie die Komponente „Schaltflächenliste“ im Rahmen der Einrichtung mit querySelector
im Arbeitsspeicher speichern und die Grenzen des Elements in menuRect
zwischenspeichern. Mit diesen Begrenzungen kannst du den Rotationsversatz festlegen, der auf der Karte basierend auf der Mausposition angewendet wird.
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Als Nächstes benötigen wir eine Funktion, die die x
- und y
-Positionen der Maus akzeptiert und einen Wert zurückgibt, mit dem wir die Karte drehen können. Bei der folgenden Funktion wird anhand der Mausposition ermittelt, in welcher Seite der Box sich die Box befindet und um wie viel. Das Delta wird von der Funktion zurückgegeben.
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}
}
Beobachten Sie schließlich die Mausbewegung, übergeben Sie die Position an die Funktion getAngles()
und verwenden Sie die Deltawerte als Stile für benutzerdefinierte Eigenschaften. und 20 geteilt, um das Delta
zu füllen und es weniger umständlich zu machen. Wie Sie sich von Anfang an erinnern, stellen wir die Attribute --x
und --y
in die Mitte einer clamp()
-Funktion. Dadurch wird verhindert, dass die Karte zu schnell durch die Maus gedreht wird.
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`)
})
}
Übersetzungen und Wegbeschreibungen
Beim Testen des Spielmenüs in anderen Schreibmodi und -sprachen kam es zu einem Fehler.
<button>
-Elemente haben im User-Agent-Stylesheet den Stil !important
für writing-mode
. Dies bedeutete, dass der HTML-Code des Spielemenüs angepasst werden musste, um das gewünschte Design zu berücksichtigen. Wird die Schaltflächenliste in eine Liste mit Links geändert, können logische Eigenschaften die Menürichtung ändern, da für <a>
-Elemente kein vom Browser bereitgestelltes !important
-Format vorhanden ist.
Fazit
Jetzt, wo Sie wissen, wie ich es gemacht habe, wie geht das? Können wir das No-Motion-Erlebnis verbessern?
Diversifizieren wir unsere Ansätze und lernen Sie alle Möglichkeiten kennen, wie wir das Web nutzen können. Erstelle eine Demo und twittere mich über Links, und ich füge sie unten zum Abschnitt über Community-Remixe hinzu.
Community-Remixe
Hier gibt es noch nichts zu sehen.