3D oyun menü bileşeni oluşturma

Duyarlı, uyarlanabilir ve erişilebilir bir 3D oyun menüsü oluşturmaya dair temel bilgiler.

Bu yayında, 3D oyun menüsü bileşeni oluşturma konusundaki düşüncelerimi 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 mekanda 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 bir renk şeması ve hareketi azaltmayı tercih eden kullanıcılar için düzenlemeler de 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 konum değerlerini tutar. Özel mülkleri adlandırmak, değerin kullanım alanını veya sonucunun kullanıcı dostu adını 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 cyan ile deeppink arasında canlı bir konik degrade bulunur. Koyu temada ise koyu ve ince bir konik degrade bulunur. 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 arka plan değiştirmeyi gösteren gösterim.

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 yapmanın bir yolu aşağıda açıklanmıştır.

Düğme grubu düzeni

Flexbox, kapsayıcı düzenini yönetebilir. flex-direction ile esnek öğenin varsayılan yönünü satırlardan sütunlara değiştirin ve align-items için stretch yerine start değerini kullanarak her öğenin içeriğinin 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. Sınırın orta değerinin özel bir özellik olduğunu unutmayın. Bu --x ve --y değerleri, daha sonra fare etkileşimi üzerine 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üştürmelerde transition ayarlayarak ara değer hesaplamayı 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. Fare bileşenle etkileşime geçemese veya etkileşime girmese bile kartın 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. Öğede ::marker gösterilmemesi için display stili değiştirilir. 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üğmelere stil uygulamak zor olabilir. Dikkate alınması gereken çok sayıda 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

Aşağıda, çocukların z ekseninde birbirlerinden uzaklaşabilmeleri için transform-style preserve-3d olarak ayarlanmıştır. transform, --distance özel özelliğine ayarlanır. Bu özellik, fareyle üzerine gelindiğinde ve odaklanıldığında artar.

.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 farklılığa 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. Bunu yapmak için --distance değişkenini başlangıçta 1px olarak ayarlayın. 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 gelindiğinde gösterilecek arka plan rengini uygulayın.
  • Mesafeyi artırın .
  • Sıçrama kolaylığı 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 }
    }
  }
}

reduced hareket tercihi için 3D perspektif 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 odaklanmayı .threeD-button-set içinde yakalamasını ve düğme alt öğelerine iletmesini 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. Öğelerin işaretçiye karşı aşırı duyarlı olması 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, kurulum kapsamında düğme listesi bileşenini querySelector ile bellekte depolayın ve öğenin sınırlarını menuRect içinde ö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. Değişiklik 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. Deltayı doldurmak ve daha az seğirme yapması için 20'ye böldüm. 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 engeller.

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. Düğme listesini bağlantı listesine dönüştürmek, <a> öğelerinde tarayıcı tarafından sağlanan bir !important stili olmadığından mantıksal özelliklerin menü yönünü değiştirmesini 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. Bir demo oluşturun, bağlantılarını bana tweetleyin. Ardından, aşağıdaki topluluk remiksleri bölümüne ekleyeceğim.

Topluluk remiksleri

Henüz burada gösterilecek bir şey yok.