Creazione di un componente del menu di un gioco 3D

Una panoramica di base su come creare un menu di gioco 3D adattabile, reattivo e accessibile.

In questo post voglio condividere alcune idee su come creare un componente del menu di un gioco 3D. Prova la demo.

Demo

Se preferisci i video, ecco una versione di questo post su YouTube:

Panoramica

I videogiochi spesso presentano agli utenti un menu creativo e insolito, animato e nello spazio 3D. Nei nuovi giochi AR/VR è molto comune fare in modo che il menu sembri fluttuare nello spazio. Oggi recrieremo gli elementi essenziali di questo effetto, ma con l'aggiunta di una combinazione di colori adattabile e di opzioni per gli utenti che preferiscono ridurre il movimento.

HTML

Un menu di gioco è un elenco di pulsanti. Il modo migliore per rappresentare questo in HTML è come segue:

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

Un elenco di pulsanti viene annunciato correttamente alle tecnologie degli screen reader e funziona senza JavaScript o CSS.

un elenco puntato molto generico con pulsanti normali
come elementi.

CSS

L'impostazione di uno stile per l'elenco dei pulsanti è suddivisa nei seguenti passaggi generali:

  1. Configurazione delle proprietà personalizzate.
  2. Un layout flexbox.
  3. Un pulsante personalizzato con pseudoelementi decorativi.
  4. Posizionamento di elementi nello spazio 3D.

Panoramica delle proprietà personalizzate

Le proprietà personalizzate aiutano a distinguere i valori assegnando nomi significativi a valori apparentemente casuali, evitando il codice ripetuto e la condivisione dei valori tra i figli.

Di seguito sono riportate le query sui media salvate come variabili CSS, note anche come supporti personalizzati. Sono globali e verranno utilizzati in vari selettori per mantenere il codice conciso e leggibile. Il componente del menu del gioco utilizza le preferenze di movimento, la combinazione di colori del sistema e le funzionalità di gamma di colori del display.

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

Le seguenti proprietà personalizzate gestiscono la combinazione di colori e memorizzano i valori di posizione del mouse per rendere interattivo il menu del gioco al passaggio del mouse. Assegnare un nome alle proprietà personalizzate contribuisce alla leggibilità del codice in quanto rivela il caso d'uso del valore o un nome facile da ricordare per il risultato del valore.

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

Sfondi conici per i temi chiaro e scuro

Il tema chiaro ha un gradiente conico vivace da cyan a deeppink, mentre il tema scuro ha un gradiente conico scuro e delicato. Per scoprire di più su cosa si può fare con i gradienti conici, consulta conic.style.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Dimostrazione del passaggio da uno sfondo chiaro a uno scuro e viceversa.

Attivazione della prospettiva 3D

Affinché gli elementi esistano nello spazio 3D di una pagina web, è necessario inizializzare un viewport con prospettiva. Ho scelto di applicare la prospettiva all'elemento body e ho utilizzato le unità dell'area visibile per creare lo stile che mi piaceva.

body {
  perspective: 40vw;
}

Questo è il tipo di impatto che può avere la prospettiva.

Aggiungere stili all'elenco di pulsanti <ul>

Questo elemento è responsabile del layout complessivo della macro dell'elenco di pulsanti ed è una scheda mobile interattiva e 3D. Ecco come fare.

Layout del gruppo di pulsanti

Flexbox può gestire il layout del container. Modifica la direzione predefinita di flex da righe a colonne con flex-direction e assicurati che ogni elemento abbia le dimensioni dei relativi contenuti passando da stretch a start per 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;
}

Successivamente, definisci il contenitore come contesto di spazio 3D e configura le funzioni CSS clamp() per assicurarti che la scheda non ruoti oltre le rotazioni leggibili. Tieni presente che il valore intermedio per il limite è una proprietà personalizzata. Questi valori --x e --y verranno impostati da JavaScript in seguito all'interazione del mouse.

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

Poi, se il movimento è accettabile per l'utente che visita il sito, aggiungi un suggerimento al browser che informi che la trasformazione di questo elemento cambierà costantemente con will-change. Inoltre, abilita l'interpolazione impostando un transition sulle trasformazioni. Questa transição si verifica quando il mouse interagisce con la scheda, consentendo transizioni плавные alle modifiche di rotazione. L'animazione è in esecuzione costante e mostra lo spazio 3D in cui si trova la scheda, anche se un mouse non può o non interagisce con il componente.

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

L'animazione rotate-y imposta il fotogramma chiave intermedio solo su 50%, poiché il browser imposterà per impostazione predefinita 0% e 100% sullo stile predefinito dell'elemento. Si tratta di una scorciatoia per le animazioni che si alternano e devono iniziare e terminare nella stessa posizione. È un ottimo modo per creare animazioni infinite alternate.

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

Stili degli elementi <li>

Ogni elemento dell'elenco (<li>) contiene il pulsante e i relativi elementi del bordo. Lo stile display viene modificato in modo che l'elemento non mostri un ::marker. Lo stile position è impostato su relative in modo che gli pseudo-elementi del pulsante successivi possano posizionarsi all'interno dell'intera area occupata dal pulsante.

.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 dell&#39;elenco ruotato nello spazio 3D per mostrare la prospettiva e
ogni elemento dell&#39;elenco non ha più un punto elenco.

Definire lo stile degli elementi <button>

Lo stile dei pulsanti può essere un lavoro difficile, in quanto sono da tenere conto di molti stati e tipi di interazione. Questi pulsanti diventano rapidamente complessi a causa del bilanciamento di pseudo-elementi, animazioni e interazioni.

Stili <button> iniziali

Di seguito sono riportati gli stili di base che supporteranno gli altri stati.

.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 dell&#39;elenco di pulsanti in prospettiva 3D, questa volta con pulsanti stilizzati.

Pseudo-elementi pulsante

I bordi del pulsante non sono bordi tradizionali, ma pseudo-elementi con bordi in posizione assoluta.

Screenshot del riquadro Elementi di Chrome DevTools con un pulsante che mostra gli elementi ::before e ::after.

Questi elementi sono fondamentali per mostrare la prospettiva 3D stabilita. Uno di questi pseudo-elementi verrà allontanato dal pulsante e uno verrà avvicinato all'utente. L'effetto è più evidente nei pulsanti superiore e inferiore.

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

Stili di trasformazione 3D

Inferiore a transform-style è impostato su preserve-3d, quindi gli elementi secondari possono distribuirsi sull'asse z. transform è impostato sulla proprietà personalizzata --distance, che verrà aumentata al passaggio del mouse e al fuoco.

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

Stili di animazione condizionali

Se l'utente accetta il movimento, il pulsante suggerisce al browser che la proprietà transform deve essere pronta per la modifica e che è impostata una transizione per le proprietà transform e background-color. Noti la differenza di durata? Ho pensato che fosse un effetto graduale molto piacevole.

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

Stili di interazione con il passaggio del mouse e lo stato attivo

Lo scopo dell'animazione di interazione è distribuire i livelli che compongono il pulsante con aspetto piatto. A questo scopo, imposta la variabile --distance, inizialmente su 1px. Il selettore mostrato nel seguente esempio di codice controlla se il pulsante è selezionato o se un dispositivo che dovrebbe visualizzare un indicatore di selezione lo sta passando sopra e non lo sta attivando. In questo caso, applica CSS per eseguire quanto segue:

  • Applica il colore di sfondo al passaggio del mouse.
  • Aumenta la distanza.
  • Aggiungi un effetto di transizione graduale.
  • Sposta le transizioni degli pseudo-elementi.
.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 }
    }
  }
}

La prospettiva 3D era ancora molto interessante per la preferenza di movimento reduced. Gli elementi superiore e inferiore mostrano l'effetto in modo elegante e discreto.

Piccoli miglioramenti con JavaScript

L'interfaccia è già utilizzabile da tastiere, screen reader, gamepad, tocchi e mouse, ma possiamo aggiungere alcuni tocchi leggeri di JavaScript per semplificare un paio di scenari.

Supporto dei tasti freccia

Il tasto Tab è un ottimo modo per navigare nel menu, ma mi aspetterei che il pad direzionale o i joystick spostino lo stato attivo su un gamepad. La libreria roving-ux, spesso utilizzata per le interfacce GUI delle sfide, gestirà i tasti freccia per noi. Il codice riportato di seguito indica alla biblioteca di bloccare lo stato attivo in .threeD-button-set e di inoltrarlo ai pulsanti secondari.

import {rovingIndex} from 'roving-ux'

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

Interazione con la parallasse del mouse

Il tracciamento del mouse e l'inclinazione del menu hanno lo scopo di imitare le interfacce di videogiochi AR e VR, dove al posto del mouse potresti avere un puntatore virtuale. Può essere divertente quando gli elementi sono estremamente consapevoli del puntatore.

Poiché si tratta di una piccola funzionalità aggiuntiva, imposteremo l'interazione dietro una query relativa alle preferenze di movimento dell'utente. Inoltre, nell'ambito della configurazione, memorizza il componente dell'elenco di pulsanti in memoria con querySelector e memorizza nella cache i limiti dell'elemento in menuRect. Utilizza questi limiti per determinare l'offset di rotazione applicato alla scheda in base alla posizione del mouse.

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

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

A questo punto, abbiamo bisogno di una funzione che accetti le posizioni del mouse x e y e restituisca un valore che possiamo utilizzare per ruotare la scheda. La seguente funzione utilizza la posizione del mouse per determinare quale lato della casella è all'interno e di quanto. Il valore delta viene restituito dalla funzione.

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

Infine, osserva il movimento del mouse, passa la posizione alla nostra funzione getAngles() e utilizza i valori delta come stili delle proprietà personalizzate. Ho diviso per 20 per aumentare il valore della variazione e renderla meno discontinua. Potrebbe esserci un modo migliore per farlo. Se ricordi dall'inizio, mettiamo gli oggetti --x e --y al centro di una funzione clamp() per evitare che la posizione del mouse ruoti eccessivamente la scheda in una posizione illeggibile.

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

Traduzioni e indicazioni

C'era una cosa da fare quando si provava il menu del gioco in altre modalità e lingue di scrittura.

Gli elementi <button> hanno uno stile !important per writing-mode nello stile delle pagine dell'agente utente. Ciò significava che il codice HTML del menu del gioco doveva essere modificato per adattarsi al design desiderato. La modifica dell'elenco di pulsanti in un elenco di link consente alle proprietà logiche di modificare la direzione del menu, poiché gli elementi <a> non hanno uno stile !important fornito dal browser.

Conclusione

Ora che sai come ho fatto, come faresti? 🙂 Puoi aggiungere l'interazione con l'accelerometro al menu, in modo che la rotazione dello smartphone ruoti il menu? Possiamo migliorare l'esperienza senza movimento?

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.

Remix della community

Ancora nessun elemento da visualizzare.