בניית רכיב בתפריט של משחק בתלת-ממד

סקירה כללית בסיסית של תהליך היצירה של תפריט משחקים בתלת-ממד רספונסיבי, נגיש ונגיש.

בפוסט הזה אני רוצה לשתף אתכם במחשבה על דרך לפיתוח רכיב בתפריט משחקים בתלת-ממד. כדאי לנסות את demo.

הדגמה

אם אתם מעדיפים סרטון, הנה גרסה של YouTube לפוסט:

סקירה כללית

משחקי וידאו מציגים למשתמשים בדרך כלל תפריט יצירתי ויוצא מן הכלל, ובמרחב תלת-ממדי. השימוש הזה פופולרי במשחקי AR/VR חדשים, כי הוא גורם לתפריט להיראות כמו צף בחלל. היום נחזור על יסודות האפקט הזה, עם הסגנון הנוסף של ערכת צבעים מותאמת וסביבה שמתאימה למשתמשים שמעדיפים תנועה מופחתת.

HTML

תפריט משחק הוא רשימת לחצנים. הדרך הטובה ביותר לייצג את זה ב-HTML היא ככה:

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

רשימת לחצנים תודיע על עצמה היטב לטכנולוגיות של קוראי מסך פועלת ללא JavaScript או CSS.

A
רשימת תבליטים כללית מאוד עם לחצנים רגילים כפריטים.

CSS

העיצוב של רשימת הלחצנים מתחלק לשלבים הכלליים הבאים:

  1. הגדרת מאפיינים מותאמים אישית.
  2. פריסת Flexbox.
  3. לחצן בהתאמה אישית עם פסאודו אלמנטים דקורטיביים.
  4. הצבת אלמנטים במרחב בתלת-ממד.

סקירה כללית של מאפיינים מותאמים אישית

מאפיינים מותאמים אישית עוזרים להבדיל בין ערכים באמצעות שהם מספקים משמעות באופן אקראי אחר, תוך הימנעות מקוד חוזר ומשיתוף בקרב ילדים.

בהמשך מוצגות שאילתות מדיה שנשמרו כמשתני CSS, שנקראים גם בהתאמה אישית מדיה. הן גלובליות ישמשו את הבוררים השונים כדי שהקוד יהיה תמציתי וקריא. רכיב תפריט המשחק משתמש בתנועה העדפות, צבע מערכת scheme, ו-טווח הצבעים יכולות של מסך.

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

המאפיינים המותאמים אישית הבאים מנהלים את ערכת הצבעים ומחזיקים את העכבר ערכי מיקום שהופכים את תפריט המשחק לאינטראקטיבי שמאפשר העברת עכבר. מתן שמות בהתאמה אישית עוזרים לקוד להיות קריא כי הוא חושף את התרחיש לדוגמה של הערך שם ידידותי עבור תוצאת הערך.

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

רקעים של חרוטים בעיצוב בהיר ועיצוב כהה

לעיצוב הבהיר יש צבעים עזים בין cyan ל-deeppink הדרגתי בעוד שלעיצוב הכהה יש הדרגתיות של חרוט עדין כהה. מידע נוסף על יכולה להיות הדרגתיות של חרוטים, ראו conic.style.

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

  @media (--dark) {
    background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
  }
}
הדגמה של שינוי הרקע בין העדפות של צבע בהיר להעדפות של צבע כהה.

הפעלת נקודת מבט תלת-ממדית

לרכיבים קיימים בשטח התלת-ממדי של דף אינטרנט, אזור תצוגה עם נקודת מבט צריך לאתחל אותו. בחרתי להוסיף את הפרספקטיבה לאלמנט body ושימוש ביחידות של אזור תצוגה כדי ליצור את הסגנון שאהבתי.

body {
  perspective: 40vw;
}

זה סוג ההשפעה של נקודת המבט.

עיצוב רשימת הלחצנים <ul>

הרכיב הזה אחראי לפריסת המאקרו הכוללת של רשימת הלחצנים וגם לפריסת המאקרו של רשימת הלחצנים שהוא כרטיס צף אינטראקטיבי ותלת-ממדי. הנה דרך לעשות זאת.

פריסה של קבוצת לחצנים

עם Flexbox אפשר לנהל את פריסת הקונטיינרים. שינוי כיוון ברירת המחדל של הגדרת ברירת המחדל משורות עד עמודות עם flex-direction ומוודאים שכל פריט הוא בגודל של על ידי שינוי התוכן שלו מ-stretch ל-start עבור 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;
}

בשלב הבא, יוצרים את המאגר בתור הקשר תלת-ממדי של מרחב ומגדירים את clamp() של שירות ה-CSS. כדי להבטיח שהכרטיס לא מסתובב מעבר לסיבובים הקריאים. הודעה שהערך האמצעי של ההצמדה הוא מאפיין מותאם אישית, --x ו---y האלה הערכים יוגדרו מ-JavaScript בזמן העכבר על האינטראקציה שלי מאוחר יותר.

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

בשלב הבא, אם התנועה מתאימה למשתמש שמבקר, מוסיפים רמז לדפדפן הטרנספורמציה של פריט זה תשתנה כל הזמן עם will-change בנוסף, מפעילים אינטרפולציה על ידי הגדרת transition לטרנספורמציות. הזה המעבר יתרחש כשהעכבר מקיים אינטראקציה עם הכרטיס, מה שמאפשר למעברים לשינויים בסבב. האנימציה היא אנימציה שפועלת כל הזמן שמדגים את השטח התלת-ממדי שבו הכרטיס נמצא, גם אם עכבר לא יכול אין אינטראקציה עם הרכיב.

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

האנימציה rotate-y מגדירה רק את תמונת המפתח האמצעית ב-50%, כי הדפדפן ישתמש בברירת המחדל של 0% וב-100% לפי סגנון ברירת המחדל של הרכיב. הזה הוא קיצור של אנימציות שמתחלפות, וצריכות להתחיל ולהסתיים באותו אופן המיקום. זוהי דרך נהדרת לנסח אנימציות מתחלפות אינסופית.

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

עיצוב הרכיבים של <li>

כל פריט ברשימה (<li>) מכיל את הלחצן ואת רכיבי הגבול שלו. הסגנון של display השתנה כך שהפריט לא מציג ::marker. הסגנון position מוגדר ל-relative כך שרכיבי ה-מופע של הלחצנים הבאים יוכלו למקם בתוך כל האזור שהלחצן צורך.

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

צילום מסך של הרשימה שסובבה במרחב התלת-ממדי כדי להראות את נקודת המבט.
כל פריט ברשימה כבר לא מכיל תבליט.

עיצוב הרכיבים של <button>

לא קל לעצב לחצנים, כי יש הרבה מצבים וסוגי אינטראקציה לקחת בחשבון. הלחצנים האלה הופכים למורכבים במהירות בגלל איזון פסאודו-אלמנטים, אנימציות ואינטראקציות.

<button> סגנונות ראשוניים

בהמשך מפורטים סגנונות הבסיס שיתמכו במדינות האחרות.

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

צילום מסך של רשימת הלחצנים מנקודת מבט תלת-ממדית, הפעם עם סגנון
הלחצנים.

אלמנטים של כפתורים

גבולות הלחצן אינם גבולות מסורתיים, הם מיקום מוחלט אלמנטים פסאודוניים עם גבולות.

צילום מסך של חלונית הרכיבים של Chrome Devtools, עם לחצן שמוצג בו
::before ו-::after.

הרכיבים האלה חיוניים להצגת נקודת המבט התלת-ממדית שמוצגת נוצרה. אחד מהאלמנטים המדומים האלה יידחף מהלחצן, ואחד מהם יילקח קרוב יותר אל המשתמש. ההשפעה מובהקת ביותר על הלחצנים העליונים והתחתונים.

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

סגנונות טרנספורמציה בתלת-ממד

הערך מתחת לערך transform-style מוגדר לערך preserve-3d כדי לאפשר לילדים מרחב משותף על ציר ה-z. הערך transform מוגדר ל---distance לנכס מותאם אישית, שיגדל במעבר מעל המיקוד.

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

סגנונות אנימציה מותנית

אם אין בעיה בתנועה, הלחצן מרמז לדפדפן נכס הטרנספורמציה צריך להיות מוכן לשינוי ומוגדר מעבר transform ו-background-color נכסים. שימו לב להבדל משך הזמן, הרגשתי שהוא נוצר עבור אפקט עדין ונחמד.

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

סגנונות אינטראקציה של העברת עכבר והתמקדות

המטרה של אנימציית האינטראקציה היא לפזר את השכבות שמרכיבות את לחצן שמופיע. כדי לעשות זאת, מגדירים את המשתנה --distance, בהתחלה ל-1px. הבורר שמוצג בדוגמה הבאה של הקוד בודק לבדוק אם המכשיר צריך להעביר את הלחצן או להעביר אותו למיקום אחר ומחוון המיקוד לא מופעל. אם כן, נעשה שימוש בשירות CSS כדי הבאים:

  • מחילים את צבע הרקע של העברת העכבר.
  • הגדלת המרחק .
  • הוספת אפקט של קלי קלות.
  • מניעים את המעברים המדומים.
.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. הרכיבים העליונים והתחתונים מציגים את האפקט בצורה עדינה.

שיפורים קטנים באמצעות JavaScript

ניתן להשתמש בממשק במקלדות, בקוראי מסך, בגיימפאד, במגע וב כבר עכבר, אבל אנחנו יכולים להוסיף כמה נגיעות קלות של JavaScript כדי להקל מגוון רחב של תרחישים.

מקשי החיצים תומכים

מקש ה-Tab הוא דרך טובה לנווט בתפריט, אבל ציפיתי להכוונה באמצעות פד או ג'ויסטיקים כדי להעביר את המיקוד לגיימפאד. ספריית roving-ux שמשמשת לעיתים קרובות ל-GUI ממשקי האתגר יטפלו במקשי החיצים עבורנו. הקוד הבא אומר לספרייה כדי לתפוס את המוקד בתוך .threeD-button-set ולהעביר את המיקוד לילדים.

import {rovingIndex} from 'roving-ux'

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

אינטראקציה עם פרלקס של עכבר

מעקב אחר העכבר והטיית התפריט נועדו לדמות AR ו-VR ממשקים של משחקי וידאו, שבהם יש מצביע וירטואלי במקום עכבר. זה יכול להיות כיף כשאלמנטים מאוד מודעים למצביע.

מאחר שזו תכונה קטנה נוספת, נציב את האינטראקציה מאחורי שאילתה של העדפות התנועה של המשתמש. בנוסף, כחלק מתהליך ההגדרה, צריך לאחסן את רשימת הלחצנים את הרכיב בזיכרון עם querySelector ושומרים את גבולות הרכיב במטמון menuRect. השתמשו בגבולות האלה כדי לקבוע את היסט הסיבוב שחל על הכרטיס על סמך המיקום של העכבר.

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

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

בשלב הבא אנחנו צריכים פונקציה שמקבלת את מיקומי העכבר x ו-y ומחזירה ערך שאנחנו יכולים להשתמש בו כדי לבצע רוטציה של הכרטיס. הפונקציה הבאה משתמשת בעכבר כדי לזהות באיזה צד של הקופסה היא נמצאת ובאיזה אחוז. והפונקציה 'דלתא' מוחזרת.

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

לסיום, בודקים את הזזת העכבר ומעבירים את המיקום לפונקציה getAngles() ולהשתמש בערכי דלתא בתור סגנונות נכס מותאמים אישית. חילקתי ב-20 כדי לאמן את ולהפוך אותה לפחות למקפצת, אולי יש דרך טובה יותר לעשות את זה. אם כדאי לזכור, מההתחלה, שמים את האביזרים --x ו---y באמצע clamp(), שמונעת מעבר יתר של מיקום העכבר על והכרטיס במצב לא קריא.

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

תרגומים ומסלולים

הייתה תוצאה אחת שבדקתי את תפריט המשחק במצבי כתיבה אחרים בשפות שונות.

לרכיבי <button> יש סגנון !important בשביל writing-mode במשתמש גיליון סגנונות של הסוכן. כלומר, ה-HTML של תפריט המשחק היה נדרש לשינוי כדי להתאים של העיצוב הרצוי. שינוי רשימת הלחצנים לרשימת קישורים מפעיל כדי לשנות את כיוון התפריט, כי לרכיבי <a> אין דפדפן סיפקת את הסגנון !important.

סיכום

עכשיו, אחרי שהבנת איך עשית את זה, איך אפשר 🙂 אפשר להוסיף מד תאוצה אינטראקציה בתפריט, אז ריצוף הטלפון מסובב את התפריט? האם אפשר להשתפר חוויית 'ללא תנועה'?

בואו לגוון את הגישות שלנו ונלמד את כל הדרכים לבניית אתרים באינטרנט. אפשר ליצור הדגמה, לשלוח לי קישורים של שלחו לי ציוץ ואני אוסיף אותם לקטע 'רמיקסים' של הקהילה שבהמשך.

רמיקסים קהילתיים

אין כאן שום דבר עדיין לראות!