3D-Spielemenükomponente erstellen

Eine grundlegende Übersicht darüber, wie Sie ein responsives, adaptives und barrierefreies 3D-Spielmenü erstellen.

In diesem Beitrag möchte ich meine Überlegungen zur Entwicklung einer 3D-Spielmenükomponente teilen. Demo ansehen

Demo

Wenn Sie lieber ein Video ansehen möchten, finden Sie hier eine YouTube-Version dieses Beitrags:

Übersicht

Videospiele bieten Nutzern oft ein kreatives und ungewöhnliches Menü, das animiert und in 3D dargestellt wird. In neuen AR‑/VR‑Spielen ist es beliebt, das Menü so zu gestalten, dass es im Raum zu schweben scheint. Heute werden wir die Grundlagen dieses Effekts nachbilden, aber mit dem zusätzlichen Flair eines adaptiven Farbschemas und Anpassungen für Nutzer, die weniger Bewegung bevorzugen.

HTML

Ein Spielmenü ist eine Liste von Schaltflächen. Am besten wird dies in HTML so dargestellt:

<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 von Schaltflächen wird von Screenreadern gut angesagt und funktioniert ohne JavaScript oder CSS.

eine sehr allgemeine Aufzählungsliste mit regulären Schaltflächen als Elementen.

CSS

Das Gestalten der Schaltflächenliste umfasst die folgenden allgemeinen Schritte:

  1. Benutzerdefinierte Eigenschaften einrichten
  2. Ein Flexbox-Layout.
  3. Ein benutzerdefinierter Button mit dekorativen Pseudoelementen.
  4. Elemente im 3D-Raum platzieren

Benutzerdefinierte Eigenschaften – Übersicht

Mit benutzerdefinierten Eigenschaften lassen sich Werte eindeutig zuordnen, indem Sie ansonsten zufällig aussehenden Werten aussagekräftige Namen geben. So vermeiden Sie wiederholten Code und können Werte zwischen untergeordneten Elementen teilen.

Unten sehen Sie Media-Queries, die als CSS-Variablen gespeichert sind, auch als benutzerdefinierte Media bezeichnet. Sie sind global und werden in verschiedenen Selektoren verwendet, um den Code kurz und lesbar zu halten. Die Komponente für das Spielmenü verwendet Bewegungseinstellungen, das Farbschema des Systems und die Farbtonbereichsfunktionen des Displays.

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

Die folgenden benutzerdefinierten Eigenschaften verwalten das Farbschema und enthalten Werte für die Mausposition, um das Spielmenü interaktiv zu gestalten. Durch die Benennung benutzerdefinierter Eigenschaften wird der Code lesbarer, da der Anwendungsfall für den Wert oder ein benutzerfreundlicher Name für das Ergebnis des Werts angegeben wird.

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

Konische Hintergründe für das helle und das dunkle Design

Das helle Design hat einen lebendigen cyan- bis deeppink-konischen Farbverlauf, während das dunkle Design einen dunklen, dezenten konischen Farbverlauf hat. Weitere Informationen zu den Möglichkeiten mit konischen Verlä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);
  }
}
Demonstration of background changing between light and dark color preferences.

3D-Perspektive aktivieren

Damit Elemente im 3D-Raum einer Webseite vorhanden sein können, muss ein Darstellungsbereich mit Perspektive initialisiert werden. Ich habe die Perspektive auf das body-Element gesetzt und Viewport-Einheiten verwendet, um den gewünschten Stil zu erstellen.

body {
  perspective: 40vw;
}

Das ist die Art von Auswirkungen, die Perspektive haben kann.

<ul>-Schaltflächenliste gestalten

Dieses Element ist für das allgemeine Makro-Layout der Schaltflächenliste sowie für die interaktive 3D-Schwebekarte verantwortlich. So gehts:

Layout der Schaltflächengruppe

Mit Flexbox kann das Containerlayout verwaltet werden. Ändern Sie die Standardrichtung von Flex von Zeilen in Spalten mit flex-direction und sorgen Sie dafür, dass jedes Element die Größe seines Inhalts hat, indem Sie stretch in start für align-items ändern.

.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 legen Sie den Container als 3D-Raumkontext fest und richten Sie CSS-clamp()-Funktionen ein, damit die Karte nicht über lesbare Rotationen hinaus gedreht wird. Der mittlere Wert für die Clamp ist eine benutzerdefinierte Eigenschaft. Die Werte --x und --y werden später bei der Mausinteraktion über 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 die Bewegung für den Besucher in Ordnung ist, fügen Sie dem Browser einen Hinweis hinzu, dass sich die Transformation dieses Elements mit will-change ständig ändert. Aktivieren Sie außerdem die Interpolation, indem Sie eine transition für Transformationen festlegen. Dieser Übergang erfolgt, wenn die Maus mit der Karte interagiert, sodass Rotationsänderungen reibungslos erfolgen. Die Animation wird fortlaufend wiederholt und zeigt den 3D-Raum, in dem sich die Karte befindet, auch wenn keine Mausinteraktion mit der Komponente stattfindet.

@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 auf 50% festgelegt, da für 0% und 100% standardmäßig der Standardstil des Elements verwendet wird. Dies ist eine Kurzform für Animationen, die sich abwechseln und am selben Ort beginnen und enden müssen. So lassen sich unendlich viele abwechselnde Animationen erstellen.

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

<li>-Elemente gestalten

Jedes Listenelement (<li>) enthält die Schaltfläche und ihre Rahmenelemente. Der Stil display wurde geändert, sodass für das Element kein ::marker angezeigt wird. Der Stil position ist auf relative gesetzt, damit die anstehenden Pseudo-Elemente der Schaltfläche sich innerhalb des gesamten Bereichs positionieren können, den die Schaltfläche einnimmt.

.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. Jeder Listeneintrag hat keinen Aufzählungsstrich mehr.

<button>-Elemente gestalten

Das Stylen von Schaltflächen kann schwierig sein, da viele Status und Interaktionstypen berücksichtigt werden müssen. Diese Schaltflächen können schnell komplex werden, da Pseudo-Elemente, Animationen und Interaktionen aufeinander abgestimmt werden müssen.

Erste <button>-Stile

Unten sehen Sie die grundlegenden Stile, die die anderen Status 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 formatierten Schaltflächen.

Pseudoelemente für Schaltflächen

Die Rahmen der Schaltfläche sind keine herkömmlichen Rahmen, sondern Pseudo-Elemente mit Rahmen, die absolut positioniert sind.

Screenshot des Bereichs „Elemente“ in den Chrome-Entwicklertools. Eine Schaltfläche mit den Elementen „::before“ und „::after“ ist zu sehen.

Diese Elemente sind entscheidend, um die 3D-Perspektive zu präsentieren. Eines dieser Pseudoelemente wird von der Schaltfläche weggedrückt und eines wird näher an den Nutzer herangezogen. Die Wirkung ist bei den oberen und unteren Schaltflächen am deutlichsten zu sehen.

.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

Unten ist transform-style auf preserve-3d festgelegt, damit die untergeordneten Elemente auf der z-Achse verteilt werden können. Die transform wird auf die benutzerdefinierte Eigenschaft --distance gesetzt, die bei Mauszeigerbewegung 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 Bewegung einverstanden ist, wird dem Browser mit der Schaltfläche mitgeteilt, dass die transform-Eigenschaft für Änderungen bereit sein sollte. Außerdem wird eine Übergangseinstellung für die Eigenschaften transform und background-color festgelegt. Achten Sie auf den Unterschied in der Dauer. Ich finde, das sorgt für einen schönen, subtilen Staffelungseffekt.

.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 für Hover und Fokus

Ziel der Interaktionsanimation ist es, die Ebenen, aus denen die flache Schaltfläche besteht, zu verteilen. Legen Sie dazu die Variable --distance fest, anfangs auf 1px. Mit dem Selektor im folgenden Codebeispiel wird geprüft, ob der Mauszeiger auf die Schaltfläche bewegt wird oder ob sie von einem Gerät, auf dem ein Fokusindikator angezeigt werden soll, fokussiert wird, ohne dass sie aktiviert wird. In diesem Fall wird CSS angewendet, um Folgendes zu tun:

  • Wenden Sie die Hintergrundfarbe für den Mouseover-Effekt an.
  • Erhöhen Sie den Abstand .
  • Einen Bounce-Ease-Effekt hinzufügen
  • Übergänge von Pseudoelementen staffeln
.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 für die Bewegungspräferenz reduced wirklich gut. Die oberen und unteren Elemente zeigen den Effekt auf subtile Weise.

Kleine Verbesserungen mit JavaScript

Die Benutzeroberfläche ist bereits über Tastaturen, Screenreader, Gamepads, Touch- und Mausbedienung nutzbar. Wir können aber mit etwas JavaScript einige Szenarien vereinfachen.

Pfeiltasten unterstützen

Mit der Tabulatortaste lässt sich gut im Menü navigieren, aber ich würde erwarten, dass sich der Fokus auf einem Gamepad mit dem Steuerkreuz oder den Joysticks verschieben lässt. Die Bibliothek roving-ux, die häufig für GUI-Challenge-Oberflächen verwendet wird, verarbeitet die Pfeiltasten für uns. Der folgende Code weist die Bibliothek an, den Fokus in .threeD-button-set zu halten und an die untergeordneten Schaltflächen weiterzuleiten.

import {rovingIndex} from 'roving-ux'

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

Maus-Parallaxen-Interaktion

Das Verfolgen der Maus und das Neigen des Menüs sollen AR- und VR-Videospieloberflächen nachempfinden, bei denen anstelle einer Maus ein virtueller Zeiger verwendet wird. Es kann interessant sein, wenn Elemente sehr auf den Mauszeiger reagieren.

Da es sich um eine kleine Zusatzfunktion handelt, wird die Interaktion erst nach einer Abfrage der Bewegungspräferenz des Nutzers ausgelöst. Speichern Sie außerdem im Rahmen der Einrichtung die Schaltflächenliste mit querySelector im Arbeitsspeicher und legen Sie die Grenzen des Elements im Cache mit menuRect ab. Mit diesen Grenzen wird der Drehoffset bestimmt, der basierend auf der Mausposition auf die Karte 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 Mauspositionen x und y akzeptiert und einen Wert zurückgibt, mit dem wir die Karte drehen können. Die folgende Funktion verwendet die Mausposition, um zu bestimmen, auf welcher Seite des Felds sie sich befindet und wie weit sie davon entfernt ist. 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 unsere getAngles()-Funktion und verwenden Sie die Deltawerte als benutzerdefinierte Property-Stile. Ich habe durch 20 geteilt, um das Delta zu vergrößern und es weniger zuckend zu machen. Vielleicht gibt es eine bessere Möglichkeit, das zu tun. Wie Sie sich vielleicht erinnern, haben wir die Attribute --x und --y in die Mitte einer clamp()-Funktion eingefügt. Dadurch wird verhindert, dass die Karte durch die Mausposition zu stark in eine unleserliche Position 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 ist ein Problem aufgetreten.

<button>-Elemente haben im Stylesheet des User-Agents einen !important-Stil für writing-mode. Das HTML des Spielmenüs musste also geändert werden, um das gewünschte Design zu ermöglichen. Wenn Sie die Schaltflächenliste in eine Liste mit Links ändern, können Sie die Menürichtung mit logischen Eigenschaften ändern, da <a>-Elemente keinen vom Browser bereitgestellten !important-Stil haben.

Fazit

Jetzt weißt du, wie ich das gemacht habe. Wie würdest du vorgehen? 🙂 Kannst du dem Menü eine Beschleunigungsmesser-Interaktion hinzufügen, sodass sich das Menü dreht, wenn du dein Smartphone neigst? Kann die Funktion „Keine Bewegung“ verbessert werden?

Wir möchten unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, die das Web bietet. Erstelle eine Demo, schick mir einen Tweet mit den Links und ich füge sie unten im Bereich „Community-Remixe“ hinzu.

Community-Remixe

Noch keine Aktivität hierzu.