3D-Spielemenükomponente erstellen

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

Demo

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.

eine sehr allgemein aussehende Aufzählungsliste
mit normalen Schaltflächen als Elemente.

CSS

Der Stil der Schaltflächenliste gliedert sich in die folgenden übergeordneten Schritte:

  1. Benutzerdefinierte Eigenschaften einrichten
  2. Ein Flexbox-Layout.
  3. Eine benutzerdefinierte Schaltfläche mit dekorativen Pseudoelementen.
  4. 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);
  }
}
Darstellung des Wechsels des Hintergrunds zwischen hellen und dunklen Farbeinstellungen.

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

Screenshot der Liste, die im 3D-Raum gedreht wurde, um die Perspektive zu zeigen. Jedes Listenelement hat keinen Aufzählungspunkt mehr.

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

Screenshot der Schaltflächenliste in 3D-Perspektive, diesmal mit Schaltflächen mit benutzerdefinierten Stilen.

Pseudoelemente der Schaltfläche

Die Rahmen der Schaltfläche sind keine herkömmlichen Rahmen, sondern absolute Positions-Pseudoelemente mit Rahmen.

Screenshot des Steuerfelds „Elemente“ der Chrome-Entwicklertools mit einer Schaltfläche mit den Elementen „::before“ und „::after“

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.