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 yayında, 3D oyun menüsü bileşeni oluşturmanın bir yolunu paylaşmak istiyorum. Demoyu deneyin.

Demo

Videoyu tercih ediyorsanız bu yayının YouTube sürümünü burada bulabilirsiniz:

Genel Bakış

Video oyunları genellikle kullanıcılara animasyonlu ve 3D bir alanda yaratıcı ve sıra dışı bir menü sunar. Yeni AR/VR oyunlarında menünün havada süzülüyormuş gibi görünmesi popüler bir özelliktir. Bugün bu efektin temel özelliklerini yeniden oluşturacağız. Ancak bu kez, uyarlanabilir renk şeması ve hareketi azaltmayı tercih eden kullanıcılar için de düzenlemeler yapacağız.

HTML

Oyun menüsü, düğmelerin listesidir. 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üğme listesi, ekran okuyucu teknolojilerine iyi bir şekilde duyurulur ve JavaScript veya CSS olmadan çalışır.

Öğe olarak normal düğmeler içeren, çok genel görünümlü bir madde listesi.

CSS

Düğme listesine stil uygulama işlemi aşağıdaki genel adımlara ayrılır:

  1. Özel mülkler oluşturma.
  2. Flexbox düzeni.
  3. Dekoratif sözde öğeler içeren özel bir düğme.
  4. Öğeleri 3D uzaya yerleştirme.

Özel özelliklere genel bakış

Özel mülkler, rastgele görünen değerlere anlamlı adlar vererek değerlerin anlamını netleştirmeye yardımcı olur, tekrarlanan koddan ve değerler arasında paylaşımdan kaçınır.

Aşağıda, özel medya olarak da bilinen CSS değişkenleri olarak kaydedilen medya sorguları verilmiştir. Bunlar geneldir ve kodun kısa ve okunaklı kalması için çeşitli seçicilerde kullanılır. Oyun menüsü bileşeni, hareket tercihlerini, sistem renk şemasını ve ekranın 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 mülkler, renk düzenini yönetir ve fareyle üzerine gelindiğinde oyun menüsünü etkileşimli hale getirmek için fare konumsal değerlerini tutar. Özel mülkleri adlandırmak, değerin kullanım alanını veya değerin sonucu için kolay anlaşılır bir ad gösterdiğinden kodun okunabilirliğine 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 koni arka planları

Açık temada canlı cyan-deeppink konik gradya, koyu temada ise koyu ve ince bir konik gradyan vardır. Konik degradelerle neler yapılabileceği hakkında daha fazla bilgi edinmek için conic.style konusuna 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 bir web sayfasının 3D alanında var olabilmesi için perspektif içeren bir görüntü alanının başlatılması gerekir. Perspektifi body öğesine yerleştirmeyi seçtim ve beğendiğim stili oluşturmak için ekran alanı birimlerini kullandım.

body {
  perspective: 40vw;
}

Bu, bakış açısının sahip olabileceği etki türüdür.

<ul> düğme listesini biçimlendirme

Bu öğe, genel düğme listesi makro düzeninden ve etkileşimli ve 3D yüzen bir kart olmaktan 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;
}

Ardından, kapsayıcıyı 3D alan bağlamı olarak oluşturun ve kartın okunabilir rotasyonlardan daha fazla dönmemesini sağlamak 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ı hareketi kabul ediyorsa tarayıcıya bu öğenin dönüştürme işleminin will-change ile sürekli değişeceğini belirten bir ipucu ekleyin. Ayrıca, dönüşümlerde transition ayarlayarak interpolasyonu etkinleştirin. Bu geçiş, fare kartla etkileşime geçtiğinde gerçekleşir ve döndürme değişikliklerine sorunsuz geçişler 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ı, 0% ve 100%'i varsayılan olarak öğenin varsayılan stiline ayarlayacağından rotate-y animasyonu yalnızca orta animasyon karesini 50% olarak ayarlar. Bu, aynı konumda başlaması ve bitmesi gereken, sırayla oynatılan animasyonlar için kısayoldur. Bu, sonsuz olarak değişen animasyonları ifade etmenin mükemmel bir yoludur.

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

<li> öğelerine stil uygulama

Her liste öğesi (<li>), düğmeyi ve kenar öğelerini içerir. display stili değiştirildiğinde öğede ::marker gösterilmez. position stili, sonraki düğme sözde öğelerinin kendilerini düğmenin kapladığı alanın tamamında konumlandırabilmesi için relative olarak ayarlanır.

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

Perspektifi göstermek için 3D uzayda döndürülen listenin ekran görüntüsü. Artık her liste öğesinde madde işareti yok.

<button> öğelerine stil uygulama

Düğmelerin stilini belirlemek zor olabilir. Hesaba katılacak birçok durum ve etkileşim türü vardır. Sözde öğeler, animasyonlar ve etkileşimlerin dengelenmesi nedeniyle bu düğmeler hızla 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;
}

Bu kez stilize düğmelerle 3D perspektifte düğme listesinin ekran görüntüsü.

Düğme sözde öğeleri

Düğmenin kenarları geleneksel kenarlık değildir, kenarlık içeren mutlak konumlu sözde öğelerdir.

::before ve ::after öğelerinin gösterildiği bir düğmenin yer aldığı Chrome Geliştirici Araçları Öğeler panelinin ekran görüntüsü.

Bu öğeler, oluşturulan 3D perspektifi göstermek için çok önemlidir. Bu sözde öğelerden biri düğmeden uzağa, diğeri ise kullanıcıya daha yakın çekilir. Bu etki en çok üst ve alt düğmelerde fark edilir.

.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ı hareketi kabul ederse düğme, tarayıcıya dönüştürme özelliğinin değişikliğe hazır olması gerektiğini ve transform ile background-color mülkleri için bir geçiş ayarlandığını belirtir. Süredeki farka dikkat edin. Bu, hoş ve zarif bir kademeli efekt oluşturdu.

.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 fareyle üzerine gelinip gelinmediğini veya odak göstergesi görmesi gereken bir cihaz tarafından odaklanıp odaklanmadığını ve etkinleştirilip etkinleştirilmediğini kontrol eder. Bu durumda, aşağıdakileri yapmak için CSS uygular:

  • Fareyle üzerine gelme arka plan rengini uygulayın.
  • Mesafeyi artırın.
  • Zıplama yumuşatma efekti ekleyin.
  • Sanal öğe geçişlerini kademeli olarak ayarlayı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 şıktı. Üst ve alttaki öğeler, efekti hoş ve ince bir şekilde gösterir.

JavaScript ile küçük iyileştirmeler

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

Ok tuşlarını destekleme

Sekme tuşu, menüde gezinmek için iyi bir yöntemdir ancak gamepad'de odağı taşımak için yön çubuğu veya kontrol çubuklarının kullanılmasını beklerdim. GUI Challenge arayüzleri için genellikle 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 edip menüyü yatırmak, fare yerine sanal bir işaretçiniz olabileceği AR ve VR video oyunu arayüzlerini taklit etmeyi amaçlar. Öğeler işaretçinin çok farkında olduğunda bunu yapmak eğlenceli olabilir.

Bu küçük bir ek özellik olduğundan etkileşimi, kullanıcının hareket tercihine yönelik bir sorgunun arkasına yerleştireceğiz. 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, fare 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, fare konumunu kullanarak farenin kutunun hangi tarafında olduğunu ve ne kadar uzakta olduğunu belirler. 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, farenin 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şlangıçta --x ve --y öğelerini bir clamp() işlevinin ortasına koyduğumuzu hatırlıyorsanız. Bu, fare konumunun kartı okunamayacak bir konuma döndürmesini önler.

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

Oyun menüsünü diğer yazım modlarında ve dillerde test ederken bir sorunla karşılaştık.

<button> öğeleri, kullanıcı aracısı stil sayfasında writing-mode için !important stiline sahiptir. Bu nedenle, oyun menüsü HTML'sinin istenen tasarıma uygun şekilde değiştirilmesi gerekiyordu. <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ç

Bunu nasıl yaptığımı öğrendiğinize göre, siz nasıl yapardınız? 🙂 Telefonunuzu kaydırarak menüyü döndürmek için menüye ivmeölçer etkileşimi ekleyebilir misiniz? Hareketsiz görüntü deneyimini iyileştirebilir miyiz?

Yaklaşımlarımızı çeşitlendirelim ve web'de uygulama 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.