3D oyun menü bileşeni oluşturma

Duyarlı, uyarlanabilir ve erişilebilir bir 3D oyun menüsünün nasıl oluşturulacağına ilişkin temel bir genel bakış.

Bu gönderide, 3D oyun menüsü bileşeni oluşturma konusunda düşüncelerimi paylaşmak istiyorum. Demoyu deneyin.

Demo

Video kullanmayı tercih ederseniz bu gönderinin YouTube versiyonunu kullanabilirsiniz:

Genel bakış

Video oyunları genellikle kullanıcılara animasyonlu ve 3B uzayda yaratıcı ve alışılmadık bir menü sunar. Yeni artırılmış gerçeklik (AR)/sanal gerçeklik (VR) oyunlarında menünün uzayda süzülüyormuş gibi görünmesini sağlamak için çok popüler. Bugün bu efektin temel özelliklerini yeniden oluşturacağız, ancak uyarlanabilen renk şeması ve daha az hareketi tercih eden kullanıcılar için konaklama olanağı ekleyeceğiz.

HTML

Oyun menüsü, düğmelerden oluşan bir listedir. Bunu HTML'de göstermenin en iyi yolu aşağıdaki gibidir:

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

Düğmelerden oluşan bir liste kendisini ekran okuyucu teknolojilerine iyice tanıtır ve JavaScript veya CSS olmadan çalışır.

normal düğmeleri içeren çok genel
bir madde listesi kullanabilirsiniz.

CSS

Düğme listesinin stilini belirlemek aşağıdaki üst düzey adımlara ayrılır:

  1. Özel özellikleri ayarlama.
  2. Flexbox düzeni.
  3. Dekoratif sözde öğeler içeren özel bir düğme.
  4. Öğeleri 3D alana yerleştirme.

Özel özelliklere genel bakış

Özel özellikler, aksi halde rastgele görünen değerlere anlamlı adlar vererek, yinelenen kodlardan ve alt öğeler arasında değer paylaşmaktan kaçınarak değerleri netleştirmeye yardımcı olur.

Aşağıda, custom media olarak da bilinen, CSS değişkenleri olarak kaydedilen medya sorguları verilmiştir. Bunlar geneldir ve kodun kısa ve okunaklı olması için çeşitli seçicilerde kullanılır. Oyun menüsü bileşeni, ekranın hareket tercihleri, sistem renk şeması ve renk aralığı özelliklerini kullanır.

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

Aşağıdaki özel özellikler, oyun menüsünü etkileşimli hale getirmek için renk şemasını yönetir ve fare konum değerlerini tutar. Özel özellikleri adlandırmak, değerin kullanım alanını veya değerin sonucuna ilişkin kolay bir adı ortaya koyduğu için kod okunaklılığına yardımcı olur.

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

Açık ve koyu temalı arka plan konik arka planlar

Açık temada canlı cyan-deeppink konik gradya, koyu temada ise koyu ve ince bir konik gradyan vardır. Konik gradyanlarla neler yapılabileceği hakkında daha fazla bilgi için conic.style öğesine bakın.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
Açık ve koyu renk tercihleri arasında değişen arka plan gösterimi.

3D perspektifi etkinleştirme

Öğelerin web sayfasının 3D alanında yer alması için perspektif içeren bir görüntü alanının başlatılması gerekir. Perspektifi body öğesine koymayı seçtim ve sevdiğim stili oluşturmak için görüntü alanı birimleri kullandım.

body {
  perspective: 40vw;
}

Bu, bakış açısının sunabileceği etki türünü ifade eder.

<ul> düğme listesinin stilini belirleme

Bu öğe, genel düğme listesi makro düzeninin yanı sıra etkileşimli ve 3D kayan bir kart olmasından sorumludur. Bunu aşağıdaki şekilde yapabilirsiniz.

Düğme grubu düzeni

Flexbox, kapsayıcı düzenini yönetebilir. flex-direction kullanarak esnek özelliğin varsayılan yönünü satırlardan sütunlara çevirin ve align-items için stretch olan değeri start olarak değiştirerek her bir öğenin içerik boyutunda olduğundan emin olun.

.threeD-button-set {
  /* remove <ul> margins */
  margin: 0;

  /* vertical rag-right layout */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2.5vh;
}

Daha sonra, kapsayıcıyı 3D alan bağlamı olarak oluşturun ve kartın okunabilir döndürmeler dışında dönmediğinden emin olmak için CSS clamp() işlevleri ayarlayın. Sabitleme için orta değerin özel bir özellik olduğuna dikkat edin. Bu --x ve --y değerleri daha sonra fare etkileşimi olduğunda JavaScript'ten ayarlanır.

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

Ardından, ziyaret eden kullanıcı harekette bir sorun değilse tarayıcıya will-change ile bu öğenin dönüşümünün sürekli değişeceğine dair bir ipucu ekleyin. Ayrıca, dönüşümlerde transition ayarlayarak interpolasyonu etkinleştirin. Bu geçiş, fare kartla etkileşime girdiğinde gerçekleşerek rotasyon değişikliklerine sorunsuz geçiş sağlar. Animasyon, farenin bileşenle etkileşimde bulunamadığı veya etkileşiminin olmadığı durumlarda bile kartın içinde bulunduğu 3D alanı gösteren, sürekli çalışan bir animasyondur.

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

Tarayıcı varsayılan olarak 0% ve 100% değerlerini öğenin varsayılan stiline ayarlayacağı için rotate-y animasyonu yalnızca 50% konumunda orta animasyon karesini ayarlar. Bu, birbiriyle değişen ve aynı konumda başlayıp bitmesi gereken animasyonların kısaltılmış halidir. Bu, sonsuz sayıda alternatif animasyonun ifade edilmesi için harika bir yoldur.

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

<li> öğelerinin stil özelliklerini ayarlama

Her liste öğesi (<li>) düğmeyi ve kenarlık öğelerini içerir. display stili değiştirildiğinde öğede ::marker gösterilmez. position stili, relative olarak ayarlandı. Böylece, yaklaşan düğme sözde öğeleri, düğmenin tükettiği tüm alan içinde kendilerini konumlandırabilir.

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

Listenin ekran görüntüsü, perspektifi göstermek için 3D alanda döndürüldü ve her liste öğesinde artık madde işareti yok.

<button> öğelerinin stil özelliklerini ayarlama

Düğmelerin stilini belirlemek zor olabilir. Hesaba katılacak birçok durum ve etkileşim türü vardır. Bu düğmeler, sözde öğelerin, animasyonların ve etkileşimlerin dengelenmesi nedeniyle hızlı bir şekilde karmaşık hale gelir.

İlk <button> stilleri

Diğer eyaletleri destekleyecek temel stiller aşağıda verilmiştir.

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

Düğme listesinin bu kez stilize edilmiş düğmelerle, 3D perspektifteki ekran görüntüsü.

Düğme sözde öğeleri

Düğmenin kenarlıkları geleneksel kenarlıklar değildir, mutlak konumda kenarlıklı sözde öğelerdir.

Chrome Devtools Elements panelinin ekran görüntüsünde
::before ve ::after öğelerine sahip bir düğme gösteriliyor.

Bu öğeler, oluşturulan 3D perspektifi sergilemede büyük önem taşır. Bu sözde öğelerden biri düğmeden uzağa itilir ve biri kullanıcıya yaklaştırılır. Bu efekt en çok üstteki ve alttaki düğmelerde görünür.

.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 dönüştürme stilleri

Çocukların z ekseninde boşluk bırakabilmeleri için transform-style altındaki değerin preserve-3d değerine ayarlanması gerekir. transform, --distance özel mülkü olarak ayarlandı. Bu mülk, fareyle üzerine gelindiğinde ve odaklanıldığında artırılacak.

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

Koşullu animasyon stilleri

Kullanıcı hareket edebilirse düğme, tarayıcıya dönüşüm özelliğinin değişim için hazır olması gerektiğini ve transform ile background-color özellikleri için bir geçişin ayarlandığını belirtir. Süredeki farka dikkat edin. Farkı, hoş, hafif, kademeli bir etki yarattığını hissettim.

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

Fareyle üzerine gelme ve odaklanma etkileşim stilleri

Etkileşim animasyonunun amacı, düz görünen düğmeyi oluşturan katmanları yaymaktır. --distance değişkenini başlangıçta 1px değerine ayarlayarak bunu başarabilirsiniz. Aşağıdaki kod örneğinde gösterilen seçici, düğmenin üzerine gelip gelmediğini veya odak göstergesi görmesi gereken bir cihazın bu düğmeye odaklanıp odaklanmadığını ve etkinleştirilmediğini kontrol eder. Bu durumda, aşağıdakileri yapmak için CSS'yi uygular:

  • Fareyle üzerine gelme arka plan rengini uygulayın.
  • Mesafeyi artırın .
  • Zıplama yumuşatma efekti ekleyin.
  • Sözde öğe geçişlerini bölümlere ayırın.
.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 }
    }
  }
}

3D perspektif, reduced hareket tercihi için yine de çok düzgündu. Üst ve alt öğeler efekti hoş ve ince bir şekilde gösteriyor.

JavaScript'teki küçük geliştirmeler

Arayüz klavye, ekran okuyucu, oyun kumandası, dokunmatik ekran ve fare ile kullanılabilir. Ancak birkaç senaryoyu kolaylaştırmak için JavaScript'e küçük dokunuşlar ekleyebiliriz.

Destekleyici ok tuşları

Sekme tuşu menüde gezinmenin iyi bir yoludur ancak yön tuşlarının veya kontrol çubuklarının odağı bir oyun kumandasına taşımasını beklerim. Genellikle GUI Yarışma arayüzleri için kullanılan roving-ux kitaplığı, ok tuşlarını bizim için yönetir. Aşağıdaki kod, kitaplığa odağı .threeD-button-set içinde yakalamasını ve odağı düğme alt öğelerine yönlendirmesini söyler.

import {rovingIndex} from 'roving-ux'

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

Fare paralaks etkileşimi

Fareyi takip etme ve menüyü yatırmayı sağlama amacı, AR ve VR video oyunu arayüzlerini taklit etmektir. Bu arayüzde, fare yerine sanal bir işaretçi bulunabilir. Öğeler işaretçinin çok farkında olduğunda eğlenceli olabilir.

Bu küçük, ekstra bir özellik olduğundan etkileşimi kullanıcının hareket tercihi sorgusunun arkasına koyacağız. Ayrıca kurulumun bir parçası olarak düğme listesi bileşenini querySelector ile belleğe kaydedin ve öğenin sınırlarını menuRect üzerinde önbelleğe alın. Fare konumuna göre karta uygulanan döndürme ofsetini belirlemek için bu sınırları kullanın.

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

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

Ardından, farenin x ve y konumlarını kabul eden ve kartı döndürmek için kullanabileceğimiz bir değer döndüren bir işleve ihtiyacımız var. Aşağıdaki işlev, kutunun içinde hangi tarafın olduğunu ve ne kadarının olduğunu belirlemek için fare konumunu kullanır. Delta, işlevden döndürülür.

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

Son olarak, fare hareketini izleyin, konumu getAngles() işlevimize iletin ve delta değerlerini özel mülk stilleri olarak kullanın. 20'ye bölerek deltayı doldurmayı ve titremeyi azaltıyorum. Bunu yapmanın daha iyi bir yolu olabilir. Baştan hatırlarsanız --x ve --y öğelerini bir clamp() işlevinin ortasına koyarız. Böylece fare pozisyonunun kartı okunaklı olmayan bir konuma aşırı döndürmesi önlenmiş olur.

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

Çeviriler ve talimatlar

Oyun menüsünü diğer yazma modlarında ve dillerde test ederken bir sorun oldu.

<button> öğeleri, kullanıcı aracısı stil sayfasında writing-mode için !important stiline sahip. Bu da oyun menüsü HTML'sinin istenen tasarıma uymak için değişmesi gerektiği anlamına geliyordu. <a> öğelerinin tarayıcı tarafından sağlanan bir !important stili olmadığından, düğme listesini bir bağlantı listesiyle değiştirmek mantıksal özelliklerin menü yönünü değiştirebilmesini sağlar.

Sonuç

Artık bunu nasıl yaptığımı biliyorsunuz. Hareketsiz deneyimi iyileştirebilir miyiz?

Gelin, yaklaşımlarımızı çeşitlendirelim ve web'de içerik geliştirmenin tüm yollarını öğrenelim. Demo oluşturup beni tweet'le bağlantıları oluşturduğumda bunu aşağıdaki topluluk remiksleri bölümüne ekleyeceğim.

Topluluk remiksleri

Henüz burada görülecek bir şey yok.